-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
445: Implementing Common Sorting Algorithms in Python
Look, I'll be the first to admit it: in a production environment, you should almost never write your own sorting algorithm. Python's list.sort() and sorted() use Timsort, which is a highly optimized hybrid that will outperform almost anything you can whip up in a few minutes. But if you're walking into a technical interview or trying to optimize a very specific data structure, you need to understand the mechanics of how we get from a chaotic list to an ordered one.
The trap of the nested loop
When people first try to implement a sort, they usually gravitate toward something like Bubble Sort. It feels intuitive because it mimics how you might physically organize a hand of cards: you look at two items, swap them if they're in the wrong order, and repeat. It’s the "brute force" approach to sorting.
def bubble_sort(data):
n = len(data)
for i in range(n):
for j in range(0, n - i - 1):
if data[j] > data[j + 1]:
data[j], data[j + 1] = data[j + 1], data[j]
return data
If you're sorting a list of ten transaction amounts, this is fine. But if you're sorting ten thousand, you're going to hit a wall. The problem here is the nested loop. For every element in the list, we are potentially scanning the rest of the list. In big-O notation, we call this $O(n^2)$. I've seen developers try to "fix" this by adding a boolean flag to stop early if no swaps occurred, but that's just putting a band-aid on a fundamental architectural flaw. The cost grows quadratically; double your data, and you quadruple your wait time.
Trading memory for speed
To get a real performance boost, we have to stop thinking linearly and start thinking recursively. This is where Merge Sort comes in. Instead of swapping neighbors, Merge Sort uses a "divide and conquer" strategy. It splits the list in half, sorts those halves, and then merges them back together. It feels like more work upfront, but the efficiency gain is massive.
def merge_sort(data):
if len(data) <= 1:
return data
mid = len(data) // 2
left = merge_sort(data[:mid])
right = merge_sort(data[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
Now we're talking $O(n \log n)$. This is a night-and-day difference. While Bubble Sort is struggling to breathe with 100,000 items, Merge Sort handles it with ease. But there's a catch—the trade-off is memory. Notice how I'm creating new lists (left, right, and result) during every recursive call? Bubble Sort sorts "in-place," meaning it uses almost no extra RAM. Merge Sort is memory-hungry. In a constrained environment, like an embedded system or a massive dataset that barely fits in memory, that trade-off becomes a critical decision.
The stability factor
One thing I want you to keep in mind is "stability." A sorting algorithm is stable if it preserves the original relative order of elements with equal keys. If you have a list of orders already sorted by date, and you then sort them by customer name using a stable sort (like Merge Sort), the orders for a single customer will still be in date order. Bubble Sort is also stable, but many "faster" algorithms, like Quick Sort, are not. When you're dealing with real-world objects rather than just integers, stability is often the difference between a feature that works and a bug that haunts your weekends.
📋 Practical Task
Implementing a Stable Merge Sort for Customer Order IDs
You have been given a list of tuples representing customer orders. Each tuple contains (order_id, priority). The list is already sorted by order_id, but you need to sort it by priority (where lower numbers mean higher priority) while maintaining the original order_id sequence for orders with the same priority.
Your Task: Implement the merge_sort_orders function using the Merge Sort logic discussed in the lesson. Ensure that your implementation is stable.
def merge_sort_orders(orders):
# Your code here
pass
# Test Case
orders = [
(101, 2),
(102, 1),
(103, 2),
(104, 1),
(105, 3)
]
# Expected Result: [(102, 1), (104, 1), (101, 2), (103, 2), (105, 3)]
sorted_orders = merge_sort_orders(orders)
print(sorted_orders)There are no comments for now.