Python Tutorial
Merge Sort
A stable divide-and-conquer sort with a guaranteed O(n log n) — split, sort halves, then merge.
Divide, Conquer, Merge
Merge sort splits the array in half, recursively sorts each half, then merges the two sorted halves into one. Because merging two sorted lists is linear and the recursion is log-deep, the total is a guaranteed O(n log n) — in the best, average, and worst case.
Implementation
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid]) # sort left half
right = merge_sort(arr[mid:]) # sort right half
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]: # <= keeps it STABLE
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
result.extend(left[i:]) # append any remainder
result.extend(right[j:])
return result
print(merge_sort([38, 27, 43, 3, 9, 82, 10]))
# [3, 9, 10, 27, 38, 43, 82]The Merge Step Visualized
Merging [3, 27, 43] and [9, 10, 82]: compare fronts, take the smaller each time.
| Compare | Take | Result so far |
|---|---|---|
| 3 vs 9 | 3 | [3] |
| 27 vs 9 | 9 | [3, 9] |
| 27 vs 10 | 10 | [3, 9, 10] |
| 27 vs 82 | 27 | [3, 9, 10, 27] |
Complexity
| Case | Time |
|---|---|
| Best / Average / Worst | O(n log n) — always |
| Space | O(n) extra for merging |
| Stable? | Yes |
Its predictable performance and stability make merge sort the algorithm of choice when worst-case guarantees matter or when sorting linked lists and huge external files.
Merge Sort vs Quick Sort
- Merge sort: guaranteed O(n log n), stable, but O(n) extra memory.
- Quick sort: in-place and usually faster, but O(n²) worst case and not stable.
- External sorting of data too big for RAM uses merge sort's merge step across disk chunks.
Python's built-in Timsort is a hybrid of merge sort and insertion sort — it is stable and O(n log n), and exploits already-sorted runs for near-O(n) performance on real data.
Best Practices
- Choose merge sort when you need stability and a hard O(n log n) guarantee.
- Use
<=in the merge to preserve stability. - For linked lists, merge sort avoids random access and is the natural choice.
- In production, reach for
sorted()— you get Timsort's merge-sort benefits for free.
Try It Yourself
Exercise 1: Merge two sorted lists [1, 4, 7] and [2, 3, 8] by hand — what is the result?
Show solution
[1, 2, 3, 4, 7, 8] — repeatedly take the smaller front element.
Exercise 2: Name merge sort's one downside versus quicksort.
Show solution
It needs O(n) extra memory for merging, whereas quicksort sorts in place.
📘 Real-World Deep Dive
Knowing <strong>DSA Merge Sort (algorithms & data structures)</strong> well is what turns algorithms & data structures from a curiosity into a daily tool — you'll reach for it in nearly every real project.
Real-Life Scenario
An end-to-end usage of DSA Merge Sort that you'd actually see in a data pipeline or analytics notebook.
Real-Life Example
def msort(xs):
if len(xs) <= 1: return xs
mid = len(xs)//2
a = msort(xs[:mid]); b = msort(xs[mid:])
out, i, j = [], 0, 0
while i < len(a) and j < len(b):
if a[i] <= b[j]: out.append(a[i]); i += 1
else: out.append(b[j]); j += 1
out.extend(a[i:]); out.extend(b[j:])
return out
print(msort([38, 27, 43, 3, 9, 82, 10]))Expected Output
(see source)Common mistakes
- Off-by-one errors in binary-search: the standard idiom is
while lo <= hiwithmid = (lo + hi) // 2. - Recursive algorithms blow the stack for n > ~10⁴; convert to iterative with an explicit stack.
- Comparison algorithms (
sorted(iterable)) are stable by default in Python — surprising for Java/C++ users. - Treating DSA Merge Sort as a black box without reading the docs — the API has subtle defaults that bite when you scale.
🚀 Performance & Best Practices
- Use
bisect.bisect_left/bisect_rightinstead of writing your own binary search. - Convert a sorted search into a tuple-access pattern with
numpy.searchsortedfor huge arrays. - Use
heapqfor priority queues instead of maintaining a sorted list manually. - When working with algorithms & data structures, prefer vectorised / batched operations over Python loops.
🧪 Try It Yourself
- Reproduce the snippet on a representative slice of your own data.
- Profile the snippet with
cProfileortimeitand find the single biggest improvement. - Generalise the snippet into a small, reusable function you can drop into future projects.