notes.

Part IA · Lent

Algorithms 1

Sorting · Design Paradigms · Data Structures

Unverified
0 of 18 sections checked against the slides · 18 never reviewed
Foundations

Course Map

unreviewed

This course is about more than particular algorithms. The central idea is that algorithmic performance comes from structure: the structure of the problem, the recurrence, the data representation, and the invariant that makes correctness provable.

Main Themes
  • Sorting and order statistics.
  • Divide and conquer, dynamic programming, greedy design.
  • Core data structures: trees, heaps, hash tables, queues, lists.
What You Need To Be Able To Do
  • Prove correctness using loop invariants or data-structure invariants.
  • Solve or estimate recurrence relations.
  • Choose a data structure that improves the asymptotic cost of an operation set.
  • Reason about worst case, expected case, space use, and practical tradeoffs.
How to read these notes. Nearly every result in the course answers one of two questions: why is this correct (an invariant) and why is this the cost (a recurrence or an amortised count). If you can name both for an algorithm, you can reconstruct the rest.
  • Performance comes from structure — of the problem, the recurrence, and the data representation.
  • Four things to be able to do: prove correctness by invariant, solve recurrences, pick a structure that improves an operation set, and argue tradeoffs.
  • Every algorithm here has an invariant (why it is correct) and a recurrence or count (why it costs what it costs). Know both.

Correctness and Algorithm Analysis

unreviewed

An algorithm is correct if it terminates and returns the right output for every valid input instance. In this course correctness carries as much weight as speed, and most marks are lost by asserting a bound rather than justifying it.

Asymptotic Notation

f(n)O(g(n))f(n) \in O(g(n))
Upper bound — ff grows no faster than gg.
f(n)Ω(g(n))f(n) \in \Omega(g(n))
Lower bound — ff grows at least as fast as gg.
f(n)Θ(g(n))f(n) \in \Theta(g(n))
Tight bound — both of the above.
f(n)o(g(n))f(n) \in o(g(n))
Strictly slower growth.
f(n)ω(g(n))f(n) \in \omega(g(n))
Strictly faster growth.

Cost Models

  • For comparison sorts, count key comparisons.
  • Ignore fixed-cost arithmetic and control overhead when it only moves constant factors.
  • Remember the hidden costs: cache behaviour, pointer chasing, hash computation, equality checks.

Loop Invariants

Where an algorithm contains a loop, correctness is usually proved by naming a property every iteration preserves.

  1. Initialisation — true before the loop starts.
  2. Maintenance — one iteration preserves it.
  3. Termination — on exit, invariant plus stop condition imply correctness.
Exam note. Name the invariant explicitly and structure the proof as initialisation, maintenance, termination. The slides do exactly this for insertion sort and for partition.
  • Correct = terminates and right output on every valid input.
  • OO upper, Ω\Omega lower, Θ\Theta tight; oo and ω\omega are the strict versions.
  • Comparison sorts are costed in key comparisons; constant factors and control overhead are ignored.
  • Loop invariant proof has exactly three parts: initialisation, maintenance, termination. Termination is the invariant plus the negated loop guard.
Multiple choice · 3 questions

A loop-invariant proof establishes correctness at termination by combining the invariant with:

Termination combines the invariant with the negated loop guard. Neither piece is sufficient alone — that pairing is what forces the result.

Which statement is true of f(n) ∈ o(g(n)) but not of f(n) ∈ O(g(n))?

Little-o is the strict version: f(n)/g(n) → 0. Big-O permits f and g to grow at the same rate, so n ∈ O(n) but n ∉ o(n).

Why do comparison sorts get costed in key comparisons rather than in total operations?

Index arithmetic and control flow move constant factors only. Counting comparisons isolates the term that actually decides the growth rate — and it is what the Ω(n log n) decision-tree bound counts.
Conceptual · self-assessed

Insertion sort and merge sort are both correct. Explain why an invariant argument is natural for one and awkward for the other.

Insertion sort is iterative, so it has state that persists across iterations — a sorted prefix — which is precisely what an invariant describes. Merge sort is recursive: its correctness argument is structural induction over subproblem size, with the base case and the merge step carrying the weight. A strong answer names the shape of each argument, not just the algorithms.

Recurrence Relations

unreviewed

Recurrences are how divide-and-conquer costs get expressed. Four methods appear in the course, and the exam expects you to pick the cheapest one that works.

Solving Methods

  • Substitute and spot a pattern — fastest when the expansion is regular.
  • Recursion tree — best when work per level is easy to total.
  • Guess and verify — needs a candidate, then induction.
  • Master theorem — only for the standard shape below.

Master Theorem

For a recurrence of the form

T(n)=aT(n/b)+f(n),a1,  b>1 T(n) = a\,T(n/b) + f(n), \qquad a \ge 1,\; b > 1

compare f(n)f(n) against nlogban^{\log_b a}, the total work done at the leaves of the recursion tree.

  • If f(n)f(n) is polynomially smaller, the recursive work dominates: T(n)=Θ(nlogba)T(n) = \Theta(n^{\log_b a}).
  • If they are the same order, every level costs the same and you pick up a factor of logn\log n: T(n)=Θ(nlogbalogn)T(n) = \Theta(n^{\log_b a}\log n).
  • If f(n)f(n) is polynomially larger and the regularity condition af(n/b)cf(n)a\,f(n/b) \le c\,f(n) holds for some c<1c < 1, the non-recursive work dominates: T(n)=Θ(f(n))T(n) = \Theta(f(n)).
Worth memorising
  • T(n)=2T(n/2)+Θ(n)Θ(nlogn)T(n) = 2T(n/2) + \Theta(n) \Rightarrow \Theta(n \log n) — merge sort.
  • T(n)=T(n/2)+Θ(1)Θ(logn)T(n) = T(n/2) + \Theta(1) \Rightarrow \Theta(\log n) — binary search.
  • T(n)=2T(n/2)+Θ(1)Θ(n)T(n) = 2T(n/2) + \Theta(1) \Rightarrow \Theta(n) — tree traversal.
  • Four methods: substitution, recursion tree, guess-and-verify, master theorem.
  • Master theorem — for T(n)=aT(n/b)+f(n)T(n) = a\,T(n/b) + f(n), compare f(n)f(n) with nlogban^{\log_b a}: smaller → recursion dominates; equal → extra logn\log n; larger (plus regularity) → f(n)f(n) dominates.
  • Three to know cold: 2T(n/2)+Θ(n)=Θ(nlogn)2T(n/2)+\Theta(n) = \Theta(n\log n), T(n/2)+Θ(1)=Θ(logn)T(n/2)+\Theta(1) = \Theta(\log n), 2T(n/2)+Θ(1)=Θ(n)2T(n/2)+\Theta(1) = \Theta(n).
Multiple choice · 3 questions

In T(n) = a·T(n/b) + f(n), the master theorem decides the case by comparing f(n) against:

n^(log_b a) is the total leaf work of the recursion tree. The theorem is really asking whether the leaves or the root dominate.

T(n) = 2T(n/2) + Θ(n) solves to:

Here f(n) = Θ(n) and n^(log_2 2) = n, so the two are the same order — case 2, which picks up an extra log n. This is merge sort.

Which recurrence describes binary search?

Binary search discards half the input and recurses into one side only, doing constant work per level — Θ(log n). Recursing into both halves would give Θ(n).
Conceptual · self-assessed

Case 3 of the master theorem carries a regularity condition. What breaks without it, and can you describe an f(n) that fails it?

Regularity guarantees the non-recursive work actually shrinks geometrically down the tree, so the total is dominated by the root. Without it, f can be polynomially larger at each level yet oscillate, so the levels never sum to Θ(f(n)). A well-known failure is f(n) = n·(2 + sin n), which grows fast enough but is not eventually well-behaved.
Sorting

Quadratic Sorting Algorithms

unreviewed

These are Θ(n2)\Theta(n^2) algorithms and they still matter — insertion sort is what the good Θ(nlogn)\Theta(n \log n) sorts drop into once subarrays get small enough that a tight loop beats the recursion overhead.

AlgorithmWorstBestStableIn-place
Insertion sortΘ(n2)\Theta(n^2)Θ(n)\Theta(n)YesYes
Selection sortΘ(n2)\Theta(n^2)Θ(n2)\Theta(n^2)NoYes

Insertion Sort

Incremental algorithm: maintain a sorted prefix and insert the next key into the right place by shifting larger elements right.

  • Worst and average case: Θ(n2)\Theta(n^2). Best case Θ(n)\Theta(n) on already-sorted input.
  • Stable and in-place.
  • Very good for small arrays because the loop body is tight.

Canonical Invariant

At the start of iteration jj, the prefix A[1j1]A[1 \ldots j-1] is sorted and contains exactly the original elements from those positions.

Selection Sort

Repeatedly select the smallest remaining item and place it into the next position.

  • Cost Θ(n2)\Theta(n^2) in every case — it scans the whole remaining array regardless of order.
  • Usually not stable unless carefully adapted.
  • Can reduce the number of writes compared with insertion sort.
Why These Still Matter

They are not just “bad algorithms”. They teach invariant-based correctness, and insertion sort appears as a subroutine in stronger algorithms, including median-of-medians and practical hybrid sorts.

Watch for. Selection sort's best case is still Θ(n2)\Theta(n^2). Insertion sort's Θ(n)\Theta(n) best case is exactly what makes it the standard small-array fallback.
  • Both Θ(n2)\Theta(n^2) worst case, both in-place. Insertion sort is stable, selection sort is not.
  • Insertion sort best case Θ(n)\Theta(n) on sorted input — why it is the small-array fallback inside faster sorts.
  • Selection sort best case is still Θ(n2)\Theta(n^2): it always scans the whole remainder. It does fewer writes.
  • Insertion sort invariant: at the start of iteration jj, A[1j1]A[1 \ldots j-1] is sorted and holds exactly those original elements.
Multiple choice · 2 questions

Selection sort's best-case running time is:

It scans the whole remaining array on every pass to find the minimum, regardless of order. There is no early exit, so best equals worst.

Why is insertion sort used as the base case inside faster sorting algorithms?

Asymptotics say nothing at n = 16. Insertion sort's loop body is tight and its best case is Θ(n), so below a threshold it wins on the constants that Θ notation deliberately hides.
Conceptual · self-assessed

Both insertion sort and selection sort are Θ(n²) and in-place. Give a concrete situation where you would still prefer each one.

Insertion sort for nearly-sorted or small data, and wherever stability matters — its Θ(n) best case is real and its inner loop stops early. Selection sort where writes are expensive relative to reads (flash memory, or large records moved by value): it performs exactly n−1 swaps regardless of input, whereas insertion sort can perform Θ(n²) shifts. A strong answer names the resource being optimised, not just 'it depends on the data'.

Merge Sort and Quicksort

unreviewed

Merge Sort

Classic divide-and-conquer: split in half, recursively sort both halves, then merge.

T(n)=2T(n/2)+Θ(n)    Θ(nlogn) T(n) = 2T(n/2) + \Theta(n) \;\Rightarrow\; \Theta(n \log n)
  • Stable: yes.
  • Guaranteed worst case Θ(nlogn)\Theta(n \log n) — no bad inputs.
  • Memory behaviour matters: textbook merge sort uses extra space; the slides note bottom-up merge sort can be in-place.

Quicksort

Partition around a pivot, then recursively sort the two sides.

  • Best case: balanced split every time, Θ(nlogn)\Theta(n \log n).
  • Any constant-ratio split still gives Θ(nlogn)\Theta(n \log n) — a 99:1 split is asymptotically fine.
  • Worst case: constant-and-rest split every time, Θ(n2)\Theta(n^2).
  • Expected case Θ(nlogn)\Theta(n \log n) with sensible or random pivots.
  • In-place in the standard array partition variants; not stable in that form.

Partition Correctness

The slides prove PARTITION with the invariant that elements left of the boundary ii are \le pivot, elements between i+1i+1 and j1j-1 are >> pivot, and the pivot itself is preserved at the end until the final swap.

Pivot Strategy Tradeoffs

  • Naive last or first element — vulnerable to sorted or adversarial input.
  • Random input shuffling — makes all orderings roughly equally likely.
  • Random pivot — removes the dependence on input order.
  • Median-of-three — lowers the probability of an extreme pivot.
  • Three-way partition — important when many duplicate keys occur.
  • Median-of-medians — deterministic good pivot, stronger worst-case guarantee, larger constants.
The comparison that gets examined. Merge sort buys a worst-case guarantee with memory; quicksort buys memory and cache behaviour with a worst case it can usually avoid. Say which resource you are trading, not just which is “faster”.
  • Merge sort2T(n/2)+Θ(n)=Θ(nlogn)2T(n/2)+\Theta(n) = \Theta(n\log n) guaranteed, stable, usually not in-place.
  • Quicksort — expected Θ(nlogn)\Theta(n\log n), worst Θ(n2)\Theta(n^2), in-place, not stable. Any constant-ratio split is still Θ(nlogn)\Theta(n\log n).
  • PARTITION invariant: left of ii is \le pivot, from i+1i+1 to j1j-1 is >> pivot, pivot swapped into place at the end.
  • Pivot choices, weakest to strongest: fixed → shuffle → random → median-of-three → median-of-medians. Three-way partition for many duplicates.
Multiple choice · 3 questions

Quicksort splits every subarray in a 99:1 ratio. Its running time is:

Any constant-ratio split gives a recursion tree of depth O(log n) with Θ(n) work per level. The base of the logarithm changes; the asymptotic class does not.

Which resource does merge sort trade to obtain its worst-case guarantee?

Textbook merge sort needs an auxiliary array for the merge. Quicksort is in-place and cache-friendly but has a Θ(n²) worst case — the two sit on opposite sides of the same trade.

In the PARTITION invariant, elements strictly between index i+1 and j-1 are:

Left of the boundary i holds elements ≤ pivot; the region from i+1 to j-1 holds elements > pivot; from j onwards is unexamined. The final swap puts the pivot between the two known regions.
Conceptual · self-assessed

Randomising the pivot does not improve quicksort's worst case, which stays Θ(n²). Explain why it is nevertheless the standard fix.

The worst case still exists, but randomisation removes the correspondence between a particular *input* and bad behaviour: no fixed input is reliably slow, so an adversary who knows the algorithm cannot construct one, and sorted or reverse-sorted real-world data stops being a trap. The guarantee changes from 'fast unless the input is unlucky' to 'fast unless the coin flips are unlucky', and the probability of consistently bad splits decays exponentially. A strong answer distinguishes worst case from expected case and says who the adversary is.

Heapsort and Heaps

unreviewed

A binary heap is a semi-structured array representation of a nearly-complete binary tree.

Heap Properties

  1. Structural — complete except possibly the bottom level, which fills left to right.
  2. Ordering — in a max-heap, a parent key is at least each child key; symmetrically for min-heaps.

Array Mapping

The tree needs no pointers because position encodes structure:

LEFT(i)=2i,RIGHT(i)=2i+1,PARENT(i)=i/2 \mathrm{LEFT}(i) = 2i, \qquad \mathrm{RIGHT}(i) = 2i+1, \qquad \mathrm{PARENT}(i) = \lfloor i/2 \rfloor

Main Operations

  • REHEAPIFYO(logn)O(\log n).
  • FULL-HEAPIFYΘ(n)\Theta(n), not Θ(nlogn)\Theta(n \log n).
  • EXTRACT-MAX or EXTRACT-MINO(logn)O(\log n).
  • PEEKO(1)O(1).
  • INSERT, key increase and decrease — O(logn)O(\log n).

Heapsort

  1. Build a max-heap from the array in Θ(n)\Theta(n).
  2. Repeatedly swap the root with the final item in the heap region.
  3. Shrink the heap by one and reheapify the root.

Total cost O(nlogn)O(n \log n). It is in-place but not stable.

The subtle point. Building the heap is linear, not Θ(nlogn)\Theta(n\log n): most nodes are near the bottom, where reheapify has almost no distance to travel. The nodes that are expensive to fix are the rare ones.
  • Two properties: structural (complete, fills left to right) and ordering (parent \ge children in a max-heap).
  • Array mapping: LEFT(i)=2i\mathrm{LEFT}(i)=2i, RIGHT(i)=2i+1\mathrm{RIGHT}(i)=2i+1, PARENT(i)=i/2\mathrm{PARENT}(i)=\lfloor i/2 \rfloor — no pointers needed.
  • FULL-HEAPIFY is Θ(n)\Theta(n), not Θ(nlogn)\Theta(n\log n). Most nodes are near the bottom and cheap to fix. This is the single most examined trap in the topic.
  • PEEK O(1)O(1); reheapify, extract and insert all O(logn)O(\log n).
  • Heapsort: build max-heap Θ(n)\Theta(n), then repeatedly swap-root-and-shrink. O(nlogn)O(n\log n), in-place, not stable.
Multiple choice · 3 questions

Building a heap from an unsorted array of n elements costs:

The bound most often got wrong. Half the nodes are leaves and need no work, and reheapify cost is proportional to height — the many cheap nodes dominate the count, and the sum converges to Θ(n).

In the array representation of a binary heap, the parent of node i is at index:

LEFT(i) = 2i, RIGHT(i) = 2i+1, PARENT(i) = ⌊i/2⌋. Position encodes structure, which is why a heap needs no pointers.

Heapsort is:

It sorts within the array, so it is in-place. Swapping the root with the last heap element moves equal keys past one another arbitrarily, so it is not stable.
Conceptual · self-assessed

Explain why FULL-HEAPIFY is Θ(n) while heapsort as a whole is Θ(n log n), given that both are built from the same reheapify operation.

Building the heap calls reheapify once per node, but on nodes whose height is mostly tiny: with n/2 leaves at height 0 and only one node at height log n, the sum of heights is Θ(n). Heapsort's second phase calls reheapify n times *from the root*, where the height is always the full log of the current heap size, so no such saving occurs. The point is that the cost of reheapify is proportional to the node's height, not to n — and the two phases apply it to completely different distributions of heights.

Sorting in Linear Time

unreviewed

Comparison sorting has an Ω(nlogn)\Omega(n \log n) lower bound — a decision tree with n!n! leaves has height at least log2(n!)=Ω(nlogn)\log_2(n!) = \Omega(n \log n). To beat it you must stop comparing and start using structure in the keys themselves.

Counting Sort

For keys drawn from a bounded integer range [0k][0 \ldots k].

  • Time Θ(n+k)\Theta(n + k) — linear only while k=O(n)k = O(n).
  • Stable, provided the output fill runs right to left as in CLRS and the slides.
  • Not comparison-based.
  • Usually not in-place, because of the output array BB and the count array CC.

Radix Sort

Sort by digits from least significant to most significant, using a stable sort at each pass.

Θ(d(n+k)) \Theta\bigl(d(n + k)\bigr)

where dd is the number of digits and kk is the digit range. Stability at each pass is what makes the earlier passes survive the later ones — with an unstable inner sort the algorithm is simply wrong.

Bucket Sort

For values uniformly distributed over a known interval such as [0,1)[0,1).

  • Average case Θ(n)\Theta(n) — under the uniformity assumption.
  • Worst case Θ(n2)\Theta(n^2) if the buckets become highly uneven.

Stability

A stable sort preserves the relative order of items with equal keys. It matters directly for radix sort, and indirectly wherever a sort is applied on top of an earlier ordering.

  • Comparison sorting is Ω(nlogn)\Omega(n\log n) — decision tree with n!n! leaves. Beating it requires assumptions about the keys.
  • Counting sort Θ(n+k)\Theta(n+k), stable, not in-place. Linear only while k=O(n)k = O(n).
  • Radix sort Θ(d(n+k))\Theta(d(n+k)), least significant digit first, requires a stable inner sort or it is wrong.
  • Bucket sort Θ(n)\Theta(n) average under uniformity, Θ(n2)\Theta(n^2) worst.
Multiple choice · 2 questions

Radix sort requires the sort used at each digit pass to be stable because:

Radix sort is not merely slower with an unstable inner sort — it is wrong. Each pass must preserve the relative order that earlier passes established on ties.

Counting sort runs in Θ(n + k). It stops being a linear-time sort when:

The count array is size k. With k = O(n) the total is Θ(n); with k = n², the k term dominates and you have done worse than comparison sorting.
Conceptual · self-assessed

Counting sort beats the Ω(n log n) comparison lower bound. Explain precisely why that is not a contradiction.

The lower bound applies to the comparison model, where the only information about the input comes from pairwise key comparisons — a decision tree with n! leaves needs height Ω(n log n). Counting sort never compares two keys; it uses a key directly as an array index, which is an operation outside the model and extracts far more information per step. A strong answer identifies the bound as a statement about a *model of computation*, not about sorting itself, and notes what counting sort assumes in exchange: bounded integer keys.

Median and Order Statistics

unreviewed

The iith order statistic is the iith smallest element. The selection problem asks for it without fully sorting.

Quickselect

The same partition idea as quicksort, but recurse only into the side containing the target rank.

  • Expected Θ(n)\Theta(n); worst case with bad pivots Θ(n2)\Theta(n^2).
  • Much faster in practice than sorting when only one rank is needed.

Median-of-Medians

Deterministic pivot selection guaranteeing a useful ratio split.

  1. Group the elements in fives.
  2. Sort each group and take its median.
  3. Recursively select the median of those medians.
  4. Use that as the pivot.

This guarantees at least about 3n/1063n/10 - 6 elements on each side, so the recursion is

T(n)=T(n/5)+T(7n/10+6)+Θ(n)    Θ(n) T(n) = T(\lceil n/5 \rceil) + T(7n/10 + 6) + \Theta(n) \;\Rightarrow\; \Theta(n)

The two recursive terms sum to strictly less than nn, which is exactly why the total collapses to linear.

Quicksort with Median-of-Medians

If distinct keys allow the true median to be used at every step, quicksort gets a guaranteed balanced split and therefore Θ(nlogn)\Theta(n \log n) worst-case time. The tradeoff is much larger constants — it is a proof device more than a practical sort.

  • Quickselect — partition, recurse into one side only. Expected Θ(n)\Theta(n), worst Θ(n2)\Theta(n^2).
  • Median-of-medians — groups of five, medians of groups, recursively select their median, use as pivot. Guarantees roughly 3n/103n/10 either side.
  • T(n)=T(n/5)+T(7n/10+6)+Θ(n)=Θ(n)T(n) = T(\lceil n/5\rceil) + T(7n/10+6) + \Theta(n) = \Theta(n); linear because 1/5+7/10<11/5 + 7/10 < 1.
  • Quicksort with MoM pivots is Θ(nlogn)\Theta(n\log n) worst case, but the constants make it impractical.
Multiple choice · 2 questions

Median-of-medians groups elements in fives. The resulting recurrence T(n) = T(⌈n/5⌉) + T(7n/10 + 6) + Θ(n) is linear because:

1/5 + 7/10 = 9/10 < 1, so the work per level decays geometrically and the total is a convergent series times Θ(n). Groups of three would give 1/3 + 2/3 = 1 and fail.

Quickselect's expected running time is:

It recurses into one side only, so the expected work per level halves: n + n/2 + n/4 + … = Θ(n). The worst case with bad pivots is still Θ(n²).
Conceptual · self-assessed

Quicksort with median-of-medians pivots achieves Θ(n log n) worst case, yet nobody uses it. Why is it still worth knowing?

It settles a question of principle — that quicksort's Θ(n²) worst case is a property of pivot selection, not of the partition-based approach — and it is the constructive proof that linear-time selection exists. Practically the constants are large: five-element groups, a recursive median call and extra passes, all to avoid a case randomisation already makes vanishingly unlikely. A good answer separates 'this establishes what is possible' from 'this is what you should run', and notes that its real use is as a subroutine for selection rather than as a sort.
Design Paradigms

Algorithm Design Paradigms

unreviewed
ParadigmCore IdeaWhen It Works Best
IncrementalGrow a partial solution one step at a time.Local maintenance is cheap.
Divide and conquerSplit into independent subproblems.Subproblems do not overlap much.
Dynamic programmingReuse overlapping subproblem answers.Optimal substructure and overlap.
GreedyCommit to the best-looking local move.Greedy-choice property and optimal substructure.
Divide and conquer against dynamic programming. The distinguishing test is overlap, not recursion. Both recurse; only DP recurses into the same subproblem repeatedly, which is what makes memoising it worthwhile.
  • Incremental — grow a partial solution; needs cheap local maintenance.
  • Divide and conquer — independent subproblems.
  • Dynamic programming — overlapping subproblems plus optimal substructure.
  • Greedy — greedy-choice property plus optimal substructure.
  • D&C against DP is decided by overlap, not by whether it recurses.

Dynamic Programming

unreviewed

DP is for optimisation problems with optimal substructure and overlapping subproblems. Both conditions are required: without substructure the recurrence is invalid, without overlap you have gained nothing over plain recursion.

Canonical Workflow

  1. Characterise the structure of an optimal solution.
  2. Write a recurrence for the value of an optimal solution.
  3. Compute the values by memoisation or bottom-up filling.
  4. If needed, reconstruct the actual solution from the table.

Top-Down against Bottom-Up

ApproachAdvantagesDisadvantages
Top-down memoisationOnly solves the subproblems actually needed; the recurrence translates directly.Uses the recursion stack.
Bottom-up tabulationNo recursion stack; often a simpler loop once the fill order is known.May solve subproblems that are never used.

Example Style

The slides use a rod-cutting and VM-hosting optimisation example. The naive recursive version is exponential because it recomputes the same suffix problems repeatedly; memoisation collapses it to polynomial time by making each distinct subproblem cost once.

The signal to look for. If a recursive optimisation branches into the same smaller parameter values again and again, that is your strongest evidence that DP is the right paradigm.
  • Needs both optimal substructure and overlapping subproblems.
  • Workflow: characterise the optimal structure → write the value recurrence → memoise or tabulate → reconstruct if asked.
  • Top-down solves only what is needed but uses the stack; bottom-up avoids the stack but may compute unused entries.
  • Signal: the same parameter values recurring in the recursion tree.
Multiple choice · 2 questions

Dynamic programming is the right paradigm when a problem has optimal substructure and:

Optimal substructure alone gives divide and conquer. Overlap is what makes storing answers pay — without it, memoisation adds bookkeeping and saves nothing.

Which is a genuine advantage of top-down memoisation over bottom-up tabulation?

Tabulation fills the whole table, including entries no path ever consults. Memoisation is demand-driven, at the cost of the recursion stack.
Conceptual · self-assessed

A colleague memoises a divide-and-conquer algorithm whose subproblems do not overlap. What happens, and what does that tell you about what DP actually buys?

Every lookup misses, so the table is written once and read never: the running time is unchanged apart from added constant-factor bookkeeping, and the space cost is now proportional to the number of distinct subproblems. This isolates what DP buys — not recursion, not tabulation, but the *reuse* of answers. The saving is exactly the ratio of recursion-tree nodes to distinct subproblems, which is why the exponential-to-polynomial collapse in rod cutting happens: an exponential tree over polynomially many distinct parameters.

Greedy Algorithms

unreviewed

A greedy algorithm picks one locally optimal move and commits to it immediately. That is only valid when local optimality is globally safe, which has to be proved, not assumed.

What Must Be True

  1. There is a meaningful greedy first move.
  2. There always exists an optimal solution that starts with that greedy move — the greedy-choice property.
  3. After making that move, the remaining subproblem is itself optimally solvable — optimal substructure.

Good and Bad Examples

  • Fractional bag-filling by value density — greedy works, because a fraction of the best item is always available.
  • Rod cutting and VM hosting — greedy fails, because the locally best first cut can destroy global optimality.
  • Activity selection — greedy works, because the earliest-finishing choice leaves the most room for later ones.

Activity Selection

After sorting activities by finishing time, repeatedly choose the earliest-finishing compatible activity. The goal is maximum cardinality, not maximum occupied time — a common misreading that produces a different and wrong algorithm.

Marks are lost here. Stating that a greedy algorithm works is not an argument. Exchange the first move of an arbitrary optimal solution for the greedy one and show the result is no worse — that is the proof the greedy-choice property demands.
  • Requires the greedy-choice property (some optimal solution starts with the greedy move) and optimal substructure.
  • Works: fractional bag-filling by density, activity selection by earliest finish time.
  • Fails: rod cutting — the best first cut can destroy global optimality.
  • Activity selection maximises the number of activities, not the time occupied.
  • Prove it by exchange: swap an optimal solution's first move for the greedy one and show it is no worse.
Multiple choice · 2 questions

The greedy-choice property states that:

The claim is existential, not universal — which is exactly what makes the exchange argument work: take any optimal solution and swap its first move for the greedy one without making it worse.

Activity selection chooses the earliest-finishing compatible activity in order to maximise:

The objective is cardinality. Maximising occupied time is a different problem, and the earliest-finish rule does not solve it — a single long activity can beat several short ones on time while losing on count.
Conceptual · self-assessed

Rod cutting has optimal substructure but greedy fails on it, while fractional bag-filling succeeds. What structural difference decides this?

In the fractional problem you can take *part* of an item, so committing to the highest-density item never forecloses anything: whatever capacity remains can still be filled by the same rule. Rod cutting is discrete — choosing the highest value-per-length first cut removes a specific length permanently, and the remainder may not decompose well, so the locally best move can rule out the global optimum. The general test is whether the greedy move constrains the remaining subproblem in a way that could cost more than the move gained; divisibility is what removes that risk here. Note that optimal substructure holds in both cases, which is exactly why it is not sufficient on its own.
Data Structures

Elementary Data Structures

unreviewed

Pointers

Pointers are runtime references to objects by base address. They are what make dynamic structures such as linked lists and trees possible. NIL denotes the absence of a target.

Stacks

LIFO structure supporting PUSH and POP. Arrays give bounded-capacity stacks easily; linked lists give unbounded ones cleanly.

Queues

FIFO structure, typically implemented with circular arrays or linked lists.

Linked Lists

  • Singly linked — cheaper, but deletion and backward context are awkward.
  • Doubly linked — easier deletion and bidirectional traversal, at the cost of an extra pointer per node.
  • Cyclic variants simplify end cases by removing NIL from the ends.

Rooted Trees

Nodes may carry parent pointers, fixed or variable arity, and child lists. The choice of representation changes the complexity of the operations, which is the general lesson: representation is a performance decision, not a formatting one.

  • Stack LIFO, queue FIFO; both do their core operations in O(1)O(1).
  • Singly linked is cheaper; doubly linked buys easy deletion and reverse traversal for one extra pointer; cyclic forms remove NIL end cases.
  • Representation choice — parent pointers, arity, child lists — is what sets the operation costs.

Binary Search Trees

unreviewed

A BST stores key and payload pairs so that every key in the left subtree is strictly smaller and every key in the right subtree is strictly larger.

Supported Operations

  • SEARCH, INSERT, DELETE.
  • MINIMUM, MAXIMUM.
  • PREDECESSOR, SUCCESSOR.

Performance

  • Expected cost on a random-shaped tree O(logn)O(\log n).
  • Worst case O(n)O(n), when the tree degenerates into a chain.

Key Structural Points

  • No duplicate keys, if you want the classical clean guarantees.
  • PREDECESSOR and SUCCESSOR need parent pointers in the textbook implementation.
  • Deletion is the subtle operation: the zero-child, one-child and two-child cases all differ, and the two-child case replaces the node with its successor.
Why balanced trees exist. A BST's average speed is worthless if an adversary — or simply inserting already-sorted data — can force chain-like behaviour. The average case is a statement about input distributions, not a guarantee.
  • BST property: left subtree strictly smaller, right subtree strictly larger.
  • O(logn)O(\log n) expected on random shape, O(n)O(n) worst case when it degenerates to a chain.
  • Deletion has three cases — zero, one and two children; the two-child case promotes the successor.
  • PREDECESSOR and SUCCESSOR need parent pointers in the textbook form.
  • Sorted insertion order is the degenerate case, which is the whole motivation for balancing.
Multiple choice · 2 questions

Inserting already-sorted data into an unbalanced BST produces:

Each key is larger than everything before it, so it goes to the right of the previous one — a linked list with extra pointers. This ordinary and very common input is the whole motivation for balanced trees.

Deleting a node with two children is handled by:

The successor is the minimum of the right subtree, so it has at most one child and is the smallest key that can legally sit at that position — which is what preserves the BST property.
Conceptual · self-assessed

A BST has O(log n) expected cost. Explain why that is a weaker claim than a red-black tree's O(log n) worst case, and when the difference matters in practice.

The expected bound is over a distribution of insertion orders, assuming random shape. Real inputs are frequently not random — sorted, reverse-sorted or clustered data all arise naturally — and nothing prevents a specific run from being the bad case. The RB bound holds for every input. It matters wherever an adversary can choose the input (anything network-facing), wherever data arrives in sorted order, and wherever a latency guarantee rather than an average is required. A strong answer notes that the two structures do the same operations at the same asymptotic cost, and what is really being bought is the removal of a dependence on input order.

Balanced Trees: B-Trees and Red-Black Trees

unreviewed

B-Trees

Designed for disk-based settings, where following a pointer can cost millions of CPU cycles. The goal is to minimise tree height, because height is the number of expensive accesses.

B-Tree of Minimum Degree T

  • Non-root internal nodes store between T1T-1 and 2T12T-1 keys.
  • A node with tt keys has t+1t+1 children.
  • All leaves sit at the same depth.
  • Separator keys partition the child key ranges.

Height is O(logTN)O(\log_T N), hence O(logN)O(\log N) search, insert and delete.

B-Tree Insert and Delete

  • Insert by splitting full nodes, ideally pre-emptively on the way down, so no second pass is needed.
  • Delete by redistributing from siblings or merging with them, preserving minimum and maximum occupancy.
  • The root is special: splitting it increases the height, and losing its last key reduces it. This is why B-trees stay perfectly balanced.

Red-Black Trees

RB trees are BSTs with colour constraints that enforce approximate balance.

Properties

  1. Each node is red or black.
  2. The root is black.
  3. Leaves are black sentinel nodes.
  4. A red node has black children — no two reds in a row.
  5. Every path from a node down to its descendant leaves contains the same number of black nodes.

Together these bound the height at 2log2(n+1)2\log_2(n+1), giving O(logn)O(\log n) worst-case operations. Properties 4 and 5 are the load-bearing pair: equal black-height fixes a floor, and no-two-reds stops any path exceeding twice it.

2-3-4 Tree Isomorphism

The slides connect RB trees explicitly to B-trees of minimum degree 2 — 2-3-4 trees. This is conceptually important: a red node encodes a key fused into its black parent's node, so an RB tree is a 2-3-4 tree drawn with binary links.

Rotations

Local restructuring that preserves the global key order, used inside the balancing logic for RB insert and delete.

  • B-trees minimise height because height counts disk accesses. Non-root nodes hold T1T-1 to 2T12T-1 keys, a node with tt keys has t+1t+1 children, all leaves at equal depth. Height O(logTN)O(\log_T N).
  • Insert by splitting full nodes on the way down; delete by redistributing or merging. Only root split and root collapse change the height.
  • Red-black — root black, leaves black sentinels, no red node has a red child, equal black-height on every downward path. Height 2log2(n+1)\le 2\log_2(n+1), so O(logn)O(\log n) worst case.
  • An RB tree is a 2-3-4 tree (B-tree of degree 2) in binary form: red nodes are keys fused into their black parent.
  • Rotations restructure locally while preserving key order.
Multiple choice · 3 questions

B-trees are designed to minimise tree height specifically because:

A pointer that leads off-disk can cost millions of CPU cycles. Wide nodes trade cheap in-node search for fewer expensive level transitions.

Which pair of red-black properties together bound the height at 2·log₂(n+1)?

Equal black-height sets a floor common to all paths; no-two-reds means red nodes can at most double any path beyond that floor. Neither alone constrains the height.

A red-black tree is isomorphic to which structure?

A red node encodes a key fused into its black parent's node. Read every black node together with its red children and you are reading a 2-3-4 node.
Conceptual · self-assessed

B-trees and red-black trees both give O(log n) worst-case operations. Explain why the course presents them as answers to different questions.

They optimise different cost models. B-trees assume the expensive operation is following a pointer to another node — a disk or page access — so they make nodes wide and the tree shallow, doing more comparisons per node in exchange for fewer levels. RB trees assume uniform memory access and optimise the number of comparisons and pointer updates, staying binary and keeping rebalancing to O(1) rotations. A strong answer states the cost model each assumes, and observes the isomorphism: an RB tree is a 2-3-4 tree whose nodes have been unpacked into binary form because packing buys nothing in RAM.

Priority Queues

unreviewed

A priority queue maintains a dynamic set and supports repeated access to the extreme-priority item.

Operations

  • MINIMUM and MAXIMUM
  • EXTRACT-MIN and EXTRACT-MAX
  • INSERT
  • DECREASE-KEY and INCREASE-KEY
ImplementationPeek at extremumOther core operations
Binary heapO(1)O(1)O(logn)O(\log n)
Red-black treeO(logn)O(\log n)O(logn)O(\log n)

The heap wins on peeking at the extremum and on constant factors. An RB tree may be preferable when richer dictionary-like operations — search by arbitrary key, ordered traversal, predecessor and successor — are needed alongside the queue behaviour.

  • Operations: peek, extract, insert, and increase or decrease key.
  • Heap — peek O(1)O(1), everything else O(logn)O(\log n). Better constants.
  • RB tree — all O(logn)O(\log n), but also gives search, ordered traversal, predecessor and successor.
  • Choose on what else you need, not on the queue operations alone.

Hash Tables

unreviewed

Hash tables implement the dictionary operations INSERT, SEARCH and DELETE in expected near-constant time, provided the hash function and load-factor management behave.

Hashing Issues

  • Range reduction — hash values usually need reducing modulo the table size.
  • Performance — real hash functions can be expensive to compute.
  • Collisions — unavoidable, by the pigeonhole principle.
  • Entropy and selectivity — similar or low-entropy keys can cluster badly.

Chaining

Table entries point to linked lists. With load factor α=n/m\alpha = n / m for mm slots, operations take expected O(1+α)O(1 + \alpha) time under simple uniform hashing.

Variants from the Slides

  • Append, or replace an existing binding.
  • Keep the lists sorted by key.
  • Push on the head with tombstone-style logical deletion.
  • Push on the head and delete from the list, giving stack-like behaviour per key.

Open Addressing

Store items directly in table slots and resolve collisions with a probe sequence.

Probe Schemes

Linear probing
Simple and cache-friendly, but suffers primary clustering — occupied runs grow and merge.
Quadratic probing
Avoids primary clustering; keys with the same initial slot still follow the same path, which is secondary clustering.
Double hashing
The best of the three: the step size itself depends on the key, so colliding keys diverge immediately.

Deletion in Open Addressing

You cannot simply clear a slot, because a later search would stop at the gap and miss items further along the probe sequence. A marker or tombstone is required.

Resizing

Manage both live keys and tombstones. Rehash into a larger, same-sized or smaller table depending on occupancy and marker count — a table full of tombstones needs rehashing even though it holds few keys.

“Expected O(1)O(1)” is not “always fast”. A poor hash function, costly equality tests, or bad load-factor control will dominate the actual running time. The constant hidden by O(1)O(1) is doing real work here.
  • Dictionary operations in expected O(1)O(1) — conditional on the hash function and the load factor, not free.
  • Chaining — expected O(1+α)O(1+\alpha) where α=n/m\alpha = n/m.
  • Open addressing — linear probing (primary clustering), quadratic probing (secondary clustering), double hashing (best; step depends on the key).
  • Deletion needs tombstones — clearing a slot breaks later probe sequences.
  • Resizing must account for tombstones as well as live keys.
Multiple choice · 3 questions

In open addressing, a deleted entry must be marked with a tombstone rather than cleared because:

An empty slot is the signal that a probe sequence has ended. Clearing a slot in the middle of one severs every item beyond it — they are still present but permanently unreachable.

Double hashing improves on quadratic probing because:

Quadratic probing removes primary clustering but every key hashing to the same slot still walks the identical path — secondary clustering. A key-dependent step size breaks that too.

With chaining and load factor α = n/m, the expected cost of a search is:

Constant work to hash and index, plus a walk along a chain of expected length α. It is only O(1) while α is held bounded — which is what resizing is for.
Conceptual · self-assessed

A hash table with a table twice the size of its live key count is performing badly. Give two distinct explanations consistent with that.

First, tombstones: after many deletions the table can be mostly markers, so probe sequences are long even though few keys are live — the fix is rehashing on tombstone count, not just on key count. Second, a poor hash function: if keys cluster into a few slots, average occupancy is irrelevant because the distribution, not the load factor, determines probe length; low-entropy or similar keys are the usual culprit. A strong answer might also raise expensive key comparison or hash computation, where the constant hidden inside O(1) is what actually dominates. The point is that 'expected O(1)' is conditional on assumptions that real data can violate.
Revision

Core Comparison Table

unreviewed
Method Worst case Average or expected Stable In-place
Insertion sortΘ(n2)\Theta(n^2)Θ(n2)\Theta(n^2)YesYes
Selection sortΘ(n2)\Theta(n^2)Θ(n2)\Theta(n^2)NoYes
Merge sortΘ(nlogn)\Theta(n \log n)Θ(nlogn)\Theta(n \log n)YesUsually no
HeapsortO(nlogn)O(n \log n)O(nlogn)O(n \log n)NoYes
QuicksortΘ(n2)\Theta(n^2)Θ(nlogn)\Theta(n \log n)NoYes
Quicksort, MoM pivotsΘ(nlogn)\Theta(n \log n)Θ(nlogn)\Theta(n \log n)NoYes
Counting sortΘ(n+k)\Theta(n+k)Θ(n+k)\Theta(n+k)YesNo
Radix sortΘ(d(n+k))\Theta(d(n+k))Θ(d(n+k))\Theta(d(n+k))If inner sort isNo
Bucket sortΘ(n2)\Theta(n^2)Θ(n)\Theta(n)Depends on handlingNo
Read the table by column, not by row. The interesting question in an exam is rarely “which is fastest” — it is which one keeps its guarantee, which one keeps equal keys in order, and which one fits in the memory you have.
  • Guaranteed Θ(nlogn)\Theta(n\log n): merge sort, heapsort, quicksort with MoM pivots.
  • Stable: insertion, merge, counting; radix if its inner sort is.
  • In-place: insertion, selection, heapsort, quicksort.
  • Both stable and guaranteed: merge sort — at the cost of memory.
  • Both in-place and guaranteed: heapsort — at the cost of stability.

Exam Use

unreviewed

What Strong Answers Usually Include

  • The right asymptotic bound, with best, worst and expected case clearly distinguished.
  • The invariant or structural property that makes the algorithm correct.
  • The recurrence, and the method used to solve it.
  • The data-structure property that enables the runtime — not just a list of operations.
  • The tradeoff: memory, stability, in-place behaviour, worst-case robustness, or constant factors.

Common Mistakes

  • Claiming BUILD-HEAP is Θ(nlogn)\Theta(n \log n) rather than Θ(n)\Theta(n).
  • Forgetting that any constant-ratio quicksort split is still Θ(nlogn)\Theta(n \log n).
  • Using DP where subproblems do not overlap, or greedy without proving the greedy-choice property.
  • Treating BST average-case behaviour as though it were a worst-case guarantee.
  • Forgetting tombstones in open-addressing deletion.
  • Calling a sort stable without checking its behaviour on equal keys.
The course is foundational: the best answers do not merely state the algorithm, they explain why its structure forces the runtime and why its invariant forces correctness.
  • Answer with: the bound (case-distinguished), the invariant, the recurrence and its method, the enabling structural property, and the tradeoff.
  • Traps: BUILD-HEAP is Θ(n)\Theta(n); constant-ratio quicksort splits are still Θ(nlogn)\Theta(n\log n); BST average case is not a guarantee; open addressing needs tombstones; check stability on equal keys.
  • State why the structure forces the runtime, not just what the runtime is.