Lecture W2M1: Insertion Sort, Merge Sort, and What They Cost
August 31, 2026
By the end of this class, you will be able to:
We concluded by mentioning the Set interface, which stores items by key and its questions are about values, not positions: find(k), find_min(), find_max(), find_next(k). 🛍️
find_min(): you must examine all \(n\) cells, because any cell you skip could be hiding the minimum. That is \(\Theta(n)\), and no cleverness avoids it.find(k) has the same problem: a position tells you nothing about which key lives there, so the search scans until it finds \(k\) or runs out. \(O(n)\) again.If the very same items sat in an array sorted by key, positions would suddenly mean something, and every Set question gets cheaper:
| Operation | Unsorted array | Sorted array |
|---|---|---|
find_min(), find_max() |
\(O(n)\) | \(O(1)\): look at the ends |
find(k) |
\(O(n)\) | \(O(\log n)\): binary search |
find_next(k), find_prev(k) |
\(O(n)\) | \(O(\log n)\) |
find_min() is the cleanest example: sorted order pins the minimum to index \(0\), so the scan that was unavoidable becomes a single read.
The missing piece is the one this table quietly assumes: someone has to put the array in sorted order first. Today we ask how, and at what cost.
Sorting. Given an array \(A\) of \(n\) numbers, output an array \(B\) that is a permutation of \(A\) (same elements, possibly reordered) and is sorted: \(B[i-1] \leq B[i]\) for every \(i \in \{1, \ldots, n-1\}\).
Sorting is a problem, not an algorithm. Monday of week 1 taught us to keep those separate, and today we dance with three different algorithms for this one problem. 💃🏼
The specification practically dares us: some permutation of \(A\) is sorted, so check them all.
Correctness is not enough. This course exists because of the gap between an answer and an answer you can afford.
You already run this algorithm when you pick up a hand of cards: keep the cards you hold sorted, and tuck each new card into its place.
| Round | Array | What happened |
|---|---|---|
| start | \((7, 3, 9, 2, 6)\) | \(A[0]\) alone is sorted |
| \(i = 1\) | \((3, 7, 9, 2, 6)\) | key \(3\) moved left past \(7\) |
| \(i = 2\) | \((3, 7, 9, 2, 6)\) | key \(9\) already in place |
| \(i = 3\) | \((2, 3, 7, 9, 6)\) | key \(2\) moved left past \(9, 7, 3\) |
| \(i = 4\) | \((2, 3, 6, 7, 9)\) | key \(6\) moved left past \(9, 7\) |
One sentence: grow a sorted prefix, one key at a time.




Invariant. At the start of the round that inserts key \(A[i]\), the prefix \(A[0..i-1]\) is sorted.
Every correctness proof this semester will have this base-case-plus-inductive-step shape.
Worst case, the key travels all the way left every round (a reverse-sorted input does exactly this):
\[T(n) = \underbrace{c \cdot 1 + c \cdot 2 + \cdots + c \cdot (n-1)}_{\text{round } i \text{ costs at most } c \cdot i} = c \cdot \frac{n(n-1)}{2} = \Theta(n^2).\]

Round \(i\) inserts key \(A[i]\) into a sorted prefix of length \(i\): at most \(i\) compare-and-swap steps, each costing at most a constant \(c\). The worst case pays full price in every round:
Sanity check at \(n = 5\): \(T(5) = c(1 + 2 + 3 + 4) = 10c\), and the triangle of work really does hold \(\frac{5 \cdot 4}{2} = 10\) units.
round 1 x
round 2 x x
round 3 x x x
round 4 x x x x
A reverse-sorted input hits every round’s bound, so the worst case really is \(\Theta(n^2)\); an already-sorted input pays one comparison per round, \(\Theta(n)\). The constants \(c\) and \(\tfrac{1}{2}\) vanished into \(\Theta\); the \(n^2\) could not.
Remember this superpower for the end of the class; it is why insertion sort never fully retired. 🏝️
Insertion sort nibbles: one key per round. Merge sort takes a completely different stance:
Example: \((5, 2, 7, 1, 9, 3, 8, 6)\) splits into \((5, 2, 7, 1)\) and \((9, 3, 8, 6)\); the recursion hands back \((1, 2, 5, 7)\) and \((3, 6, 8, 9)\); merging gives \((1, 2, 3, 5, 6, 7, 8, 9)\).
All the actual work hides in one question: how cheaply can we merge two sorted arrays?



def merge_sort(A): # T(n)
if len(A) <= 1: # base case: sorted
return A
mid = len(A) // 2
L = merge_sort(A[:mid]) # T(n/2)
R = merge_sort(A[mid:]) # T(n/2)
return merge(L, R) # O(n)
def merge(L, R): # two-finger walk
B, i, j = [], 0, 0
while i < len(L) or j < len(R):
if j >= len(R) or (i < len(L) and L[i] <= R[j]):
B.append(L[i]); i += 1 # smaller item is at L[i]
else:
B.append(R[j]); j += 1 # smaller item is at R[j]
return BNot in place: the merge needs \(\Theta(n)\) extra space for its output. That is part of the price tag we will weigh at the end.
Let \(T(n)\) be the cost of merge sort on \(n\) items. Reading the code:
\[T(1) = c, \qquad T(n) = \underbrace{2 \, T(n/2)}_{\text{two recursive sorts}} + \underbrace{c \cdot n}_{\text{split and merge}}.\]
Take one worksheet per pair.
Side 1: run merge sort on the array \((6, 2, 8, 4, 7, 1, 5, 3)\): fill in the splitting tree on the way down, then the merged results on the way back up.
Side 2: count the merge work level by level, total it, then generalize from \(n = 8\) to any \(n\).
Finish early? Take on the challenge question at the bottom of side 2.
12 minutes. Worksheets come in at the end; one per pair, both names on it.

Insertion sort’s recurrence unrolled into a chain and cost \(\Theta(n^2)\); merge sort’s unrolled into a balanced tree and cost \(\Theta(n \log n)\). The shape of the recursion decides the bill. On Wednesday we turn this picture into a theorem that solves whole families of recurrences at sight.
The tree summed the levels for us. Here is the same answer by pure algebra: substitute the recurrence into itself and watch the pattern grow.
Sanity check at \(n = 8\): three levels of merging copy \(8\) items each, \(24\) copies, plus \(8\) base cases: a rectangle of work, \(n\) wide and \(\log_2 n + 1\) tall.
level 0 x x x x x x x x
level 1 x x x x x x x x
level 2 x x x x x x x x
leaves x x x x x x x x
Triangle versus rectangle is the whole comparison: insertion sort stacks levels of growing width, \(n\) of them; merge sort stacks levels of constant width, only \(\log_2 n + 1\) of them.
| Algorithm | Worst case | Sorted input | Extra space | In place? |
|---|---|---|---|---|
| Permutation sort | \(\Omega(n! \cdot n)\) | \(O(n)\) | \(O(n)\) | no |
| Insertion sort | \(\Theta(n^2)\) | \(\Theta(n)\) | \(O(1)\) | yes |
| Merge sort | \(\Theta(n \log n)\) | \(\Theta(n \log n)\) | \(O(n)\) | no |

sorted and list.sort use Timsort, which merges sorted runs and hands small pieces to an insertion-style sort. Both of today’s algorithms are inside every Python program you have ever run.Same lesson as Wednesday’s data structures: there is no best sort, only a best sort for a workload. The analysis is what lets you name which.
Sorting is a problem; algorithms are bids to solve it. Brute force is correct and unaffordable. Insertion sort grows a sorted prefix: an induction proof, \(\Theta(n^2)\) worst case, unbeatable on nearly sorted input. Merge sort divides, recurses, and merges with two fingers: \(\Theta(n \log n)\) always, paid for with \(\Theta(n)\) extra space. And recursion trees turn recurrences into pictures you can total up.
Topics: Divide and conquer as a design pattern, and recurrences as its price tags: recursion trees in general, and the Master Theorem, which solves \(T(n) = a\,T(n/b) + f(n)\) on sight.
Supplemental reading (highly recommended): CLRS Chapter 4, in your own copy or online through Canvas/Perusall.
Upcoming deliverables:
