Midterm study guide
Everything from the eight slide decks, rewritten so it can be learned without the lecture videos. Each lecture page explains the ideas in plain language, works the examples on the slides step by step, answers every "Why?" the slides leave open, and lets you run the algorithms yourself.
- Go through the lectures in order. Later topics lean on earlier ones: recurrences need asymptotic notation, and heapsort's analysis needs both.
- On every page, read the explanation, then play with the interactive panels. Watching an algorithm move is the fastest way to understand it.
- Try each Check yourself question before you open the answer. When you can answer them all, press Mark as studied at the bottom of the page.
- Finish with the practice exam and the flashcards, and read the cheat sheet the night before.
What the course covers before the midterm
What an exam on these slides usually asks
Based on what the slides emphasize (and what CLRS-based midterms almost always include), expect questions of these kinds. Each one links to where it is taught.
| Question type | Example | Where |
|---|---|---|
| Prove a bound with constants | Show 10n2 − 3n = Θ(n2) by giving c1, c2, n0. | 4a |
| Compare growth rates | Is nlg 4 in O(3lg n)? Order these functions by growth. | 4a, 4b |
| Solve a recurrence | T(n) = 16T(n/4) + n by the master method; T(n)=2T(n/2)+n by a recursion tree. | 5, 6 |
| Count steps | Best/worst/average running time of linear search or insertion sort; evaluate a triple loop sum. | 2, 4b |
| Loop invariant proof | State the invariant for insertion sort and prove initialization, maintenance, termination. | 3, 5 |
| Trace an algorithm | Show the array after each step of Merge, BuildMaxHeap, heapsort, insertion sort. | 5, 8, 3 |
| Properties of sorts | Which sorts are stable? in place? What is each one's running time? | 7, cheat sheet |
| Heap facts | Index of parent/children, height, number of leaves, why BuildMaxHeap is O(n). | 8 |
A study plan if time is short
- Session 1Lectures 1–3. Learn the RAM model, how to count steps, and the three-part loop invariant proof. (about 1.5 h)
- Session 2Lecture 4a. This is the hardest and most tested material. Do every "Check yourself" and play with the constants widget until finding c and n0 feels routine. (about 2 h)
- Session 3Lecture 4b. Memorize the log rules and summation formulas; rank functions by growth. (about 1 h)
- Session 4Lectures 5 and 6. Merge sort, recursion trees, master method. Solve at least ten recurrences with the solver, predicting the answer first. (about 2 h)
- Session 5Lectures 7 and 8. Trace selection sort, Shellsort, MaxHeapify, BuildMaxHeap and heapsort by hand, then check with the animations. (about 2 h)
- Session 6Practice exam, flashcards, cheat sheet. Re-read any page where you missed a question. (about 1.5 h)
All content follows Dr. Li's slides and the CLRS textbook they cite (Cormen, Leiserson, Rivest, Stein, Introduction to Algorithms, 3rd ed.). Where a slide asks a question and leaves the answer for the lecture, the answer here is the standard CLRS one. Where a slide has a typo, this guide says so.
The role of algorithms in computing
This first lecture sets up the vocabulary for the rest of the course: what a problem and an algorithm are, what it means to analyze one, and the big design strategies you will meet later. Expect definition-style questions from here.
Problems and algorithms
The slides separate two ideas that people often blur together.
A mathematical problem specified by an input/output relation: for every legal input, it says which outputs count as correct. It says what, never how.
A computational procedure for solving a computational problem: a step-by-step method that turns any legal input into a correct output.
Example: sorting is a problem (input: a sequence of numbers; output: the same numbers in non-decreasing order). Insertion sort, merge sort and heapsort are three different algorithms for that one problem.
Well-known computational problems on the slide
| Problem | What it asks |
|---|---|
| Sorting | Put n keys in order. Studied in lectures 3, 5, 7, 8. |
| Searching | Is a key in a collection, and where? (Linear search in lecture 2.) |
| Shortest paths in a graph | Cheapest route between vertices (later in the course, Ch 24–25). |
| Minimum spanning tree | Cheapest set of edges connecting every vertex (Ch 23). |
| Primality testing | Is n prime? |
| Traveling salesman | Shortest tour visiting every city once. No efficient algorithm is known (NP-hard). |
| Knapsack | Pick items of maximum value under a weight limit. |
| Chess | Find the best move. Finite, but astronomically large. |
| Towers of Hanoi | Move n disks; needs exactly 2n − 1 moves. |
| Program termination | Does a program halt? This is the halting problem: no algorithm can solve it for all programs. |
History, in one breath
Euclid (300 BC, the GCD algorithm below), Bhaskara (6th century), Al-Khwarizmi (9th century; the word "algorithm" comes from his name), Fibonacci (13th century), Babbage (19th century, mechanical computers), Turing (20th century, the formal model of computation), then von Neumann, Knuth, Karp, Tarjan and others.
Euclid's algorithm
The greatest common divisor GCD(a,b) is the largest integer dividing both. The slide builds the algorithm in two steps.
- Observation. If a ≥ b, then GCD(a,b) = GCD(a−b, b). Any number dividing a and b also divides a−b, and the other way round, so the set of common divisors does not change.
- Euclid's rule. Subtracting b over and over is the same as taking the remainder, so GCD(a,b) = GCD(a mod b, b). One mod replaces many subtractions.
The slide writes the recursive call as GCD(a mod b, b). Taken literally that never ends: after one step the first argument is smaller than b, and (a mod b) mod b is the same number again. The arguments must swap each time: GCD(b, a mod b). If you write it on the exam, use the swapped version.
The slide asks: GCD(210, 588) = ?
GCD(588, 210) → GCD(210, 168) → GCD(168, 42) → GCD(42, 0) = 42. (Starting as GCD(210, 588) just adds one extra step that swaps the two numbers, because 210 mod 588 = 210.)
Why does Euclid's algorithm always terminate?
The second argument strictly decreases each call (a mod b < b) and never goes below 0. A strictly decreasing sequence of non-negative integers must reach 0, which is the base case. It is also fast: the numbers at least halve every two steps, so it takes O(lg b) calls.
What "analysis of algorithms" means
The slides list the basic issues around any algorithm: how to design it, how to express it (pseudocode), how to prove it correct, its efficiency (theoretical analysis on paper, or empirical analysis by running it) and its optimality (can anything do better?).
- Correctness: right output on every legal input.
- Time efficiency: amount of work done.
- Space efficiency: amount of memory used.
- Simplicity, clarity: recursion can shorten long algorithms.
- Lower bounds: the least work any algorithm for the problem must do (for example, comparison sorting needs Ω(n lg n) comparisons).
- Optimality: an algorithm is optimal if it meets the lower bound.
- Some problems have no efficient algorithm at all.
Correctness
Proving a big algorithm correct in one go is "dreadful", so the strategy is to break it into pieces, write each piece's precondition (what must be true before it runs) and postcondition (what it guarantees after), and prove each piece assuming the others are correct. Loop invariants (lecture 2 and 3) are exactly this idea applied to a loop.
Amount of work done
Instead of counting every instruction, pick the key instructions (for sorting: key comparisons) and count how often they run as a function of input size n. Then keep only the largest-growing term, written in asymptotic notation. Two kinds of analysis matter: worst case and average case.
Preview: the five growth-rate notations
Slide 16 defines them with limits. Lecture 4a covers them properly; for now just notice that each one says how f compares with g for huge n.
| Notation | Limit test | Reads like |
|---|---|---|
| f ∈ O(g) | limn→∞ f(n)/g(n) < ∞ | f grows no faster than g (like ≤) |
| f ∈ o(g) | limn→∞ f(n)/g(n) = 0 | f grows strictly slower (like <) |
| f ∈ Ω(g) | limn→∞ f(n)/g(n) > 0 | f grows at least as fast (like ≥) |
| f ∈ ω(g) | limn→∞ f(n)/g(n) = ∞ | f grows strictly faster (like >) |
| f ∈ Θ(g) | lim = c for a constant 0 < c < ∞; also Θ(g) = Ω(g) ∩ O(g) | same growth rate (like =) |
The other background the slide lists (floor and ceiling, exponentials and logarithms, Fibonacci numbers, summations and series) is all in Lecture 4b.
Algorithm design strategies
Slide 20 lists the classic strategies. You only need to recognize them now; several get whole lectures later.
| Strategy | Idea | Examples |
|---|---|---|
| Brute force | Try the obvious thing directly. | Linear search, selection sort, the Θ(n3) max-subvector loop |
| Divide and conquer | Split into independent subproblems, solve recursively, combine. | Merge sort, quicksort |
| Decrease and conquer | Shrink the problem by one or by half, solve the smaller one. | Insertion sort (by 1), binary search (by half), Euclid |
| Transform and conquer | Change the data into a more convenient form first. | Binary search trees, heapsort (array → heap), presorting |
| Greedy | Make the locally best choice at each step. | Dijkstra, Prim, Huffman codes |
| Dynamic programming | Solve overlapping subproblems once and store the answers. | Rod cutting, longest common subsequence |
| Backtracking | Build a solution piece by piece, undo on dead ends. | Eight queens puzzle |
| Branch and bound | Backtracking that prunes branches that cannot beat the best so far. | Traveling salesman, knapsack |
| Space–time tradeoff | Use extra memory to save time. | Hash tables, counting sort |
Course roadmap
The whole semester, so you can see where the midterm sits. Everything up to heapsort is midterm material.
| Unit | Chapters | Goals from the slides |
|---|---|---|
| Algorithmics basics | Ch 1, 2, 3, App. A | Formal problem specs, models of computation, measuring efficiency, upper vs lower bounds, proving correctness. |
| Divide and conquer | 2.3, Ch 4 | Know when it fits, its general structure, write recurrences, solve them, memorize common solutions. |
| Sorting & selection | Ch 6–9 | Performance of each sort, when to use it; applications of binary heaps; why sorting matters. |
| Dynamic programming | Ch 15 | When to use it and how it differs from divide and conquer. |
| Greedy algorithms | Ch 16, 23, 24 | Prove greedy correctness; minimum spanning trees; shortest paths. |
| Graph algorithms | Ch 22, 24, 25 | Graph representations, searching, single/all-pairs shortest paths. |
| Special topics | Ch 27, 32–34 | NP-completeness, string matching, geometry, parallel algorithms. |
Check yourself
What is the difference between a problem and an algorithm?
A problem specifies the input/output relation (what counts as a correct answer). An algorithm is a specific procedure that produces a correct output for every input. One problem can have many algorithms.
Name the two major types of running-time analysis the slides mention, and say which is easier.
Worst-case and average-case. Worst case is easier ("most tractable") because average case needs a probability distribution over inputs.
Which design strategy is binary search? Insertion sort? Heapsort?
Binary search: decrease and conquer (by half). Insertion sort: decrease and conquer (by one: sort n−1 items, insert the last). Heapsort: transform and conquer (turn the array into a heap first). Binary search is also often called divide and conquer.
What is a lower bound, and what does it mean for an algorithm to be optimal?
A lower bound for a problem is a minimum amount of work that every algorithm for that problem must do in the worst case. An algorithm is optimal when its running time matches that lower bound (for example, merge sort's Θ(nlg n) matches the Ω(n lg n) bound for comparison sorting).
What is an algorithm, and how do we analyze one?
This lecture gives the machinery used for the rest of the course: a model of a computer (the RAM), a definition of running time (count the steps), the three kinds of case analysis, and the loop invariant technique for proving correctness. Linear search is the running example.
A sharper definition of "algorithm"
An algorithm is a finite sequence of unambiguous instructions for solving a well-specified computational problem.
The slide lists five features every algorithm must have (Knuth's list). These are easy exam points if you can explain each in one line:
| Feature | Meaning |
|---|---|
| Finiteness | It always stops after a finite number of steps. |
| Definiteness | Every step is precisely and unambiguously defined. |
| Input | It takes zero or more inputs from a specified set. |
| Output | It produces at least one output, related to the input as the problem requires. |
| Effectiveness | Each step is basic enough to be carried out exactly, in finite time (in principle, by a person with pencil and paper). |
Formally, an algorithm can be defined as a Turing machine that always halts (lambda calculus is another equivalent definition). A mathematical definition is what lets us ask questions like "is this problem solvable at all?" and "which complexity class is it in?".
Running time and the RAM model
Algorithm analysis means predicting the resources an algorithm needs: time, memory, communication bandwidth. Time is the main concern. We analyze so we can pick the most efficient of several algorithms, know whether the best possible time is practical, and know whether something better could exist.
A running time has to be machine-independent, so we analyze on an idealized computer: the RAM (Random Access Machine) model.
- Generic single processor, instructions run one after another (no concurrency).
- Simple instructions, each costing 1 time unit: arithmetic (+, −, ×, /, %, floor, ceiling), data movement (load, store, copy), control (branch, subroutine call).
- Unlimited memory, flat (no cache hierarchy); accessing any word costs 1 unit.
- Words have a finite size.
Yes. Real machines differ by constant factors (a multiply may cost more than an add, cache misses cost more than hits), but we only care about how time grows with n. Constant factors disappear in asymptotic notation, so a simple model gives the right growth rate.
If the model had a SORT instruction, sorting would cost 1 step and the analysis would mean nothing. Likewise, if a word could be arbitrarily large, you could pack a whole array into one word and process it in one "step". CLRS assumes a word holds about c lg n bits: just enough to store an index into the input.
Each simple instruction or memory access is one primitive operation or step. The running time of an algorithm on a given input is the number of steps it executes on that input. It is often called the algorithm's complexity.
Running time depends on the size of the input (number of items to sort; number of vertices and edges in a graph) and on other characteristics of the input (is it already sorted? does the graph have cycles?). That second dependency is why we need cases.
Worst, average and best case
The maximum number of steps over all inputs of size n. The most tractable measure, and a guarantee: the algorithm never does worse. This is the default meaning of "running time".
The average over all inputs of size n. Needs a probability for each input (usually "all orders equally likely"), which is hard to justify and to analyze.
The minimum number of steps over all inputs of size n. Rarely useful.
It tells you almost nothing about what to expect, and it is easy to cheat: any algorithm can be given a great best case by first checking for one special input (say, an already sorted array) and returning immediately. That changes nothing for real inputs. A guarantee has to come from the worst case.
Pseudocode conventions (CLRS p. 18–20)
- Indentation shows block structure (no braces).
- After a
forloop ends, the loop counter keeps the value that made the test fail (afterfor j = 2 to n, j = n+1). Proofs rely on this. - Parameters are passed by value; variables are local by default.
- Error handling and software-engineering concerns are left out.
- Arrays are indexed from 1: A[1..n].
Worked example: linear search
Input: a sequence of n numbers A[1..n] and a key. Output: true if the key occurs in the sequence, false otherwise.
| Line | cost | times |
|---|---|---|
| 1 | c1 | 1 |
| 2 | c2 | x |
| 3 | c3 | x − 1 |
| 4 | c4 | 1 |
| 5 or 6 | c5 or c6 | 1 |
Here x is how many times the loop test on line 2 runs. If the key is at position k, the test runs k times (it succeeds k−1 times, then stops on the match), so x = k. If the key is missing, the test runs for i = 1, …, n+1, so x = n+1. So x ranges from 1 to n+1, and the body (line 3) always runs one time fewer than the test.
c1 + c2 + c4 + c5. With every cost set to 1: 1+1+1+1 = 4 steps. Constant, so Θ(1).
c1 + c2(n+1) + c3 n + c4 + c6. With unit costs: 1 + (n+1) + n + 1 + 1 = 2n + 4. Linear, so Θ(n).
Lines 2 and 3 run about n/2 times each: 1 + n/2 + n/2 + 1 + 1 = n + 3. Also Θ(n).
Show the average case n + 3 exactly (not just "about n/2").
If the key is at position k, the cost is 1 + k + (k−1) + 1 + 1 = 2k + 2. Each k ∈ {1,…,n} has probability 1/n, so the average is 1nn∑k=1(2k+2) = 2n·n(n+1)2 + 2 = (n+1) + 2 = n+3. Exactly what the slide says.
Order of growth
What we really want is how running time grows with input size (order of growth), and the running time for large inputs (asymptotic complexity). For that, lower-order terms and the leading coefficient do not matter.
7n5. At n = 100 it is 7 × 1010 while 6n3 = 6 × 106 (ten thousand times smaller). Drop the coefficient 7 as well and the complexity is Θ(n5).
Notation preview (fully defined in lecture 4a): O(n), Θ(1), Ω(n2) and so on. Running time that does not depend on n at all is constant, written O(1) or Θ(1). For linear search: best case Θ(1), worst and average case Θ(n).
Comparing algorithms
Algorithm A is more efficient than B if A's complexity function has a lower order. The slide's pairs:
- Linear search Θ(n) versus binary search Θ(lg n).
- Insertion sort Θ(n2) versus quicksort Θ(n lg n) (on average) or merge sort Θ(nlg n).
- Multiplying two numbers: classical method O(nm); divide and conquer about O(n m0.59). For 1000-digit operands: 40 s versus 15 s.
- Sorting 106 numbers: insertion sort on a supercomputer in machine language took 5.56 hours; merge sort on a PC in C/C++ took 16.67 minutes. A better algorithm on a slow machine beats a worse algorithm on a fast machine, once n is large.
Why order of growth matters even as computers get faster
Hardware speed doubles every couple of years, so why care? Because the gain you get from faster hardware depends on the algorithm. Say the machine does S operations per second and you have one second. An n2 algorithm handles the n with n2 = S, so n = √S. Double S and n only grows by √2 ≈ 1.414. An n lg n algorithm almost doubles its n (gain 1.9). An exponential 2n algorithm gains only one extra item.
Correctness proofs
If the algorithm terminates, its output is correct.
Partial correctness plus a proof that the algorithm always terminates.
Proof techniques: construction, induction, contradiction. For loops, the standard tool is a loop invariant.
A logical statement about the loop's variables such that:
- Initialization: it is true before the first iteration.
- Maintenance: if it is true before an iteration, it is still true before the next one.
- Termination: when the loop ends, the invariant together with the reason the loop stopped gives a property that shows the algorithm is correct.
Initialization is the base case and maintenance is the inductive step, exactly like induction. The difference is termination: ordinary induction goes on forever (it proves the statement for every n), while a loop invariant argument stops when the loop stops and then uses the invariant at that moment to conclude something useful. Induction has no termination step.
Correctness of linear search
Invariant: at the start of each iteration of the while loop, the key is not in the subarray A[1..i−1].
- Initialization. Before the first iteration i = 1, so A[1..0] is empty. The key is trivially not in an empty subarray.
- Maintenance. Suppose the key is not in A[1..i−1] and the loop body runs. The body only runs when the test passed, so A[i] ≠ key. Then the key is not in A[1..i]. After i is incremented, that is exactly "not in A[1..i−1]" for the new i.
- Termination. The loop stops for one of two reasons. If i ≤ n, it stopped because A[i] = key, so returning true is correct. If i = n+1, the invariant says the key is not in A[1..n], the whole array, so returning false is correct.
- It does terminate. i increases by 1 every iteration and the loop must stop once i > n, so there are at most n iterations.
The slide then says "go through the correctness proof of insertion sort in the text, p. 18–20". That is the whole of Lecture 3.
Check yourself
Linear search with unit costs: what are the best and worst case step counts, and for which inputs?
Best: 4 steps when the key is A[1]. Worst: 2n+4 steps when the key is not in the array (the loop test runs n+1 times and the body n times).
A machine 4 times faster arrives. How much bigger an input can an n2 algorithm handle in the same time? An n3 one?
n2: √4 = 2 times bigger. n3: √[3]4 ≈ 1.59 times bigger. In general, a k times faster machine gives nc algorithms a factor of k1/c.
What does "partial correctness" leave out?
Termination. Partial correctness only promises a correct answer if the algorithm stops; total correctness also proves that it stops.
Write the three parts of a loop invariant proof from memory.
Initialization (true before the first iteration), maintenance (true before one iteration implies true before the next), termination (at loop exit, the invariant plus the exit condition gives correctness).
Insertion sort and loop invariants
A short deck with one job: show how a loop invariant proves an algorithm correct, using insertion sort. Being able to write this proof from memory is one of the most reliable exam points. The running-time analysis from CLRS 2.2 is included here too, since lecture 7 builds on it.
The algorithm
Problem: sort n numbers in A[1..n]. Output: A in sorted order, meaning A[i−1] ≤ A[i] for every i ∈ [2..n].
Idea: sort the way most people sort a hand of playing cards. The left part of the array is a sorted "hand". Take the next card (the key), slide every bigger card in the hand one place to the right, and drop the key into the gap.
Line 3 is a comment, as in CLRS, so the line numbers here match the cost table below.
Proof of correctness with a loop invariant
At the start of each iteration of the for loop (lines 1–8), the subarray A[1..j−1] consists of the elements originally in A[1..j−1], but in sorted order.
Both halves of that sentence matter. "Sorted" alone is not enough: you also need "the same elements that were there originally", otherwise an algorithm that overwrote everything with zeros would satisfy it.
- Initialization. Before the first iteration j = 2, so the subarray is A[1..1]: the single original element A[1]. One element is always sorted. ✓
- Maintenance. Assume A[1..j−1] holds the original first j−1 elements in sorted order. The while loop moves A[j−1], A[j−2], … one position to the right until it finds a position i with A[i] ≤ key (or reaches i = 0). Then the key, formerly A[j], goes into position i+1. Everything left of it is ≤ key and everything right of it (the shifted elements) is > key, so A[i] ≤ A[i+1] ≤ A[i+2]. Result: A[1..j] holds the original first j elements in sorted order. Incrementing j for the next iteration makes the invariant true again. ✓
(Fully formal treatment would also state and prove an invariant for the inner while loop. The slides, like CLRS, accept this informal argument.) - Termination. The for loop ends when j exceeds length(A) = n, which means j = n+1. Substituting into the invariant: A[1..n] consists of the elements originally in A[1..n], in sorted order. That is the entire array, sorted. ✓
In the termination step, say why j = n+1 (the loop counter is one past the upper bound when a for loop exits) and then substitute n+1 for j in the invariant. Students lose points for writing "at the end the array is sorted" without connecting it to the invariant.
Running time analysis
CLRS gives each line a cost and counts how many times it runs. Let tj be the number of times the while-loop test (line 5) runs for that value of j.
| Line | Code | cost | times |
|---|---|---|---|
| 1 | for j ← 2 to n | c1 | n |
| 2 | key ← A[j] | c2 | n−1 |
| 3 | comment | 0 | n−1 |
| 4 | i ← j − 1 | c4 | n−1 |
| 5 | while i > 0 and A[i] > key | c5 | n∑j=2 tj |
| 6 | A[i+1] ← A[i] | c6 | n∑j=2 (tj − 1) |
| 7 | i ← i − 1 | c7 | n∑j=2 (tj − 1) |
| 8 | A[i+1] ← key | c8 | n−1 |
Line 1 runs n times, not n−1: the test runs once more than the body, when j = n+1 fails it.
Every while test fails immediately (A[j−1] ≤ key), so tj = 1. Lines 6–7 never run. T(n) = (c1+c2+c4+c5+c8)n − (c2+c4+c5+c8), which has the form an + b: linear, Θ(n). Comparisons: n−1.
Each key is compared with every element to its left and moves all the way to position 1, so tj = j. Using n∑j=2 j = n(n+1)2 − 1 and n∑j=2(j−1) = n(n−1)2, T(n) has the form an2 + bn + c: quadratic, Θ(n2). Comparisons: n(n−1)2.
On average the key is larger than about half of the sorted part, so tj ≈ j/2. That still sums to roughly n2/4: Θ(n2). Lecture 7 derives this exactly.
- Insertion sort is in place (only the variable
keyand the index use extra space). - It is stable: the test is
A[i] > key(strictly greater), so an element never jumps over an equal one. - Best Θ(n), worst Θ(n2), average Θ(n2). It is very fast on nearly sorted input, which is why Shellsort (lecture 7) builds on it.
If the while condition were A[i] ≥ key, the algorithm would still sort correctly but would no longer be stable: equal keys would leapfrog each other. It would also do more shifts.
Check yourself
Trace insertion sort on ⟨5, 2, 4, 6, 1, 3⟩ (CLRS Figure 2.2). Show the array after each for-loop iteration.
j=2: ⟨2, 5, 4, 6, 1, 3⟩ · j=3: ⟨2, 4, 5, 6, 1, 3⟩ · j=4: ⟨2, 4, 5, 6, 1, 3⟩ (6 stays) · j=5: ⟨1, 2, 4, 5, 6, 3⟩ · j=6: ⟨1, 2, 3, 4, 5, 6⟩. Type these numbers into the animation above to check.
CLRS 2.1-1: trace ⟨31, 41, 59, 26, 41, 58⟩.
j=2: no change · j=3: no change · j=4: ⟨26, 31, 41, 59, 41, 58⟩ · j=5: ⟨26, 31, 41, 41, 59, 58⟩ (the second 41 stops behind the first one: stability) · j=6: ⟨26, 31, 41, 41, 58, 59⟩.
How would you change the algorithm to sort in non-increasing (descending) order?
Change the while test to A[i] < key. Everything else, including the invariant (with "sorted" now meaning non-increasing), stays the same.
What is the value of j when the for loop terminates, and why does the proof need it?
j = n+1. The termination step plugs it into the invariant: "A[1..j−1] = A[1..n] is sorted and holds the original elements", which is the correctness claim.
How many key comparisons does insertion sort make on an already sorted array of size n? On a reverse-sorted one?
Sorted: n−1 (one per key). Reverse sorted: 1 + 2 + ⋯ + (n−1) = n(n−1)/2.
Asymptotic notation: Θ, O, Ω, o, ω
The most important lecture before the midterm. These five symbols are the language for every running time in the course, and the exam will ask you both to use them and to prove statements with them by finding constants.
The idea in one paragraph
The asymptotic complexity of an algorithm is its running time as a function of input size n, for large n, written using only the highest-order term. Instead of "the running time is 3n2 + 7n + 12 steps", we say "Θ(n2)". The notation describes how the function behaves in the limit, and it hides constant factors that depend on the machine anyway.
Each notation is defined as a set of functions. Θ(n2) is the set of all functions that grow like n2. Writing f(n) = Θ(n2) really means f(n) ∈ Θ(n2). The slide says either form is accepted. All functions here are assumed non-negative for large n.
Θ-notation: tight bound
Θ(g(n)) = { f(n) : there exist positive constants c1, c2, n0 such that 0 ≤ c1 g(n) ≤ f(n) ≤ c2 g(n) for all n ≥ n0 }.
In words: from some point n0 on, f is sandwiched between two constant multiples of g. g(n) is an asymptotically tight bound for f(n): same rate of growth.
Two things to notice. The constants only have to exist; you choose them. And nothing is promised for n < n0: small inputs are ignored.
That slide uses the phrase in a second sense: an algorithm is asymptotically tight (optimal) when its complexity exactly matches the proven complexity of the problem, i.e. it solves the problem at the theoretical minimum. Example: merge sort's Θ(n lg n) matches the Ω(nlg n) lower bound for comparison sorting. In the Θ definition, "tight" just means "both upper and lower bound".
Worked example: prove 10n2 − 3n = Θ(n2)
The slide's tip: make c1 a little smaller than the leading coefficient, and c2 a little bigger. To compare orders of growth, look at the leading term.
- Upper bound. Pick c2 = 10. Then 10n2 − 3n ≤ 10n2 for every n ≥ 1, because 3n ≥ 0.
- Lower bound. Pick c1 = 9. We need 9n2 ≤ 10n2 − 3n, i.e. 3n ≤ n2, i.e. n ≥ 3.
- Combine. With c1 = 9, c2 = 10, n0 = 3: 0 ≤ 9n2 ≤ 10n2 − 3n ≤ 10n2 for all n ≥ 3. ∎
Other choices work too (for example c1 = 1, c2 = 10, n0 = 1). The exam only wants one valid set, with the algebra that justifies it.
Slide exercise: prove n2/2 − 3n = Θ(n2).
Divide the sandwich c1 n2 ≤ n22 − 3n ≤ c2 n2 by n2: we need c1 ≤ 12 − 3n ≤ c2.
Right side: 12 − 3n ≤ 12 for all n ≥ 1, so c2 = 12. Left side: 12 − 3n grows toward 12; at n = 7 it equals 12 − 37 = 114. So c1 = 114, n0 = 7 works. (This is CLRS's own choice.) Any n0 ≥ 7 with a matching c1 also works, e.g. n0 = 12, c1 = 14.
Slide asks: is 3n3 ∈ Θ(n4)?
No. The upper bound is fine (3n3 ≤ 3n4). But the lower bound would need c1 n4 ≤ 3n3, i.e. n ≤ 3/c1, which fails for all large n no matter how small c1 is. So 3n3 ∈ O(n4) but not Ω(n4), hence not Θ(n4). (In fact 3n3 = o(n4).)
Slide asks: is 22n ∈ Θ(2n)?
No. 22n = (2n)2 = 4n. An upper bound would need 4n ≤ c· 2n, i.e. 2n ≤ c, which fails once n > lg c. The ratio 22n/2n = 2n → ∞, so 22n = ω(2n). Constants in an exponent matter, unlike constant factors in front.
O-notation: upper bound
O(g(n)) = { f(n) : there exist positive constants c and n0 such that 0 ≤ f(n) ≤ c g(n) for all n ≥ n0 }.
Intuitively: the set of functions whose rate of growth is the same as or lower than g's. g(n) is an asymptotic upper bound for f(n). It need not be tight: n = O(n2) is true.
Since Θ demands both bounds, f(n) = Θ(g(n)) ⇒ f(n) = O(g(n)). As sets, Θ(g(n)) ⊆ O(g(n)).
Take c = |a| + |b| and n0 = 1. For n ≥ 1: an + b ≤ |a|n + |b| ≤ |a|n2 + |b|n2 = c n2. (It is also O(n), a tighter bound.)
c = 3, n0 = 1: 3n3 ≤ 3n4 whenever n ≥ 1. Or c = 1, n0 = 3: 3n3 ≤ n4 ⇔ 3 ≤ n.
Ω-notation: lower bound
Ω(g(n)) = { f(n) : there exist positive constants c and n0 such that 0 ≤ c g(n) ≤ f(n) for all n ≥ n0 }.
Intuitively: functions whose rate of growth is the same as or higher than g's. g(n) is an asymptotic lower bound. Again f(n) = Θ(g(n)) ⇒ f(n) = Ω(g(n)), and Θ(g(n)) ⊆ Ω(g(n)).
Try c = 1: we need lg n ≤ √n. Check values: n = 4: 2 ≤ 2 ✓; n = 8: 3 ≤ 2.83 ✗; n = 16: 4 ≤ 4 ✓; n = 64: 6 ≤ 8 ✓. It fails between 5 and 15, and holds from 16 on (the gap keeps widening because √n grows faster). So c = 1, n0 = 16. With a smaller constant you can start earlier: c = 12, n0 = 1 also works, since 12 lg n ≤ √n for all n ≥ 1.
How Θ, O and Ω relate
For any two functions f(n) and g(n): f(n) = Θ(g(n)) if and only if f(n) = O(g(n)) and f(n) = Ω(g(n)). That is, Θ(g(n)) = O(g(n)) ∩ Ω(g(n)).
This is how tight bounds are usually proved in practice: prove an upper bound and a lower bound separately, then combine them.
What O, Ω and Θ say about running times
This slide (asymp-15) is subtle and a favourite source of true/false questions.
| Statement | Means | Why |
|---|---|---|
| "Running time is O(f(n))" | The worst case is O(f(n)). | An upper bound on the worst case is an upper bound on every input, because every input is at most as slow as the worst one. |
| "Running time is Ω(f(n))" | The best case is Ω(f(n)). | A lower bound on the best case is a lower bound on every input. |
| "Worst-case running time is Θ(f(n))" | The worst-case function is in Θ(f(n)). | This does not mean every input takes Θ(f(n)). Insertion sort's worst case is Θ(n2), yet a sorted input takes Θ(n). |
| "Worst-case running time is Ω(f(n))" | Some unspecified g(n) ∈ Ω(f(n)) gives the worst case. | There exists at least one bad input family that is that slow. |
"An O(f) bound on the worst case implies an O(f) bound on every input" is true. "A Θ(f) bound on the worst case implies a Θ(f) bound on every input" is false (the slide marks it with ⇏).
Insertion sort takes Θ(n2) in the worst case, so sorting (as a problem) is O(n2). Why? Because an upper bound for a problem only needs one algorithm that achieves it. Insertion sort solves every instance in O(n2) time, so the problem's complexity is at most that.
Any sort must look at every item, so sorting is Ω(n). If an algorithm skipped an item, that item could be anywhere and the output could be wrong.
Using merge sort, sorting is Θ(n lg n) in the worst case, and later it is proved that no comparison sort can do better than Ω(n lg n) in the worst case.
Asymptotic notation inside equations
A Θ inside a formula stands for some anonymous function in that set. The slide's chain:
How to interpret it (the slide asks): in the second expression, Θ(n) stands for the specific function 2n + 1, which is in Θ(n). In the third, Θ(n2) stands for 3n2 + 2n + 1. Each "=" is read left to right as "no matter how the anonymous functions on the left are chosen, there is a way to choose the ones on the right so the equation holds". The chain shows we can throw away lower-order terms one at a time.
The same convention lets us write recurrences like T(n) = 2T(n/2) + Θ(n).
o-notation and ω-notation: strict bounds
o(g(n)) = { f(n) : for every c > 0 there exists n0 > 0 such that 0 ≤ f(n) < c g(n) for all n ≥ n0 }.
Equivalently limn→∞ f(n)g(n) = 0: f becomes insignificant next to g. g is an upper bound that is not asymptotically tight.
ω(g(n)) = { f(n) : for every c > 0 there exists n0 > 0 such that 0 ≤ c g(n) < f(n) for all n ≥ n0 }.
Equivalently limn→∞ f(n)g(n) = ∞: f becomes arbitrarily large next to g. A lower bound that is not tight.
In O and Ω the constant is "there exists a c". In o and ω it is "for all c > 0" (and n0 may depend on c), with a strict inequality. For f = O(g) it is enough that one multiple of g eventually stays above f. For f = o(g), every multiple, however tiny (c = 0.0001), must eventually stay above f. That only happens if f/g → 0, so the bound cannot be tight.
Example: 2n2 = O(n2) (take c = 3) but 2n2 ≠ o(n2) (with c = 1, 2n2 < n2 never holds). Meanwhile 2n = o(n2): for any c, 2n < cn2 once n > 2/c.
More examples: n22 = ω(n), but n22 ≠ ω(n2). And n lg n = o(n2), n2 = ω(nlg n).
The real-number analogy
Comparing functions behaves like comparing two numbers a and b:
| Functions | Numbers | Limit of f/g |
|---|---|---|
| f(n) = O(g(n)) | a ≤ b | finite |
| f(n) = Ω(g(n)) | a ≥ b | positive (or ∞) |
| f(n) = Θ(g(n)) | a = b | a constant 0 < c < ∞ |
| f(n) = o(g(n)) | a < b | 0 |
| f(n) = ω(g(n)) | a > b | ∞ |
Any two real numbers are comparable (trichotomy), but two functions need not be. Example: n and n1 + sin n. The exponent swings between 0 and 2, so neither is O of the other.
Using limits: the fastest way to compare
When the limit of f(n)/g(n) exists, it decides the relationship immediately. This is usually quicker than hunting for constants.
| If limn→∞ f(n)/g(n) is | then |
|---|---|
| = 0 | f(n) ∈ o(g(n)) (and so also O) |
| < ∞ | f(n) ∈ O(g(n)) |
| 0 < lim < ∞ | f(n) ∈ Θ(g(n)) |
| > 0 | f(n) ∈ Ω(g(n)) |
| = ∞ | f(n) ∈ ω(g(n)) (and so also Ω) |
| undefined (oscillates) | can't say from the limit |
L'Hôpital's rule is the main tool: if both go to ∞, lim fg = lim f′g′. Example, lg n versus √n: (lg n)′(√n)′ = 1/(n ln 2)1/(2√n) = 2ln 2 · √n → 0, so lg n = o(√n).
Properties
f = Θ(g) and g = Θ(h) ⇒ f = Θ(h). Same for O, Ω, o, ω.
f = Θ(f), f = O(f), f = Ω(f). Not o or ω: f < f is false.
f = Θ(g) if and only if g = Θ(f).
f = O(g) iff g = Ω(f).
f = o(g) iff g = ω(f).
Check yourself
Prove 5n2 + 100n = Θ(n2) with explicit constants.
Lower: 5n2 ≤ 5n2 + 100n for all n ≥ 1, so c1 = 5. Upper: 5n2 + 100n ≤ 6n2 ⇔ 100n ≤ n2 ⇔ n ≥ 100, so c2 = 6, n0 = 100. (Or c2 = 105, n0 = 1, since 100n ≤ 100n2.)
True or false: n2 = O(n3), n2 = Ω(n3), n3 = ω(n2), 2n+1 = Θ(2n).
True, false, true, true. 2n+1 = 2· 2n: a constant factor, so Θ. Compare with 22n, which is not Θ(2n).
What is the key difference between the definitions of O and o?
O: there exists some c > 0 with f(n) ≤ c g(n) eventually. o: for every c > 0, f(n) < c g(n) eventually. So o is strictly stronger and means f/g → 0.
Can a function be both O(g) and ω(g)?
No. ω(g) means f/g → ∞, while O(g) needs f/g to stay below some constant. (Similarly nothing is both o(g) and Ω(g).)
If an algorithm's worst case is Θ(n2) and its best case is Θ(n), what can you say about its running time in O and Ω terms?
The running time is O(n2) (the worst case bounds every input from above) and Ω(n) (the best case bounds every input from below). It is not Θ of a single function overall. That is exactly insertion sort.
Common functions and summations
The algebra toolkit. Log and exponent rules let you compare growth rates; summation formulas turn loops into closed-form running times. Most of this is memorization, and the cheat sheet repeats all of it.
Monotonicity, floors and ceilings
- monotonically increasing: m ≤ n ⇒ f(m) ≤ f(n)
- monotonically decreasing: m ≤ n ⇒ f(m) ≥ f(n)
- strictly increasing: m < n ⇒ f(m) < f(n)
- strictly decreasing: m < n ⇒ f(m) > f(n)
⌊ x ⌋ = greatest integer ≤ x; ⌈ x ⌉ = least integer ≥ x.
x − 1 < ⌊ x ⌋ ≤ x ≤ ⌈ x ⌉ < x + 1
For integer n: ⌈ n/2 ⌉ + ⌊ n/2 ⌋ = n.
a mod n = a − n⌊ a/n ⌋.
Floors appear all over the course: merge sort splits at q = ⌊ (p+r)/2 ⌋, a heap node's parent is ⌊ i/2 ⌋, a heap has height ⌊ lg n ⌋. A polynomial of degree d with positive leading coefficient is Θ(nd).
Exponentials
a−1 = 1/a · (am)n = amn · am an = am+n · a0 = 1
For all constants a > 1 and b: limn→∞ nban = 0, so nb = o(an).
Even n100 = o(1.01n).
Logarithms
x = logb a is the exponent with a = bx. Notation: ln a = loge a (natural), lg a = log2 a (binary, the course default), lgk a = (lg a)k (a power, not repeated log), lglg a = lg(lg a) (composition).
| Rule | Why / example |
|---|---|
| a = blogb a | Definition of log. 8 = 2lg 8 = 23. |
| logc(ab) = logc a + logc b | Product becomes sum. |
| logb an = n logb a | lg(n3) = 3 lg n. |
| logb a = logc alogc b | Change of base. log3 n = lg nlg 3. |
| logb(1/a) = −logb a | lg(1/8) = −3. |
| logb a = 1loga b | Change of base with c = a. |
| alogb c = clogb a | Swap rule. 3lg n = nlg 3 ≈ n1.585. Used constantly with the master method. |
Prove the swap rule alogb c = clogb a.
Take logb of both sides. Left: logb c · logb a. Right: logb a · logb c. They are equal, and logb is one-to-one, so the two sides are equal.
Changing a logarithm's base only changes it by a constant factor: log10 n · lg 10 = lg n. So the base of a log never matters inside O, Θ, Ω. Write O(lg n), never worry about log3 versus log2.
Exponentials with different bases differ by an exponential factor, not a constant: 2n = (2/3)n · 3n, and (2/3)n → 0. So 2n = o(3n): the base matters.
Polylogarithms and factorials
For constants a ≥ 0, b > 0: limn→∞ lga nnb = 0, so lga n = o(nb) and nb = ω(lga n).
Prove with L'Hôpital repeatedly, or substitute n = 2m: it becomes ma / 2bm, a polynomial over an exponential.
Example: lg100 n = o(n0.01).
Stirling's approximation: n! = √2π n(ne)n(1 + Θ(1n)).
Consequences: n! = o(nn), n! = ω(2n), and lg(n!) = Θ(n lg n).
Prove lg(n!) = Θ(nlg n) without Stirling.
Upper: n! = 1·2⋯ n ≤ nn, so lg(n!) ≤ nlg n.
Lower: the largest n/2 factors are each at least n/2, so n! ≥ (n/2)n/2 and lg(n!) ≥ n2 lgn2 = n2(lg n − 1) ≥ 14 n lg n for n ≥ 4.
Both bounds are constant multiples of nlg n. This fact is the heart of the Ω(n lg n) lower bound for comparison sorting.
F0 = 0, F1 = 1, Fi = Fi−1 + Fi−2: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, … They grow exponentially: Fi = ⌊ φi√5 + 12 ⌋ where φ = 1+√52 ≈ 1.618 is the golden ratio. So Fn = Θ(φn).
The growth-rate ladder
From slowest to fastest. Each function is o of the next one. Memorize this order; "rank these functions" is a classic exam question.
The slide's exercise, fully worked
"Express functions in A in asymptotic notation using functions in B."
| A | B | Answer | Reasoning |
|---|---|---|---|
| 5n2 + 100n | 3n2 + 2 | A ∈ Θ(B) | A ∈ Θ(n2) and n2 ∈ Θ(B); transitivity. |
| log3(n2) | log2(n3) | A ∈ Θ(B) | Change of base: A = 2lg nlg 3, B = 3lg n. A/B = 23lg 3, a positive constant. |
| nlg 4 | 3lg n | A ∈ ω(B) | A = n2. Swap rule: B = 3lg n = nlg 3 ≈ n1.585. A/B = nlg(4/3) → ∞. |
| lg2 n | n1/2 | A ∈ o(B) | Polylog versus polynomial with a = 2, b = 1/2: the limit is 0. |
Summations: why we need them
A loop's running time is a sum: add up the cost of each iteration. Nested loops give nested sums. The slides' example is the maximum subvector problem.
Given A[1..n] of numbers (positive, zero or negative), find the subvector A[i..j], 1 ≤ i ≤ j ≤ n, whose elements have the largest sum. For A = [1, −2, 2, 2] the answer is A[3..4] with sum 4.
Count how many times line 6 runs:
The slide says "this is not a simplified solution. What is the final answer?" Simplify from the inside out:
- Innermost: j∑k=i 1 = j − i + 1 (constant series: count of terms).
- Middle: n∑j=i (j − i + 1) = 1 + 2 + ⋯ + (n−i+1) = (n−i+1)(n−i+2)2 (arithmetic series).
- Outer: substitute p = n − i + 1, which runs from n down to 1: n∑p=1p(p+1)2 = 12(∑ p2 + ∑ p) = 12(n(n+1)(2n+1)6 + n(n+1)2).
- Simplify: = n(n+1)12(2n + 1 + 3) = n(n+1)(n+2)6.
T(n) = n(n+1)(n+2)6 = Θ(n3). For n = 4 that is 20 additions.
The slide then shows two faster solutions from Wikipedia's "Maximum subarray problem" page:
Scan left to right keeping best sum ending here = max(x, best ending at previous + x), and the best seen so far. A negative running sum is never worth extending, so you restart.
Same recurrence, but store dp[i] = best sum of a subvector ending at i in an array: dp[i] = A[i] + max(dp[i−1], 0). The answer is maxi dp[i].
Summation formulas to memorize
| Series | Formula | Growth |
|---|---|---|
| Constant | b∑i=a 1 = b − a + 1 | |
| Arithmetic (linear) | n∑i=1 i = n(n+1)2 | Θ(n2) |
| Quadratic | n∑i=1 i2 = n(n+1)(2n+1)6 | Θ(n3) |
| Cubic | n∑i=1 i3 = n2(n+1)24 | Θ(n4) |
| Geometric, x ≠ 1 | n∑k=0 xk = xn+1−1x−1 | Θ(xn) if x > 1 |
| Infinite geometric, |x| < 1 | ∞∑k=0 xk = 11−x | constant |
| Linear-geometric, c ≠ 1 | n∑i=1 i ci = n cn+2 − (n+1)cn+1 + c(c−1)2 | |
| Differentiated geometric, |x| < 1 | ∞∑k=0 k xk = x(1−x)2 | with x = 12: ∑ k2k = 2 |
| Harmonic | Hn = n∑k=11k = ln n + O(1) | Θ(lg n) |
| Telescoping | n∑k=1(ak − ak−1) = an − a0 | all middle terms cancel |
- Arithmetic series: insertion sort and selection sort's n(n−1)/2 comparisons.
- Geometric series: recursion trees where level costs shrink by a constant ratio (e.g. 3T(n/4) + n2).
- ∑ k/2k = 2: the proof that BuildMaxHeap runs in O(n) (lecture 8).
- Harmonic series: average-case insertion sort (lecture 7).
Approximation by integrals
When there is no closed form, bound the sum by integrals. For monotonically increasing f:
For monotonically decreasing f the inequalities flip:
How? (the slide asks) Draw each term f(k) as a rectangle of width 1. For a decreasing function, the rectangle on [k, k+1] with height f(k) sits above the curve, so the rectangles' total area (the sum) is at least the area under the curve. Shift the rectangles one unit left, to [k−1, k], and they sit below the curve, giving the upper bound.
Lower: n∑k=11k ≥ n+1∫1dxx = ln(n+1).
Upper: n∑k=21k ≤ n∫1dxx = ln n, so n∑k=11k ≤ ln n + 1.
(The k = 1 term is split off for the upper bound because ∫0 of 1/x diverges.) Together: Hn = Θ(lg n).
From loops to sums: a quick reference
| Loop shape | Iterations of the innermost body |
|---|---|
for i ← 1 to n / for j ← 1 to n | n2 |
for i ← 1 to n / for j ← 1 to i | n∑i=1 i = n(n+1)2 = Θ(n2) |
for i ← 1 to n / for j ← i+1 to n | n(n−1)2 (all pairs) |
i ← n; while i > 1: i ← ⌊i/2⌋ | ⌊ lg n ⌋ = Θ(lg n) |
for i ← 1 to n / j ← 1; while j < n: j ← 2j | n ⌈ lg n ⌉ = Θ(nlg n) |
for i ← 1 to n / j ← i; while j ≤ n: j ← j + i | n∑i=1 ni ≈ n Hn = Θ(nlg n) |
Check yourself
Simplify 2lg n, 4lg n and nlogn 7.
2lg n = n. 4lg n = nlg 4 = n2. nlogn 7 = 7.
Is log10 n = Θ(lg n)? Is 10n = Θ(2n)?
Yes: log10 n = lg n / lg 10, a constant multiple. No: 10n / 2n = 5n → ∞, so 10n = ω(2n).
Evaluate n∑i=1(3i + 2).
3·n(n+1)2 + 2n = 3n2 + 7n2 = Θ(n2).
Evaluate lg n∑k=0 2k (assume n is a power of 2).
Geometric with x = 2: 2lg n + 1 − 1 = 2n − 1 = Θ(n). A geometric series is dominated by its largest term.
Rank: n2, 2lg n, √n, (lg n)3, n lg n, 1.5n, n!.
(lg n)3 < √n < 2lg n = n < nlg n < n2 < 1.5n < n!.
Divide and conquer: merge sort and recurrences
Merge sort is the model divide-and-conquer algorithm. Analyzing it produces a recurrence, and the second half of the lecture is about solving recurrences three ways: substitution, recursion trees, and (introduced here, covered in lecture 6) the master method.
The paradigm
Divide-and-conquer algorithms are recursive in structure. Each level of recursion does three things:
Split the problem into subproblems that are similar to the original but smaller.
Solve the subproblems recursively. If a subproblem is small enough, solve it directly (the base case).
Put the subproblem solutions together into a solution for the original problem.
For merge sort on n elements: divide the sequence into two halves of n/2 elements; conquer by sorting each half recursively with merge sort; combine by merging the two sorted halves. The base case is a sequence of length 1, which is already sorted. All the real work happens in the combine step.
Merge sort
Initial call: MergeSort(A, 1, n).
When p ≥ r the subarray has at most one element, so the procedure does nothing: that is the base case, hidden in the if.
q is the midpoint, so A[p..q] gets ⌈ n/2 ⌉ elements and A[q+1..r] gets ⌊ n/2 ⌋.
The slide's example sorts ⟨ 18, 26, 32, 6, 43, 15, 9, 1⟩. The left half of the picture is the divide phase splitting down to single elements; the right half is the merges climbing back up. Step through it:
The Merge procedure
Input: an array where A[p..q] and A[q+1..r] are each already sorted. Output: A[p..r] sorted.
Lines 1–6 copy the two halves into new arrays L and R. Lines 11–16 repeatedly compare the smallest remaining element of each (L[i] and R[j]), copy the smaller one back into A, and advance that side's pointer. Think of two sorted piles of cards face up: always take the smaller top card.
Without them, every iteration would need to check whether L or R has run out. With ∞ at the end of each, an exhausted side always shows ∞, which loses every comparison, so the other side's remaining elements get copied automatically. The loop runs exactly r − p + 1 times and never touches a sentinel's position in A.
Correctness of Merge
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. Also, L[i] and R[j] are the smallest elements of their arrays that have not been copied back into A.
- Initialization. Before the first iteration k = p, so A[p..k−1] is empty: it trivially holds the 0 smallest elements. i = j = 1, and L[1], R[1] are the smallest elements of their (sorted) arrays, none copied yet.
- Maintenance. Case L[i] ≤ R[j]: by the invariant, L[i] is the smallest element not yet copied. Line 13 puts it in A[k], so A[p..k] now holds the k − p + 1 smallest, sorted. Incrementing i (line 14) and k (the for loop) re-establishes the invariant. The case L[i] > R[j] is symmetric (lines 15–16).
- Termination. The loop ends with k = r + 1. By the invariant, A[p..r] holds the r − p + 1 smallest elements of L and R, sorted. L and R together hold n1 + n2 + 2 = r − p + 3 elements, so everything except the two sentinels has been copied back.
Copying takes Θ(n1 + n2) = Θ(n) and the main loop runs n = r − p + 1 times doing constant work each: Merge is Θ(n). It also needs Θ(n) extra space for L and R, which is why merge sort is not in place. It is stable because ties go to L (the test is ≤).
Analysis of merge sort
Let T(n) be the worst-case running time on n elements (assume n is a power of 2, so halves are exact).
- Divide: computing the middle q takes Θ(1).
- Conquer: two subproblems of size n/2 take 2T(n/2).
- Combine: merging n elements takes Θ(n).
The rest of this lecture shows that T(n) = Θ(n lg n).
Recurrences
An equation or inequality that describes a function in terms of its value on smaller inputs. Recurrences come out of analyzing recursive (and sometimes iterative) algorithms.
A general divide-and-conquer algorithm that makes a subproblems, each 1/b the size, with divide cost D(n) and combine cost C(n):
Three solution methods (CLRS Chapter 4): substitution, recursion tree, master method.
Method 1: substitution
Guess the form of the solution, then prove it by mathematical induction. The name comes from substituting the guessed answer for the function when applying the inductive hypothesis to smaller values. It works well when the solution is easy to guess, and there is no general way to guess.
Recurrence: T(1) = 1; T(n) = 2T(n/2) + n for n > 1 (n a power of 2). Guess: T(n) = nlg n + n.
- Basis: n = 1: nlg n + n = 0 + 1 = 1 = T(1). ✓
- Hypothesis: T(k) = klg k + k for all k < n.
- Inductive step:
Show T(n) = 2T(⌊ n/2⌋) + n is O(nlg n). Guess T(n) ≤ c nlg n. Assume it holds for ⌊ n/2 ⌋:
T(n) ≤ 2(c⌊n2⌋lg⌊n2⌋) + n ≤ c nlgn2 + n = c nlg n − c n + n ≤ c nlg n as long as c ≥ 1.
Base case: T(1) = 1 but c· 1 · lg 1 = 0, so n = 1 fails. Asymptotic notation lets us pick n0: use n = 2 and n = 3 as base cases (T(2) = 4 ≤ 2c and T(3) = 5 ≤ 3clg 3 hold for c ≥ 2).
"Guess T(n) ≤ cn. Then T(n) ≤ 2cn2 + n = cn + n = O(n)." Wrong. The induction must prove the exact form of the hypothesis, T(n) ≤ cn, and cn + n is not ≤ cn. Writing "= O(n)" at the end hides the failure.
Method 2: recursion trees
A good guess can be hard to come by. A recursion tree draws out the recurrence: each node is the cost of one subproblem (not counting its recursive calls), children are its subproblems. Sum each level, then sum the levels. It organizes the algebra and produces a guess you can verify by substitution.
For merge sort, rewrite the recurrence with an explicit constant c > 0 (the base-case time, and the per-element time for divide plus combine): T(1) = c, T(n) = 2T(n/2) + cn.
- Each level costs cn: going down one level doubles the number of subproblems but halves the cost of each.
- Level i has 2i nodes of size n/2i; the leaves are at size 1, so the bottom level is i = lg n. That gives lg n + 1 levels (height lg n, assuming n is a power of 2; provable by induction).
- Total = (number of levels) × (cost per level) = (lg n + 1) cn = cnlg n + cn = Θ(nlg n).
The slide's two "other examples", solved
Use T(n) = 3T(n/4) + cn2. Level i has 3i nodes, each of size n/4i and cost c(n/4i)2. Level total: 3i · cn216i = (316)i cn2. The tree has depth log4 n, with 3log4 n = nlog4 3 leaves costing Θ(nlog4 3) in total.
The level costs form a decreasing geometric series (ratio 3/16), so even summed to infinity:
T(n) ≤ ∞∑i=0(316)i cn2 + Θ(nlog4 3) = 11 − 3/16cn2 + Θ(nlog4 3) = 1613cn2 + o(n2) = O(n2).
And the root alone costs cn2, so T(n) = Ω(n2). Answer: Θ(n2). The root dominates.
Use cost cn. The split is uneven but each level's sizes still add up to n, so every full level costs cn.
The longest root-to-leaf path keeps taking the 2/3 branch: n → 23 n → (23)2 n → ⋯ → 1, so it has length log3/2 n. The shortest path takes the 1/3 branch and has length log3 n.
Upper: at most log3/2 n + 1 levels, each costing at most cn: T(n) ≤ cn(log3/2 n + 1) = O(nlg n). Lower: the first log3 n levels are complete, each costing exactly cn: T(n) ≥ cnlog3 n = Ω(nlg n). Answer: Θ(nlg n). (The master method cannot do this one, because the two subproblems have different sizes.)
Recursion trees only generate guesses; verify the guess with the substitution method. If you draw the tree carefully and sum the costs exactly, it can serve as a direct proof.
A small amount of "sloppiness" can be tolerated. Why? Because the tree is only used to produce a guess, and the substitution proof that follows is rigorous. So it is fine to drop floors and ceilings, assume n is an exact power of b, or sum a geometric series to infinity: any error gets caught when you verify.
Method 3: the master method (preview)
A "cookbook" for recurrences of the form T(n) = aT(n/b) + f(n) with constants a ≥ 1, b > 1 and f asymptotically positive. It compares f(n) with nlogb a and has three cases to memorize. Lecture 6 covers it in full, with a solver you can practise on.
Check yourself
Merge L = ⟨2, 4, 5, 7⟩ with R = ⟨1, 2, 3, 6⟩. Which side does each output element come from?
Output 1 2 2 3 4 5 6 7, taken from R, L, R, R, L, L, R, L. The tie (2 versus 2) goes to L because the test is L[i] ≤ R[j]. That choice is what makes merge sort stable.
How many times is Merge called when sorting n = 8 elements? How many levels does the recursion tree have?
Merge is called once per internal node of the tree: 4 + 2 + 1 = 7 = n − 1 calls. The tree has lg 8 + 1 = 4 levels.
Write the recurrence for binary search and solve it with a recursion tree.
T(n) = T(n/2) + Θ(1). The tree is a single path of lg n + 1 nodes each costing c: T(n) = Θ(lg n).
Solve T(n) = 4T(n/2) + n with a recursion tree.
Level i: 4i nodes costing n/2i each, total 2i n, an increasing geometric series. The leaves dominate: there are 4lg n = n2 of them. T(n) = Θ(n2).
Is merge sort in place? Stable? What is its best-case running time?
Not in place (Θ(n) extra space for L and R). Stable. Best case is still Θ(nlg n): it does the same splitting and merging no matter how the input is ordered.
Recurrences II: the master method
A cookbook that solves most divide-and-conquer recurrences in three lines. Memorize the three cases, understand what each one means in the recursion tree, and know the situations where the theorem does not apply. Almost every algorithms midterm has at least one of these.
The master theorem
It applies to recurrences of the form
where a ≥ 1 and b > 1 are constants and f(n) is asymptotically positive. n/b may stand for ⌊ n/b ⌋ or ⌈ n/b ⌉.
Let a ≥ 1 and b > 1 be constants, let f(n) be a function, and let T(n) be defined on the non-negative integers by T(n) = aT(n/b) + f(n). Then T(n) can be bounded asymptotically as follows.
- If f(n) = O(nlogb a − ε) for some constant ε > 0, then T(n) = Θ(nlogb a).
- If f(n) = Θ(nlogb a), then T(n) = Θ(nlogb alg n).
- If f(n) = Ω(nlogb a + ε) for some constant ε > 0, and if a f(n/b) ≤ c f(n) for some constant c < 1 and all sufficiently large n, then T(n) = Θ(f(n)).
Everything hinges on one comparison: f(n) versus nlogb a. Call nlogb a the watershed function. The bigger of the two wins; if they tie, you pick up an extra lg n.
Rounding changes the subproblem sizes by less than 1, which perturbs the solution by at most a constant factor. CLRS section 4.6.2 proves the theorem still holds with floors or ceilings, so for asymptotic answers they can be dropped.
Why it works: the recursion tree
Draw the tree for T(n) = aT(n/b) + f(n). The root costs f(n) and has a children; each child costs f(n/b) and has a children, and so on.
- Level j has aj nodes, each of size n/bj, costing aj f(n/bj) in total.
- Sizes hit 1 when n/bj = 1, i.e. at depth j = logb n.
- Number of leaves = alogb n = nlogb a (by the log swap rule). Each costs Θ(1), so the leaves cost Θ(nlogb a).
That is why nlogb a is the thing to compare with: it is the total cost of the leaves. The three cases are the three ways that sum can behave.
f(n) is polynomially smaller than nlogb a. The level costs grow geometrically going down, so the bottom level (the leaves) dominates: Θ(nlogb a).
f(n) matches nlogb a. Every level costs about the same as the leaves, and there are Θ(lg n) levels: Θ(nlogb alg n).
f(n) is polynomially larger. The level costs shrink geometrically going down (that is what the regularity condition guarantees), so the root's f(n) dominates: Θ(f(n)).
You can see this in the recursion tree explorer on lecture 5: the level-cost bars grow, stay flat, or shrink.
Applying it: a recipe
- Check the form: constant a ≥ 1, constant b > 1, all subproblems the same size n/b.
- Compute nlogb a. Simplify the exponent when you can (log2 4 = 2, log4 16 = 2, log3 9 = 2); otherwise leave it, e.g. nlog2 3 ≈ n1.585.
- Compare f(n) with it.
- Smaller by a factor of nε? Case 1.
- Same up to a constant? Case 2.
- Larger by a factor of nε? Check regularity (a f(n/b) ≤ c f(n) with c < 1), then case 3.
- Larger or smaller but only by a log factor? The theorem does not apply.
- Write the answer.
"Asymptotically larger" is not enough for case 3; it must be larger by a polynomial factor nε. Same for case 1. nlg n is bigger than n, but only by lg n, and lg n = o(nε) for every ε > 0. So nlg n falls in the gap between cases 2 and 3.
The slide's four examples
a = 16, b = 4, nlogb a = nlog4 16 = n2. f(n) = n = O(n2−ε) with ε = 1. Case 1: T(n) = Θ(n2).
a = 1, b = 7/3 (since n/b = 3n/7). nlog7/3 1 = n0 = 1. f(n) = 1 = Θ(1). Case 2: T(n) = Θ(n0lg n) = Θ(lg n).
a = 3, b = 4, nlog4 3 ≈ n0.793. f(n) = nlg n = Ω(n0.793 + ε) with ε ≈ 0.2 (since nlg n ≥ n). Regularity: a f(n/b) = 3·n4lgn4 ≤ 34 nlg n = c f(n) with c = 34 < 1. Case 3: T(n) = Θ(nlg n).
a = 2, b = 2, nlog2 2 = n. f(n) = nlg n is asymptotically larger than n but not polynomially larger: the ratio f(n)/n = lg n is o(nε) for every ε > 0. The master theorem does not apply. (A recursion tree gives Θ(nlg2 n): each of the lg n levels costs about nlg n.)
More examples (the kind you will see on the exam)
| Recurrence | nlogb a | Case | Answer |
|---|---|---|---|
| 2T(n/2) + n (merge sort) | n | 2 | Θ(nlg n) |
| 9T(n/3) + n | n2 | 1 | Θ(n2) |
| T(2n/3) + 1 | 1 | 2 | Θ(lg n) |
| T(n/2) + 1 (binary search) | 1 | 2 | Θ(lg n) |
| 2T(n/2) + 1 | n | 1 | Θ(n) |
| 4T(n/2) + n | n2 | 1 | Θ(n2) |
| 4T(n/2) + n2 | n2 | 2 | Θ(n2lg n) |
| 4T(n/2) + n3 | n2 | 3 | Θ(n3) |
| 8T(n/2) + n2 | n3 | 1 | Θ(n3) |
| 7T(n/2) + n2 (Strassen) | nlg 7 ≈ n2.81 | 1 | Θ(nlg 7) |
| 3T(n/2) + n (Karatsuba) | nlg 3 ≈ n1.585 | 1 | Θ(nlg 3) |
| 2T(n/4) + √n | n1/2 | 2 | Θ(√nlg n) |
| 3T(n/3) + n/2 | n | 2 | Θ(nlg n) |
| 2T(n/2) + n/lg n | n | gap | does not apply |
| T(n/3) + T(2n/3) + n | — | does not apply (unequal sizes); Θ(nlg n) by tree | |
| T(n−1) + n | — | does not apply (not n/b); Θ(n2) by summing | |
| 0.5 T(n/2) + n | — | does not apply (a < 1) |
When the master theorem does not apply
- Subproblems of different sizes, like T(n/3) + T(2n/3) + n. Use a recursion tree.
- Not a division, like T(n−1) + n (that is insertion sort's recursive form). Unroll it: n + (n−1) + ⋯ + 1 = Θ(n2).
- a not a constant ≥ 1 or b not a constant > 1, like 2n T(n/2) + n or 0.5 T(n/2) + n.
- The gap: f(n) larger or smaller than nlogb a but not polynomially, like nlg n or n/lg n against n.
- Case 3 without regularity. Rare for the polynomial-type f you will see, because for f(n) = nk with k > logb a regularity is automatic: a f(n/b) = abknk and abk < 1 exactly when k > logb a.
Check yourself
Solve T(n) = 2T(n/2) + Θ(n) with the master method.
nlog2 2 = n = Θ(f(n)): case 2, Θ(nlg n). This is merge sort, matching the recursion tree.
Solve T(n) = 5T(n/2) + n2.
nlog2 5 ≈ n2.32. f(n) = n2 = O(n2.32 − ε) with ε ≈ 0.3: case 1, T(n) = Θ(nlog2 5).
Solve T(n) = 3T(n/9) + √n.
nlog9 3 = n1/2. f(n) = √n = Θ(n1/2): case 2, T(n) = Θ(√n lg n).
What does nlogb a count in the recursion tree?
The number of leaves: alogb n = nlogb a. Each leaf costs Θ(1), so it is the total cost of the bottom level.
State the regularity condition and say which case needs it.
Case 3: a f(n/b) ≤ c f(n) for some constant c < 1 and all large n. It says the cost shrinks going down each level, so the root dominates.
Elementary sorting algorithms
The vocabulary of sorting (stable, in place, comparison-based), two simple Θ(n2) algorithms analyzed carefully, a theorem explaining why they are stuck at n2, and Shellsort, which escapes by fixing many inversions per comparison.
Definitions
Input: n records R1, …, Rn. Each record has a key Ki and possibly other satellite information that travels with it (think: sorting student records by ID).
The keys need an ordering relation ≤ with two properties:
For any two keys a, b, exactly one of a < b, a = b, a > b is true.
For any keys a, b, c: if a ≤ b and b ≤ c then a ≤ c.
Such a relation is a total ordering (linear ordering) on the keys.
Find a permutation π = (p1, …, pn) of the records that puts the keys in non-decreasing order: Kp1 ≤ Kp2 ≤ ⋯ ≤ Kpn.
A one-to-one function from {1, …, n} onto itself. There are n! distinct permutations of n items.
The number of keys that precede it: rank(Kj) = |{Ki : Ki < Kj}|. With distinct keys, a key of rank r ends up at position r + 1.
Terminology (likely exam material)
| Term | Meaning |
|---|---|
| Internal vs external | Internal: the file is in main memory and can be accessed randomly. External: the file is on secondary storage and accessed sequentially. |
| Comparison-based | Uses only comparisons between keys, never special properties of how keys are represented (unlike radix sort, which looks at digits). |
| Stable | Records with equal keys keep their original relative order: if i < j and Kpi = Kpj then pi < pj. |
| Array-based vs list-based | Consecutive keys in consecutive memory, versus linked nodes in arbitrary locations. |
| In place | Needs only a constant amount of extra space beyond the array holding the keys. |
Sort a class list by name, then sort it again by grade with a stable sort: students with the same grade stay in alphabetical order. An unstable second sort could scramble them. Radix sort depends on this property.
Categories of sorting
| Sorting by… | Algorithms |
|---|---|
| Insertion | insertion sort, Shellsort |
| Exchange | bubble sort, quicksort |
| Selection | selection sort, heapsort |
| Merging | merge sort |
| Distribution | radix sort |
Why study elementary methods? They show the basic mechanisms clearly, they are good for small files and for well-structured files (almost sorted), and they are used inside more powerful methods (e.g. finishing off small subarrays in quicksort or merge sort).
Selection sort
The slide's version builds the sorted part at the right end: for i = n down to 2, find the largest element in A[1..i] and swap it into position i.
- In place? Yes: one temporary variable t plus indices.
- Stable? No. The swap can carry an element over an equal one. Example: ⟨ 3, 1a, 1b⟩. With i = 3 the max is the 3 at position 1; swapping it with A[3] = 1b gives ⟨ 1b, 1a, 3⟩, and the two 1s are now in reversed order.
- Comparisons: line 4 runs (n−1) + (n−2) + ⋯ + 1 = n(n−1)2 times in every case, sorted or not: Θ(n2) best, average and worst.
- Swaps: exactly n − 1, the fewest of any simple sort. Useful when moving records is expensive.
- Finding the maximum by a linear scan is the slow part. Doing it with a heap instead turns selection sort into heapsort (next lecture).
Insertion sort, analyzed more carefully
Same algorithm as lecture 3 (the slide writes the test as key < A[i], which is the same thing). It is in place and stable.
| Input | Comparisons |
|---|---|
| Already sorted | Θ(n) (exactly n − 1) |
| Reverse sorted | Θ(n2) (exactly n(n−1)/2) |
| Random order | Θ(n2) (about n2/4) |
Worst case
Inserting A[i] takes at most i − 1 comparisons, so
For which input does it perform exactly n(n−1)/2 comparisons? A strictly decreasing (reverse-sorted) array: every new key is smaller than everything before it and travels all the way to position 1.
Average case
Assume all orderings are equally likely. Then the key A[j] is equally likely (probability 1/j) to belong in any of the positions 1..j of the sorted prefix.
- If it belongs at position k with 2 ≤ k ≤ j, the while loop compares it with A[j−1], …, A[k−1]: that is j − k + 1 comparisons (the last one fails and stops the loop).
- If it belongs at position 1, it is compared with all j − 1 elements and the loop stops because i reaches 0, not because of a failed comparison: j − 1 comparisons.
Summing over all keys:
So on average insertion sort does about half the worst-case work, n2/4, which is still quadratic.
Inversions: why simple sorts are stuck at n²
A pair of positions (i, j) is an inversion of a permutation Π if i < j and Π(i) > Π(j): two elements that are out of order relative to each other.
A sorted array has 0 inversions. Sorting means removing all of them.
- Worst case: n(n−1)2 inversions, for the reverse-sorted permutation ⟨ n, n−1, …, 1⟩ (every pair is out of order).
- Average case: n(n−1)4. The argument: let ΠT be Π reversed, ⟨Πn, …, Π1⟩. For each of the n(n−1)2 pairs, the pair is an inversion in exactly one of Π and ΠT (reversing flips the relative order). So inv(Π) + inv(ΠT) = n(n−1)2. Permutations come in such pairs, so the average is half: n(n−1)4.
Each shift in insertion sort (line A[i+1] ← A[i]) swaps two adjacent out-of-order elements, which removes exactly one inversion. So insertion sort does exactly as many shifts as the input has inversions. Bubble sort, which also swaps adjacent elements, is in the same boat.
Any algorithm that sorts by comparison of keys and removes at most one inversion after each comparison must do at least n(n−1)2 comparisons in the worst case and at least n(n−1)4 comparisons on average.
So if we want to beat Θ(n2), each comparison has to remove more than a constant number of inversions. That means moving elements over long distances, which is exactly what Shellsort, merge sort, heapsort and quicksort do.
Shellsort
A simple extension of insertion sort that gains speed by letting elements that are far apart be exchanged, fixing many inversions with one move.
- To h-sort the file: split it into h interleaved subsequences, each made of keys h positions apart, and insertion-sort each one. The result is h-sorted: taking every h-th key starting anywhere gives a sorted sequence.
- h-sort for a decreasing sequence of increments, ending with h = 1.
- Large-h passes are cheap (short subsequences) and move elements long distances, so later small-h passes see a file that is nearly sorted, where insertion sort is fast.
- Correctness is easy: the last pass (h = 1) is plain insertion sort, which sorts anything.
Shellsort is really a family of algorithms, one for each increment sequence {hk}. The slide's code uses h ← 3h + 1, which gives 1, 4, 13, 40, 121, … (Knuth's sequence).
The slide writes for i = h to n and if j < h then break, which is correct with 0-based array indices. With 1-based indices the loop starts at h + 1 and the guard is j ≤ h, as shown. When h = 1 this is exactly insertion sort; otherwise it is insertion sort on keys h apart.
- In place, not stable (long-distance moves can carry an element past an equal one in a different subsequence).
- The exact behaviour depends on the increment sequence and is difficult to analyze.
- For increments hk = 2k − 1 (1, 3, 7, 15, …; Hibbard's sequence), the worst case is T(n) = Θ(n3/2): better than n2.
Summary of the sorts so far
| Algorithm | Best | Average | Worst | In place | Stable |
|---|---|---|---|---|---|
| Selection sort | Θ(n2) | Θ(n2) | Θ(n2) | yes | no |
| Insertion sort | Θ(n) | Θ(n2) | Θ(n2) | yes | yes |
| Shellsort (2k − 1) | Θ(n lg n) | depends | Θ(n3/2) | yes | no |
| Merge sort | Θ(nlg n) | Θ(nlg n) | Θ(nlg n) | no (Θ(n) extra) | yes |
| Heapsort | Θ(nlg n) | Θ(nlg n) | Θ(nlg n) | yes | no |
Check yourself
Define stable and in place. Classify the sorts from this course by these two properties.
Stable: equal keys keep their original relative order. In place: only O(1) extra space. Both: insertion sort. Stable but not in place: merge sort. In place but not stable: selection sort, Shellsort, heapsort.
How many inversions does ⟨2, 4, 1, 3⟩ have? List them.
Three: (2,1), (4,1), (4,3) by value, i.e. position pairs (1,3), (2,3), (2,4). Insertion sort will do exactly 3 shifts on it.
Why does selection sort do Θ(n²) comparisons even on a sorted array?
Its inner loop always scans the whole unsorted part to find the maximum; nothing lets it stop early. So it always does n(n−1)/2 comparisons.
What increments does the slide's ShellSort use for n = 50?
The first loop grows h: 1 → 4 → 13 → 40 → 121 (stops since 121 > 50). Then the repeat loop divides by 3 before each pass: passes with h = 40, 13, 4, 1.
What does the inversion theorem say, and what does it imply?
A comparison sort that removes at most one inversion per comparison needs at least n(n−1)/2 comparisons in the worst case and n(n−1)/4 on average. So to beat Θ(n2) an algorithm must remove many inversions per comparison, i.e. move elements long distances.
Heapsort and priority queues
Heapsort combines the best of merge sort (O(nlg n) time) and insertion sort (sorts in place). It also introduces a design technique: build a data structure, the heap, to manage information while the algorithm runs. The same heap gives an efficient priority queue.
The binary heap
A heap is an array viewed as a nearly complete binary tree. Physically it is a plain array; logically it is a tree that is completely filled on every level except possibly the lowest, which is filled from the left.
- Root: A[1]
- Left child of i: A[2i]
- Right child of i: A[2i + 1]
- Parent of i: A[⌊ i/2 ⌋]
In binary these are shifts: left child = shift left, parent = shift right. That is why they are fast.
length[A]: number of elements in the array.
heap-size[A]: number of those elements that are currently part of the heap. Always heap-size[A] ≤ length[A]. Heapsort shrinks heap-size while the sorted tail grows.
The heap property
For every node i other than the root: A[parent(i)] ≥ A[i]. So the largest element is at the root, and no value in a subtree is larger than that subtree's root.
For every node i other than the root: A[parent(i)] ≤ A[i]. The smallest element is at the root.
Heapsort uses max-heaps. Note that a heap's array is not sorted; only the parent-child relationship is guaranteed. Siblings can be in any order.
The slide's example max-heap, ⟨ 26, 24, 20, 18, 17, 19, 13, 12, 14, 11⟩. Click a node to see its parent and children; edit the array to test the heap property.
Height
The height of a node is the number of edges on the longest simple downward path from it to a leaf. The height of a tree is the height of its root.
- Height = ⌊ lg n ⌋. A nearly complete tree of height h has between 2h and 2h+1 − 1 nodes, so h = ⌊ lg n⌋. Hence basic heap operations run in O(lg n).
- Number of leaves = ⌈ n/2 ⌉. The leaves are exactly positions ⌊ n/2⌋ + 1, …, n (their "children" 2i would be past n).
- Number of nodes of height h is at most ⌈ n/2h+1 ⌉.
MaxHeapify: fixing one violation
Suppose the two subtrees of node i are max-heaps, but A[i] itself may be smaller than a child. Fix it by exchanging A[i] with the larger of its children. That may break the heap property in the child's subtree, so recurse there. The value "floats down" until it is no smaller than its children or reaches a leaf.
- Node 2 holds 14; its children are A[4] = 24 and A[5] = 17. Largest is 24. Exchange: A = ⟨ 26, 24, 20, 14, 17, …⟩.
- Recurse on node 4 (holding 14); its children are A[8] = 12 and A[9] = 18. Largest is 18. Exchange: A[4] = 18, A[9] = 14.
- Recurse on node 9: it is a leaf (no children within heap-size 10). Stop.
Result: ⟨ 26, 24, 20, 18, 17, 19, 13, 12, 14, 11⟩, the heap from the earlier example. Run it in the heap animator below (choose "MaxHeapify(A, 2)").
Running time of MaxHeapify
Fixing node i and its children takes Θ(1), plus the time to MaxHeapify the subtree rooted at one child:
The child's subtree is largest when the bottom level of the tree is exactly half full: then the left subtree has one more full level than the right one. Counting nodes, the left subtree holds about 2n/3 of them. The solution follows from case 2 of the master theorem (a = 1, b = 3/2, nlog3/21 = 1 = Θ(1)).
Alternatively: MaxHeapify on a node of height h takes O(h), since each recursive call goes down one level.
BuildMaxHeap: turning an array into a heap
Call MaxHeapify on every node, bottom-up. The leaves (positions ⌊ n/2⌋ + 1 to n) are already one-element heaps, so start at ⌊ n/2 ⌋ and go down to 1. Going bottom-up guarantees that when we reach node i, both its subtrees are already heaps, which is MaxHeapify's precondition.
n = 10, so call MaxHeapify on nodes 5, 4, 3, 2, 1 in that order.
- MaxHeapify(5): 36 versus child 27. Already fine.
- MaxHeapify(4): 22 versus children 34, 28. Swap with 34 → ⟨ 24, 21, 23, 34, 36, 29, 30, 22, 28, 27⟩.
- MaxHeapify(3): 23 versus 29, 30. Swap with 30 → ⟨ 24, 21, 30, 34, 36, 29, 23, 22, 28, 27⟩.
- MaxHeapify(2): 21 versus 34, 36. Swap with 36 (node 5); then 21 versus 27 (node 10), swap → ⟨ 24, 36, 30, 34, 27, 29, 23, 22, 28, 21⟩.
- MaxHeapify(1): 24 versus 36, 30. Swap with 36 (node 2); 24 versus 34, 27: swap with 34 (node 4); 24 versus 22, 28: swap with 28 (node 9) → ⟨ 36, 34, 30, 28, 27, 29, 23, 22, 24, 21⟩.
Final max-heap: ⟨ 36, 34, 30, 28, 27, 29, 23, 22, 24, 21⟩, matching the slide.
Correctness of BuildMaxHeap
At the start of each iteration of the for loop, each node i+1, i+2, …, n is the root of a max-heap.
- Initialization. Before the first iteration i = ⌊ n/2⌋. Nodes ⌊ n/2⌋ + 1, …, n are leaves, and each leaf is trivially the root of a one-node max-heap.
- Maintenance. The children of node i have larger indices than i, so by the invariant they are roots of max-heaps. That is exactly MaxHeapify's precondition, so MaxHeapify(i) makes node i a max-heap root, while keeping nodes i+1, …, n max-heap roots. Decrementing i re-establishes the invariant.
- Termination. The loop ends with i = 0. By the invariant, nodes 1, 2, …, n are all max-heap roots; in particular node 1 is, so the whole array is a max-heap.
Running time of BuildMaxHeap: O(n), not O(n lg n)
Loose bound: O(n) calls to MaxHeapify, each O(lg n): O(nlg n). Correct, but not tight.
Tight bound: MaxHeapify on a node of height h costs O(h), and most nodes are near the bottom where h is small. Half the nodes are leaves (h = 0), a quarter have h = 1, and so on. There are at most ⌈ n/2h+1⌉ nodes of height h, for h from 0 to ⌊lg n⌋:
using the differentiated geometric series ∞∑h=0 h xh = x(1−x)2 with x = 1/2: 1/2(1/2)2 = 2 (CLRS formula A.8).
You can build a heap from an unordered array in linear time. This is a favourite exam question: be ready to explain why the "obvious" O(nlg n) is not tight.
Heapsort
- Build a max-heap on all of A. The maximum is now at the root, A[1].
- Exchange A[1] with A[n]: the maximum moves to its correct final position.
- "Discard" A[n] by decrementing heap-size: it is now sorted and no longer part of the heap.
- The new root may violate the heap property (its subtrees are still heaps), so call MaxHeapify(A, 1).
- Repeat steps 2–4 until heap-size is down to 1 (the loop runs for i = n down to 2).
- In place: the heap and the sorted part share the one array.
- Not stable: swapping the root to the end jumps elements over equal ones.
- Build-Max-Heap takes O(n), and each of the n − 1 calls to MaxHeapify takes O(lg n): T(n) = O(nlg n).
| Procedure | Time |
|---|---|
| MaxHeapify | O(lg n) |
| BuildMaxHeap | O(n) |
| HeapSort | O(nlg n) |
Priority queues
The most popular application of heaps. A priority queue maintains a dynamic set S of elements, each with a key; the goal is to support insertion and extraction efficiently.
- Insert(S, x): S ← S ∪ {x}.
- Maximum(S): return the element with the largest key.
- Extract-Max(S): remove and return the element with the largest key.
- Increase-Key(S, x, k): raise x's key to the new value k (assumed ≥ the current key).
Insert, Minimum, Extract-Min, Decrease-Key.
Ready list of processes in an operating system, ordered by priority: a max-priority queue. The scheduler repeatedly takes the highest-priority job (Extract-Max), and Increase-Key can boost a job that has waited too long.
Event-driven simulator, events ordered by time of occurrence: a min-priority queue keyed by time. The simulator always processes the earliest event next (Extract-Min), and new events are Inserted with future times.
Why a heap? It is a good compromise. An unsorted list inserts in O(1) but needs Θ(n) to find the maximum; a sorted list finds the maximum in O(1) but needs Θ(n) to insert. A heap does both in O(lg n).
| Structure | Insert | Maximum | Extract-Max |
|---|---|---|---|
| Unsorted array/list | O(1) | Θ(n) | Θ(n) |
| Sorted array/list | Θ(n) | O(1) | O(1) |
| Binary max-heap | O(lg n) | Θ(1) | O(lg n) |
Extract-Max is one step of heapsort: move the last leaf to the root, shrink, and float it down. Its running time is dominated by MaxHeapify: O(lg n).
Increase-Key works in the opposite direction from MaxHeapify: the increased value may now be bigger than its parent, so it "floats up", swapping with its parent until the parent is larger or it reaches the root. The path from a node to the root has length O(lg n), so it is O(lg n).
Insert adds a new leaf with key −∞ (which cannot violate anything) and then increases it to the real key. The slides also show a direct version that shifts parents down instead of swapping (like insertion sort's shifting); both are O(lg n).
Start: ⟨ 16, 14, 10, 8, 7, 9, 3, 2, 4, 1⟩. Set A[9] = 15. Its parent A[4] = 8 < 15: swap. Now at node 4, parent A[2] = 14 < 15: swap. Now at node 2, parent A[1] = 16 > 15: stop. Result: ⟨ 16, 15, 10, 14, 7, 9, 3, 2, 8, 1⟩.
Check yourself
Is ⟨23, 17, 14, 6, 13, 10, 1, 5, 7, 12⟩ a max-heap? (CLRS 6.1-6)
No. Node 4 holds 6 and its child node 9 holds 7 > 6. Everything else is fine. Paste it into the heap explorer to see the violation highlighted.
Run BuildMaxHeap on ⟨5, 3, 17, 10, 84, 19, 6, 22, 9⟩. (CLRS 6.3-1)
MaxHeapify on 4, 3, 2, 1 gives ⟨ 84, 22, 19, 10, 3, 17, 6, 5, 9⟩. Check it with the animator's custom input.
Where in a max-heap can the smallest element be?
In a leaf (positions ⌊ n/2⌋ + 1 to n). An internal node has a child no larger than itself, so it cannot be the strict minimum (with distinct keys).
Is an array sorted in decreasing order a max-heap?
Yes. Every parent has a smaller index than its children, so it holds a value at least as large.
Why does BuildMaxHeap's loop go from ⌊n/2⌋ down to 1 and not from 1 up to ⌊n/2⌋?
MaxHeapify(i) requires both subtrees of i to already be heaps. Going downward from ⌊ n/2⌋ processes children before parents, so that precondition always holds. Going upward from 1 would call MaxHeapify on the root while its subtrees are still unsorted.
What are the minimum and maximum numbers of elements in a heap of height h?
Minimum 2h (levels 0 to h−1 full, one node on level h). Maximum 2h+1 − 1 (all levels full).
Cheat sheet
Read this the night before and the morning of the exam. Every item links back to the lecture where it is explained.
- Θ: ∃ c1, c2, n0 > 0: 0 ≤ c1 g ≤ f ≤ c2 g for all n ≥ n0
- O: ∃ c, n0: 0 ≤ f ≤ c g
- Ω: ∃ c, n0: 0 ≤ c g ≤ f
- o: ∀ c > 0 ∃ n0: 0 ≤ f < c g (⇔ f/g → 0)
- ω: ∀ c > 0 ∃ n0: 0 ≤ c g < f (⇔ f/g → ∞)
- Θ(g) = O(g) ∩ Ω(g)
L = lim f/g: L = 0 ⇒ o; L < ∞ ⇒ O; 0 < L < ∞ ⇒ Θ; L > 0 ⇒ Ω; L = ∞ ⇒ ω. Use L'Hôpital.
Analogy: O ∼ ≤, Ω ∼ ≥, Θ ∼ =, o ∼ <, ω ∼ >.
- Transitive: all five.
- Reflexive: Θ, O, Ω.
- Symmetric: Θ only.
- Transpose: f = O(g) ⇔ g = Ω(f); f = o(g) ⇔ g = ω(f).
- Worst case O(f) ⇒ every input O(f). Worst case Θ(f) ⇏ every input Θ(f).
Pick c1 just below and c2 just above the leading coefficient, then solve for n0.
10n2 − 3n: c1 = 9, c2 = 10, n0 = 3.
n2/2 − 3n: c1 = 1/14, c2 = 1/2, n0 = 7.
√n ≥ lg n: c = 1, n0 = 16.
1 < lglg n < lg n < lgk n < nε < √n < n < nlg n < n2 < n3 < 2n < 3n < n! < nn
lga n = o(nb); nb = o(an) for a > 1; lg(n!) = Θ(nlg n); log base irrelevant, exponent base matters.
- logb a = logc alogc b, logb a = 1loga b
- log(ab) = log a + log b, log an = nlog a
- alogb c = clogb a (e.g. 3lg n = nlg 3)
- (am)n = amn, am an = am+n, 2lg n = n
- x − 1 < ⌊ x⌋ ≤ x ≤ ⌈ x⌉ < x + 1
- n∑i=1 i = n(n+1)2
- n∑i=1 i2 = n(n+1)(2n+1)6
- n∑i=1 i3 = n2(n+1)24
- n∑k=0 xk = xn+1 − 1x − 1; ∞∑k=0 xk = 11−x for |x| < 1
- ∞∑k=0 kxk = x(1−x)2; ∑ k2k = 2
- Hn = n∑k=11k = ln n + O(1)
- Max-subvector triple loop: n(n+1)(n+2)6 = Θ(n3)
- RAM: one processor, sequential, unit-cost simple ops, flat unlimited memory, finite word.
- Running time = number of primitive steps on that input.
- Worst = max over inputs of size n; average needs a distribution; best is not useful.
- Linear search: best 4, worst 2n+4, average n + 3 (unit costs).
- Algorithm features: finiteness, definiteness, input, output, effectiveness.
- Initialization: true before the first iteration.
- Maintenance: true before an iteration ⇒ true before the next.
- Termination: invariant + exit condition ⇒ correctness.
Insertion sort: "A[1..j−1] holds the elements originally in A[1..j−1], in sorted order"; exits with j = n+1.
Merge: "A[p..k−1] holds the k−p smallest of L and R, sorted".
BuildMaxHeap: "nodes i+1..n are max-heap roots".
- D&C: T(n) = aT(n/b) + D(n) + C(n)
- Merge sort: 2T(n/2) + Θ(n) = Θ(nlg n); lg n + 1 levels of cost cn
- Substitution: guess, prove the exact form by induction
- Recursion tree: sum each level, sum levels; gives a guess
- 3T(n/4) + n2 = Θ(n2) (geometric, root dominates)
- T(n/3) + T(2n/3) + n = Θ(nlg n)
T(n) = aT(n/b) + f(n), a ≥ 1, b > 1. Compare f with nlogb a:
- f = O(nlogb a − ε) ⇒ Θ(nlogb a)
- f = Θ(nlogb a) ⇒ Θ(nlogb alg n)
- f = Ω(nlogb a + ε) and af(n/b) ≤ cf(n), c < 1 ⇒ Θ(f(n))
Gap (only a log factor apart, e.g. 2T(n/2) + nlg n): does not apply.
- Stable: equal keys keep their relative order.
- In place: O(1) extra space.
- Comparison-based: only compares keys.
- Inversion: i < j with Π(i) > Π(j); max n(n−1)2, average n(n−1)4.
- Removing ≤ 1 inversion per comparison ⇒ ≥ n(n−1)2 worst, n(n−1)4 average.
| best | avg | worst | in pl. | stable | |
|---|---|---|---|---|---|
| Selection | n2 | n2 | n2 | ✓ | ✗ |
| Insertion | n | n2 | n2 | ✓ | ✓ |
| Shell | nlg n | — | n3/2 | ✓ | ✗ |
| Merge | nlg n | nlg n | nlg n | ✗ | ✓ |
| Heap | nlg n | nlg n | nlg n | ✓ | ✗ |
All Θ. Selection: exactly n(n−1)/2 comparisons, n−1 swaps. Insertion: shifts = inversions.
- Parent ⌊ i/2⌋, left 2i, right 2i+1, root A[1].
- Max-heap: A[parent(i)] ≥ A[i].
- Height ⌊lg n⌋; leaves ⌈ n/2⌉ at ⌊ n/2⌋+1..n; nodes of height h ≤ ⌈ n/2h+1⌉.
- MaxHeapify O(lg n): T(n) ≤ T(2n/3) + Θ(1).
- BuildMaxHeap O(n): loop ⌊ n/2⌋ down to 1; ∑ h/2h = 2.
- HeapSort O(nlg n), in place, not stable.
- Priority queue: Maximum Θ(1); Extract-Max, Insert, Increase-Key O(lg n).
- OS scheduler → max-PQ; event simulator → min-PQ.
GCD(a, b) = GCD(b, a mod b), base GCD(a, 0) = a. GCD(210, 588) = 42. O(lg b) steps.
Flashcards
Read the prompt, say the answer out loud, then flip. Mark each card as known or not yet; the "not yet" pile comes back until you know it. Keyboard: Space flips, → marks known, ← marks not yet.
Practice exam
Questions in the style of a CLRS-based midterm, covering every lecture. Multiple-choice questions grade themselves and explain the answer. Written problems have a full model solution: work them on paper first, then compare.