Part I · Foundations Chapter 2

Getting Started

Two sorting algorithms, one proof technique, and the analysis vocabulary that the remaining thirty-three chapters assume you own.

Chapter 2 is where CLRS stops describing and starts doing. It presents insertion sort and proves it correct with a loop invariant; it sets up the RAM model and analyses insertion sort line by line to get the Θ(n²) worst case; then it introduces divide-and-conquer and derives merge sort, whose Θ(n lg n) cost comes out of a recurrence. Three techniques — invariants, line-by-line counting, and recurrences — get used constantly from here on. This is the chapter to reread if the later analysis ever feels like hand-waving.

4th edition notes. Two visible changes from the 3rd edition. Procedures now take the array length as an explicit parameter, so it is INSERTION-SORT(A, n) rather than relying on A.length. And MERGE no longer uses sentinels; it checks the indices directly, which is both closer to how you would really write it and easier to reason about at the boundaries. Array slices are written A[p:r].

Contents

  1. Insertion sort
  2. Tracing it by hand
  3. Loop invariants, and proving it correct
  4. Pseudocode conventions
  5. The RAM model
  6. Analysing insertion sort
  7. Worst case, best case, average case
  8. Order of growth
  9. Divide-and-conquer
  10. The MERGE procedure
  11. MERGE-SORT and its recursion
  12. Solving the recurrence
  13. Recap

Insertion sort

The chapter restates the sorting problem from Chapter 1, then adds a piece of vocabulary that matters in practice. The numbers being sorted are called keys, and in a real program a key rarely travels alone — it is attached to satellite data, and the key plus its satellite data forms a record. When CLRS shows an algorithm moving a key, understand it as moving the whole record, or a pointer to it. That detail decides whether a swap costs a few bytes or a few kilobytes, and it is why real implementations sort arrays of pointers.

The idea: insertion sort works the way most people sort a hand of playing cards. Keep the cards in your left hand sorted. Pick up the next card from the table, scan right to left through your hand until you find where it belongs, and slide it in. When the table is empty, your hand is sorted.

Two things about that analogy are exactly right. The sorted region grows by one element per round, and inserting means shifting the larger elements up to open a gap, not swapping repeatedly. Here it is in the book’s notation. The algorithm sorts in place: it rearranges A itself, using only a constant amount of extra storage.

INSERTION-SORT(A, n) 1 for i = 2 to n 2 key = A[i] 3 // Insert A[i] into the sorted subarray A[1:i-1]. 4 j = i - 1 5 while j > 0 and A[j] > key 6 A[j+1] = A[j] 7 j = j - 1 8 A[j+1] = key

Eight lines. The outer loop chooses the next element to place; the inner loop opens the gap by sliding larger elements one position right.

Read the inner loop carefully, because two details are easy to get wrong when you rewrite this from memory:

Note also that the loop starts at i = 2, not 1. A single element is already sorted, so there is nothing to do for A[1]. CLRS indexes arrays from 1.

1 2 3 4 5 6 index initial 5 2 4 6 1 3 ← sorted / unsorted boundary i = 2, key = 2 2 5 4 6 1 3 i = 3, key = 4 2 4 5 6 1 3 i = 4, key = 6 2 4 5 6 1 3 i = 5, key = 1 1 2 4 5 6 3 i = 6, key = 3 1 2 3 4 5 6 sorted so far the key just inserted untouched
Figure 2.1 — Insertion sort on ⟨5, 2, 4, 6, 1, 3⟩. The blue boundary moves right by exactly one position per iteration of the outer loop, and everything to its left is sorted. At i = 4 the key 6 is already in place, so the inner loop never runs.

Tracing it by hand

Figure 2.1 is worth walking through slowly once, because every claim in the correctness proof is visible in it.

The pattern to notice: the cost of one outer iteration is the number of elements the key has to travel past. Sum that over all i and you have the running time, which is exactly the analysis in §2.2.

Loop invariants, and proving it correct

The trace makes insertion sort look obviously right, but “obviously” is not a proof, and correctness is a claim about every input. The tool CLRS introduces here, and uses for the rest of the book, is the loop invariant: a property that holds before and after every iteration of a loop, chosen so that when the loop finally stops, the property gives you what you wanted.

Invariant for the outer loop of INSERTION-SORT. At the start of each iteration of the for loop of lines 1–8, the subarray A[1:i-1] consists of the elements originally in A[1:i-1], but in sorted order.
sorted key not yet examined 1 i-1 i i+1 n the original elements of A[1:i-1], reordered line 8 drops the key into the gap the inner loop opened After the iteration, the boundary has moved one right: A[1:i] is sorted.
Figure 2.2 — The invariant, drawn. The proof obligation is to show this picture is true before the first iteration and is restored by each one.

To use an invariant as a proof you must establish three things. CLRS names them, and the names are worth memorising because the book uses them verbatim in every later proof.

PropertyWhat you must showFor INSERTION-SORT
Initialization The invariant is true before the first iteration. Before the first iteration i = 2, so A[1:i-1] is A[1:1] — a single element, which is the original element and is trivially sorted.
Maintenance If it is true before an iteration, it remains true before the next one. The body shifts A[i-1], A[i-2], … right by one while they exceed key, then places key in the gap. The result is that A[1:i] holds the original elements of A[1:i] in sorted order. Incrementing i restores the invariant for the next iteration.
Termination When the loop ends, the invariant plus the reason it ended gives you the result you want. The loop ends when i > n, that is i = n+1. Substituting into the invariant: A[1:n] consists of the original elements in sorted order. That is the entire array, so the algorithm is correct.
Why this is induction in disguise. Initialization is the base case; maintenance is the inductive step. The one difference CLRS points out is that mathematical induction applies the step infinitely, whereas here you stop applying it when the loop terminates — and termination is precisely where the useful conclusion gets extracted. That third property is the one people forget, and it is the one that does the work.
The invariant is not optional decoration. Notice that it mentions the elements are the ones originally there, not just that they are sorted. Drop that clause and the invariant is still true of an algorithm that overwrites the array with 1, 2, 3, …. Getting the invariant exactly strong enough, and no stronger, is the real skill; the proof is mechanical afterwards.

Strictly, a full proof also needs an invariant for the inner while loop — something like “A[j+2:i] holds the original elements of A[j+1:i-1], all of them greater than key”. CLRS leaves that as an exercise. In practice, proving the outer loop and arguing the inner one informally is what most people do, and it is what the book models here.

Pseudocode conventions

CLRS pseudocode is not any real language, and a few of its conventions trip up readers who assume C or Python semantics. The ones that actually change meaning:

ConventionWhat it means
Indentation shows block structureThere are no braces and no end keywords. The body of a loop or conditional is whatever is indented under it — the same rule Python later adopted.
Arrays are 1-indexedA[1] is the first element, A[n] the last. Off-by-one differences against 0-indexed code are expected; translate deliberately.
A[p:r] is a sliceThe subarray from index p through r inclusive, unlike Python where the endpoint is excluded. New notation in the 4th edition.
= assigns, == comparesAnd i = j = e assigns e to both, evaluated right to left.
and and or short-circuitEvaluate left to right and stop as soon as the answer is determined. Line 5 of insertion sort depends on this for its bounds safety.
The loop counter survives the loopAfter for i = 2 to n finishes, i equals n+1. The termination argument above uses this directly.
Variables are localUnless stated otherwise, so no accidental globals.
Objects are referencesAttributes are written x.attr. Assigning y = x makes y.attr and x.attr the same field. Parameters are passed by value, but for an object that means the pointer is copied, so mutations are visible to the caller.
NIL is the null pointerUsed throughout the tree and list chapters.
Error handling is omittedDeliberately. It would obscure the algorithm, which is what the pseudocode exists to communicate.
That last one is the important cultural point. Pseudocode in CLRS is written for a human reader, chosen for clarity rather than executability. If a clearer English sentence beats three lines of code, the book writes the sentence.

The RAM model

Before you can say how long an algorithm takes, you have to say what machine it runs on. If the model is too vague the analysis is meaningless; if it is too detailed the analysis is impossible. CLRS uses the random-access machine (RAM), a deliberate middle point.

The RAM model assumesWhy the assumption is there
One processor, instructions executed one at a time, no concurrencyKeeps the count a single number. Chapter 26 relaxes this.
Each primitive instruction takes a constant amount of timeArithmetic, comparison, data movement, control flow. Constants may differ between instruction types, but none depends on the operand values.
Data types are integer, floating point, and characterMatches real hardware closely enough.
Each word holds c lg n bits for input size n, with c ≥ 1This is the subtle one, explained below.
Memory access costs the same regardless of address“Random access.” No cache hierarchy in the model.

The word-size assumption deserves a moment. It has to be at least lg n bits, or you could not even hold an index into the input. But it must be bounded above too, because if a word could be arbitrarily large you could pack the entire input into one word and perform absurd operations in “constant time”, which would make the model useless. c lg n for a constant c is the compromise: big enough to be realistic, small enough to prevent cheating.

What the RAM model quietly ignores. There is no memory hierarchy in it — no cache, no virtual memory, no locality effects. Modelling those makes the analysis dramatically harder and, in CLRS’ judgement, rarely changes the answer by more than a constant. But it is why an algorithm with a better asymptotic bound can lose in practice to a cache-friendly one, and why Θ notation is a guide rather than a verdict. Keep it in mind and the model will not mislead you.

Two more definitions the chapter pins down:

Analysing insertion sort

Now count. Let tᵢ denote the number of times the while loop test on line 5 is executed for that value of i. Each line costs a constant per execution, and the total is the sum of cost times count.

LineStatementCostTimes executed
1for i = 2 to nc₁n
2key = A[i]c₂n - 1
3// comment0n - 1
4j = i - 1c₄n - 1
5while j > 0 and A[j] > keyc₅ from i=2 to n of tᵢ
6A[j+1] = A[j]c₆ from i=2 to n of (tᵢ - 1)
7j = j - 1c₇ from i=2 to n of (tᵢ - 1)
8A[j+1] = keyc₈n - 1

Three counts are worth checking rather than accepting. Line 1 runs n times, not n-1, because the loop test is evaluated one extra time to discover that i has passed n. Lines 6 and 7 run tᵢ - 1 times rather than tᵢ, because the last evaluation of the line 5 test is the one that fails and exits the loop. And line 3 is a comment, costing nothing — it is listed only to keep the line numbers honest.

The running time is the sum over lines of cost × times:

T(n) = c₁n + c₂(n-1) + c₄(n-1) + c₅·∑tᵢ + c₆·∑(tᵢ-1) + c₇·∑(tᵢ-1) + c₈(n-1)

Everything now depends on the tᵢ, and those depend on the input, not just its size. That is the whole reason the next section exists.

Worst case, best case, average case

Best case: the array is already sorted

If A is already in sorted order, then for every i the test A[i-1] > key fails on the first try, so tᵢ = 1 and lines 6 and 7 never execute. Substituting:

T(n) = c₁n + (c₂ + c₄ + c₅ + c₈)(n - 1) = (c₁ + c₂ + c₄ + c₅ + c₈)·n - (c₂ + c₄ + c₅ + c₈) = an + b for constants a, b that depend on the cᵢ = Θ(n)

A linear function of n. Insertion sort on sorted input does a single pass, confirming each element is already in place.

Worst case: the array is in reverse sorted order

Now every key must travel past every element before it. For each i, the inner loop runs until j hits 0, so it executes the test tᵢ = i times. Two summations are needed, and both are standard:

∑ from i=2 to n of i = n(n+1)/2 - 1 ∑ from i=2 to n of (i - 1) = n(n-1)/2

The first is the arithmetic series 1 + 2 + … + n with the i = 1 term removed. The second is 1 + 2 + … + (n-1). Appendix A collects these.

Substituting gives a quadratic:

T(n) = (c₅/2 + c₆/2 + c₇/2)·n² + (c₁ + c₂ + c₄ + c₅/2 - c₆/2 - c₇/2 + c₈)·n - (c₂ + c₄ + c₅ + c₈) = an² + bn + c for constants a, b, c = Θ(n²)

Why the worst case, and not some other case

CLRS gives three reasons for concentrating on the worst case, and they are the reasons the whole field does:

  1. It is a guarantee. The worst-case running time is an upper bound for any input. You know the algorithm will never take longer, which is exactly what you need when the input is chosen by someone else, or by an adversary.
  2. The worst case occurs often. Searching a database for absent information hits the worst case of the search algorithm every time, and absent information is a common query.
  3. The average case is often as bad. For insertion sort it is, as the next paragraph shows — the two differ by a constant factor, not by an order of growth.

Average case

Suppose the input is a randomly chosen permutation. To insert A[i], on average half of the i-1 elements in A[1:i-1] are greater than it, so tᵢ is about i/2. Halving each term halves the sum, so the leading coefficient shrinks by a factor of two — and a quadratic with a smaller constant is still a quadratic:

The average-case running time of insertion sort is Θ(n²), the same order of growth as the worst case. Being twice as fast on typical input does not rescue a quadratic algorithm.

CLRS is careful with the vocabulary here. An average-case analysis assumes a probability distribution over inputs, which you may not know. An expected running time is the average over the algorithm’s own random choices, which you control because you built the randomness in. That distinction becomes the subject of Chapter 5, and it is the difference between hoping your input is not adversarial and guaranteeing it does not matter.

Order of growth

Look at what happened in both derivations. Real constants c₁ through c₈ were carried through several lines of algebra and then thrown away, leaving Θ(n) and Θ(n²). The chapter makes that abstraction official:

It is the rate of growth, or order of growth, of the running time that interests us. We consider only the leading term of the formula, since lower-order terms are relatively insignificant for large n, and we ignore the leading term’s constant coefficient, since constant factors are less significant than the rate of growth.

Two simplifications, both justified by letting n grow:

DiscardedBecauseExample
Lower-order termsTheir share of the total shrinks to nothing as n growsAt n = 1000, bn is a thousandth of an² when a = b
The leading constantIt is a fixed multiplier, so it shifts the curve but never changes its shapeChapter 1’s Computer A had a 1000× constant advantage and still lost

We therefore say insertion sort has a worst-case running time of Θ(n²), and we call one algorithm more efficient than another if its worst-case running time has a lower order of growth. Because of the constants, that judgement can be wrong for small inputs — and is, as Chapter 1’s table showed — but it is right for large ones, and large ones are where the time goes.

Chapter 3 replaces this informal “drop the small stuff” rule with the precise definitions of O, Ω, and Θ. Everything in this section is legitimate; it is simply stated loosely here so the sorting can proceed.

Divide-and-conquer

Insertion sort is incremental: having sorted A[1:i-1], it inserts one more element to get A[1:i]. Section 2.3 introduces the alternative that produces most of the fast algorithms in the book.

Divide-and-conquer solves a problem by breaking it into subproblems that are smaller instances of the same problem, solving those recursively, and combining the answers. Three steps, and CLRS names them every time: divide, conquer, combine.
StepIn generalIn merge sort
DivideSplit the problem into subproblems that are smaller instances of the same problemSplit the n-element subarray into two halves of n/2 elements each
ConquerSolve the subproblems recursively; if a subproblem is small enough, solve it directly as a base caseSort each half by recursive calls; a subarray of length 1 is already sorted
CombineAssemble the subproblem solutions into a solution for the originalMerge the two sorted halves into one sorted subarray

The recursion has to stop, which is what the base case is for. In merge sort the base case is a subarray of length 1 or less: nothing to do, because a one-element sequence is already in sorted order. The problem shrinks strictly on every call, so the base case is always reached.

The MERGE procedure

The combine step is where all the work happens, and it is worth understanding on its own before looking at the recursion. MERGE(A, p, q, r) assumes A[p:q] and A[q+1:r] are each already sorted, and merges them into a single sorted A[p:r]. The indices must satisfy p ≤ q < r.

Why merging is cheap: to find the smallest unplaced element overall, you only ever have to look at the front of each of the two sorted piles. Two comparisons’ worth of information settles it, no matter how long the piles are. Think of two sorted stacks of cards face up on a table: repeatedly take the smaller of the two exposed cards.
MERGE(A, p, q, r) 1 nL = q - p + 1 // length of A[p:q] 2 nR = r - q // length of A[q+1:r] 3 let L[0:nL-1] and R[0:nR-1] be new arrays 4 for i = 0 to nL - 1 // copy A[p:q] into L 5 L[i] = A[p + i] 6 for j = 0 to nR - 1 // copy A[q+1:r] into R 7 R[j] = A[q + j + 1] 8 i = 0 // smallest remaining element in L 9 j = 0 // smallest remaining element in R 10 k = p // location in A to fill 11 // While both L and R still have unmerged elements, 12 // copy the smaller one back into A[p:r]. 13 while i < nL and j < nR 14 if L[i] ≤ R[j] 15 A[k] = L[i] 16 i = i + 1 17 else 18 A[k] = R[j] 19 j = j + 1 20 k = k + 1 21 // One of L, R is exhausted. Copy the remainder of the other. 22 while i < nL 23 A[k] = L[i] 24 i = i + 1 25 k = k + 1 26 while j < nR 27 A[k] = R[j] 28 j = j + 1 29 k = k + 1

The 4th edition version. Note L and R are indexed from 0 while A is indexed from 1 — deliberate, and a common source of confusion when transcribing.

3rd edition difference. The older MERGE appended a sentinel of to the end of both L and R, which guaranteed the main loop could always compare two real values and let it run for exactly r - p + 1 iterations with no cleanup loops. It is elegant, but it requires a value larger than every key, which is awkward in real code. The 4th edition drops sentinels and pays for it with the two tidy-up loops on lines 22–29. Same cost, fewer assumptions.

Three points worth checking:

Cost. Lines 1–3 and 8–10 are constant. The copy loops run nL and nR times. Each iteration of any of the three merge loops places exactly one element into A and never removes one, so across all of them exactly n = r - p + 1 elements are placed, at constant cost each. Total: Θ(n). That linear combine step is what makes the whole recursion work.

The merge loop invariant

CLRS proves MERGE correct with the same three-property method. The invariant for the main loop of lines 13–20:

At the start of each iteration, the subarray A[p:k-1] contains the k - p smallest elements of L and R, in sorted order. Moreover, L[i] and R[j] are the smallest elements of their arrays that have not been copied back into A.

MERGE-SORT and its recursion

With MERGE as the combine step, merge sort itself is six lines.

MERGE-SORT(A, p, r) 1 if p ≥ r // zero or one element? 2 return 3 q = ⌊(p + r) / 2⌋ // midpoint of A[p:r] 4 MERGE-SORT(A, p, q) // recursively sort A[p:q] 5 MERGE-SORT(A, q+1, r) // recursively sort A[q+1:r] 6 MERGE(A, p, q, r) // merge the two sorted halves

Call it as MERGE-SORT(A, 1, n) to sort the whole array. Lines 1–2 are the base case, line 3 divides, lines 4–5 conquer, line 6 combines.

The recursion is easiest to see as a picture. The array splits until every piece has one element, then merges back up, and each merge produces a sorted run twice as long as its inputs.

1 2 2 3 4 5 6 7 2 4 5 7 1 2 3 6 2 5 4 7 1 3 2 6 5 2 4 7 1 3 2 6 sorted result merge runs of 4 merge runs of 2 8 single elements merge ↑
Figure 2.3 — Merge sort on ⟨5, 2, 4, 7, 1, 3, 2, 6⟩, viewed bottom-up. The divide phase runs the other way, splitting until each piece holds one element. Every level does Θ(n) total work merging, and there are lg n + 1 levels.

Solving the recurrence

Divide-and-conquer running times are naturally described by recurrences, because the algorithm is defined in terms of itself. Let T(n) be the worst-case time to sort n elements. Read it off the pseudocode:

The divide term is dominated by the combine term, so the two collapse into Θ(n):

╱ Θ(1) if n = 1 T(n) = │ ╱ 2T(n/2) + Θ(n) if n > 1

CLRS notes that the split is really into ⌈n/2⌉ and ⌊n/2⌋, and that ignoring the floors and ceilings does not change the answer. Chapter 4 justifies that shortcut properly.

Why the answer is Θ(n lg n)

The cleanest argument is the recursion tree. Rewrite the Θ(n) as cn for some constant c, then expand.

cost per level cn cn cn/2 cn/2 cn cn/4 cn/4 cn/4 cn/4 cn ccc ccc cn n leaves, one per element, each costing c lg n + 1 levels cn lg n + cn
Figure 2.4 — The recursion tree for T(n) = 2T(n/2) + cn. Subproblems halve in size but double in number, so every level costs the same cn. Multiply by the number of levels and you have the answer.

The argument in three steps:

  1. Each level costs cn. At depth d there are 2ᵈ subproblems, each of size n/2ᵈ, each costing c · n/2ᵈ. The 2ᵈ cancels the 1/2ᵈ, leaving cn at every level. This is the crux — the doubling of subproblem count exactly offsets the halving of subproblem size.
  2. There are lg n + 1 levels. The size sequence n, n/2, n/4, … reaches 1 after lg n halvings, and counting the root level gives lg n + 1.
  3. Multiply. Total cost is cn(lg n + 1) = cn lg n + cn. Dropping the lower-order term and the constant gives Θ(n lg n).
This is an informal recursion-tree argument, good enough to believe the answer and to remember how to rederive it. Chapter 4 turns it into three rigorous methods — substitution, recursion trees proper, and the master method — and the master theorem will hand you Θ(n lg n) for this recurrence in one step.

Merge sort against insertion sort

Insertion sortMerge sort
StrategyIncrementalDivide-and-conquer
Best caseΘ(n), on sorted inputΘ(n lg n), always
Worst caseΘ(n²), on reverse-sorted inputΘ(n lg n), always
Average caseΘ(n²)Θ(n lg n)
Extra spaceΘ(1) — sorts in placeΘ(n) — the L and R copies
StableYesYes, because of the on line 14
Constant factorSmallLarger, from recursion and copying
Wins whenn is small, or data is nearly sortedn is large

Merge sort beats insertion sort asymptotically and loses on constants and space. That is why real library sorts are hybrids: recurse like merge sort or quicksort while the subarray is large, then switch to insertion sort once it drops below a threshold of a few dozen elements. CLRS makes this an exercise (2-1), and it is the standard trick in production implementations.

Recap

The seven things to carry forward

Where this goes next

Chapter 3 makes the notation honest. “Drop the lower-order terms and the constant” becomes the formal definitions of O, Ω, and Θ, along with o and ω, so that statements like “insertion sort is Θ(n²) in the worst case” can be proved rather than asserted. Chapter 4 then returns to the recurrence T(n) = 2T(n/2) + Θ(n) and gives you three general methods for solving recurrences, so you never have to invent the tree argument again.


Ch 1 — The Role of Algorithms in Computing Ch 3 — Characterizing Running Times