Algorithms & Complexity
A Complete 10-Chapter Maths for Programmers Course
Table of Contents
- Why Algorithmic Complexity Matters for Programmers
- Big-O Notation: Formal Definition & Growth Rates
- Analyzing Loops: From Code to Big-O
- Big-Omega & Big-Theta: Best, Worst & Average Case
- Recursive Algorithms & Recurrence Relations
- Solving Recurrences: Substitution & the Master Theorem
- Common Complexity Classes in Practice: Searching & Sorting
- Space Complexity & Amortized Analysis
- Beyond Polynomial Time: Exponential Growth & a Taste of P vs. NP
- Capstone — Analyzing and Comparing Real Algorithms
Why Algorithmic Complexity Matters for Programmers
Algorithms & Complexity
Chapter 1 · Why Algorithmic Complexity Matters for Programmers
Two pieces of code can produce the exact same correct output and still be worlds apart in practice — one finishes instantly no matter how much data you throw at it, the other quietly grinds to a halt the moment real-world scale shows up. Complexity analysis is the math for predicting which one you've written, before it becomes a production incident.
What Complexity Analysis Actually Measures
Not raw seconds — that depends on hardware, language, and a dozen other details that change from machine to machine. Complexity analysis measures something more durable: how the number of operations (or the amount of memory) an algorithm needs grows as the input size grows. It's a statement about shape, not speed.
Five Concrete Connections to Code Already On This Site
| Complexity topic | Where it actually shows up |
|---|---|
| Big-O & growth rates (Ch.2) | Why a dictionary/set lookup is effectively instant (O(1)) while searching a plain list is not (O(n)) — a real, measurable performance difference in everyday code |
| Analyzing loops (Ch.3–4) | Spotting that an innocent-looking nested loop has quietly made a function O(n²) — the single most common accidental performance bug |
| Recursion & recurrences (Ch.5–6) | Understanding why one recursive solution explodes exponentially while a structurally similar one (like merge sort) stays efficient |
| Searching & sorting (Ch.7) | Knowing when sorting data first, then searching repeatedly, beats scanning linearly every single time |
| Real relevance | Technical Support's own perfdiag1 asks "why is this slow" from a diagnostic, after-the-fact angle; this course is the proactive, code-level version of that exact same question |
What This Course Won't Cover
A few genuinely related areas are deliberately left out, to keep this course focused specifically on the math of measuring efficiency:
- A full catalog of algorithms — this course teaches how to analyze any algorithm's complexity, not a comprehensive tour of every sorting/searching algorithm's own implementation details; a general Programming-subject course would be the place for that breadth
- Graph algorithms specifically — pathfinding, traversal, and network analysis are reserved for this subject's own future Graph Theory course
- Deep computational complexity theory — Chapter 9 gives P vs. NP a light, honest conceptual treatment, not a formal complexity-theory course
Watch These Numbers Diverge
The whole point of complexity classes is how differently they scale. A few common classes, at increasing input sizes:
| n | log₂n | n | n log₂n | n² | 2ⁿ |
|---|---|---|---|---|---|
| 5 | 2.3 | 5 | 11.6 | 25 | 32 |
| 10 | 3.3 | 10 | 33.2 | 100 | 1,024 |
| 20 | 4.3 | 20 | 86.4 | 400 | 1,048,576 |
By n = 20, the O(n²) column has grown 20× from its own n=5 value — and the O(2ⁿ) column has grown over 32,000× from its own n=5 value. Same starting point, wildly different destinies. Chapter 9 pushes this comparison even further.
Where This Course Is Headed
| Chapter | Topic |
|---|---|
| 2 | Big-O Notation: Formal Definition & Growth Rates |
| 3 | Analyzing Loops: From Code to Big-O |
| 4 | Big-Omega & Big-Theta: Best, Worst & Average Case |
| 5 | Recursive Algorithms & Recurrence Relations |
| 6 | Solving Recurrences: Substitution & the Master Theorem |
| 7 | Common Complexity Classes in Practice: Searching & Sorting |
| 8 | Space Complexity & Amortized Analysis |
| 9 | Beyond Polynomial Time: Exponential Growth & a Taste of P vs. NP |
| 10 | Capstone — Analyzing and Comparing Real Algorithms |
Hands-On Exercises
A sorted array has 100,000 elements. Compute the worst-case number of comparisons for a linear scan, and the worst-case number of comparisons for binary search (⌈log₂(n)⌉). Express the ratio between the two as a single number.
A colleague claims "complexity analysis doesn't matter — modern computers are fast enough that it's never worth thinking about." Using this chapter's own growth-rate table, explain why this claim breaks down specifically for O(n²) and O(2ⁿ) algorithms as input size grows, even on very fast hardware.
📄 View solutionFor each of the following, name which topic from this chapter's own five-connections table it most directly maps to, and explain the connection in one or two sentences: (a) a function that checks whether a username already exists by scanning a Python list of all existing usernames; (b) a function with two nested loops comparing every pair of items in a list; (c) a function that repeatedly halves a sorted list to find a target value.
📄 View solutionChapter 1 Quick Reference
- Complexity analysis measures how the number of operations grows with input size — a statement about shape, not raw seconds
- Linear search vs. binary search over 1,000,000 elements: 1,000,000 comparisons vs. 20 — a 50,000× difference from algorithm choice alone
- Five direct connections: dict/set O(1) lookups, spotting accidental O(n²) nested loops, why some recursion explodes and some doesn't, sort-then-search vs. repeated linear search, and Technical Support's own diagnostic mindset applied proactively
- Deliberately out of scope here: a full algorithms/data-structures catalog, graph algorithms (reserved for a future Graph Theory course), and deep complexity theory
- Different complexity classes diverge dramatically even at small input sizes — O(2ⁿ) overtakes O(n²) astonishingly fast
- Next chapter: Big-O notation — formal definition and growth rates
Big-O Notation: Formal Definition & Growth Rates
Algorithms & Complexity
Chapter 2 · Big-O Notation: Formal Definition & Growth Rates
Chapter 1 used Big-O informally — "O(n²)" as a label for "the work grows like the square of the input." This chapter makes that label precise, with a real formal definition, and builds the full ranked ladder of complexity classes it describes.
The Formal Definition
f(n) = O(g(n)) if there exist positive constants c and n₀ such that f(n) ≤ c·g(n) for all n ≥ n₀.
In plain terms: f(n) is O(g(n)) if, past some point (n₀), f(n) never grows faster than a constant multiple of g(n). Big-O describes an upper bound on growth — "grows no faster than" — ignoring exactly how much faster or slower below that bound the real behavior is, and ignoring everything that happens before n₀.
Verifying the Definition With Real Numbers
Claim: f(n) = 3n² + 5n + 100 is O(n²). Trying c = 4:
| n | f(n) = 3n²+5n+100 | 4n² | f(n) ≤ 4n²? |
|---|---|---|---|
| 11 | 518 | 484 | No |
| 12 | 592 | 576 | No |
| 13 | 672 | 676 | Yes |
| 14 | 758 | 784 | Yes |
With c = 4 and n₀ = 13, the definition holds for every n ≥ 13 — confirmed exactly at the boundary. f(n) = 3n² + 5n + 100 is genuinely, verifiably O(n²), not just "probably" or "roughly."
The Dominant-Term Rule
In practice, nobody hunts for c and n₀ by hand every time. The shortcut: drop every term except the fastest-growing one, and drop its constant coefficient too.
3n² + 5n + 100 simplifies to O(n²) — the n² term dominates as n grows; the 5n and 100 terms, and even the leading 3, become irrelevant to the growth shape.
Why this is valid — watch the ratio of the full expression to just n² as n grows:
| n | f(n) = 3n²+5n+100 | n² | f(n) / n² |
|---|---|---|---|
| 10 | 450 | 100 | 4.50 |
| 100 | 30,600 | 10,000 | 3.06 |
| 1,000 | 3,005,100 | 1,000,000 | 3.005 |
n grows, that ratio settles toward exactly 3 — the leading coefficient. It doesn't shrink to nothing (which would mean n² overstates the growth) or blow up (which would mean n² understates it) — it converges to a fixed multiple, which is precisely what "constant c" means in the formal definition above. The dominant-term shortcut isn't a hand-wave; it's the formal definition, worked out in advance for the general case.
The Ranked Ladder of Common Complexity Classes
| Class | Name | Real example |
|---|---|---|
| O(1) | Constant | Array index access, hash table lookup |
| O(log n) | Logarithmic | Binary search (Chapter 1's own example) |
| O(n) | Linear | A single pass over a list, linear search |
| O(n log n) | Linearithmic | Efficient sorting — merge sort, average-case quicksort |
| O(n²) | Quadratic | Nested loops, bubble sort |
| O(n³) | Cubic | Triple-nested loops, naive matrix multiplication |
| O(2ⁿ) | Exponential | Naive recursive Fibonacci, generating every subset |
| O(n!) | Factorial | Generating every permutation, brute-force traveling salesman |
n past some point, so the formal definition holds. But stating "O(n)" for a genuinely constant-time algorithm would be true yet uselessly loose. By strong convention, Big-O is always reported as the smallest (tightest) class that still validly bounds the function — Chapter 4's Big-Theta gives this convention a fully formal footing.
Big-O Verification in Code
Hands-On Exercises
Using this chapter's own dominant-term rule, simplify f(n) = 7n³ + 2n² + 50 to its Big-O class. State which terms were dropped and why.
Verify f(n) = 2n² + 3n + 10 is O(n²) using c = 3. Find the smallest integer n₀ for which f(n) ≤ 3n² holds for all n ≥ n₀, showing the boundary value where it first becomes true.
Rank the following complexity classes from slowest-growing to fastest-growing: O(n²), O(log n), O(1), O(2ⁿ), O(n log n), O(n). Briefly justify the placement of O(n log n) relative to its two neighbors in your ranking.
Chapter 2 Quick Reference
- Formal definition:
f(n)=O(g(n))iff(n) ≤ c·g(n)for some constantsc, n₀and alln ≥ n₀ - Dominant-term rule: keep only the fastest-growing term, drop its coefficient — valid because the ratio to that term converges to a constant, never zero or infinity
- Ranked classes: O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(n³) < O(2ⁿ) < O(n!)
- By convention, always report the tightest valid Big-O class, even though looser bounds are technically also true
- Next chapter: Analyzing loops — from code to Big-O
Analyzing Loops: From Code to Big-O
Algorithms & Complexity
Chapter 3 · Analyzing Loops: From Code to Big-O
Chapter 2 built the math; this chapter is where it gets applied directly to real code. The core technique is almost mechanical once you know the rules: count how many times each line of code runs, as a function of n, then simplify with Chapter 2's own dominant-term rule.
A Single Loop — O(n)
The loop body runs exactly n times, each iteration doing a fixed, constant amount of work. Total: O(n).
Sequential Loops — The Sum Rule
n times: total work is n + n = 2n, which the dominant-term rule collapses straight to O(n). Sequential blocks add; the slowest-growing block among them never changes the overall class, only its irrelevant constant.
Nested Loops (Independent Bounds) — The Product Rule
n times for each of the outer loop's n iterations: n × n = n² total. Nested blocks multiply. This is exactly Chapter 1's own "innocent-looking nested loop" — two ordinary for loops, quietly O(n²).
Triangular Loops (Dependent Bounds) — Still O(n²)
Here the inner bound depends on the outer loop variable — the total operation count is a sum: 0 + 1 + 2 + ... + (n−1). This is exactly the summation Discrete Mathematics Fundamentals Chapter 8 proved a closed form for: Σᵢ₌₀ⁿ⁻¹ i = n(n−1)/2.
n = 6: direct sum 0+1+2+3+4+5 = 15; formula 6×5/2 = 15 — exact match. And n(n−1)/2 simplifies (Chapter 2's own dominant-term rule) to O(n²) — the same class as the fully independent nested loop above, just with roughly half the actual operations.
Gotcha 1: A Constant Inner Bound Is Not O(n²)
n × 10 = 10n. Since 10 is a fixed constant, entirely unrelated to n, this simplifies to O(n), not O(n²) — despite the visible nesting. The product rule only produces a genuinely higher complexity class when both bounds actually grow with n.
Gotcha 2: A Multiplicative Loop Variable Gives O(log n)
Because i doubles each iteration rather than simply incrementing, the loop runs far fewer than n times. For n = 1,000,000: this loop runs exactly 20 times before i reaches or exceeds n — matching log₂(1,000,000) ≈ 19.93, and matching Chapter 1's own binary search figure exactly. Whenever a loop variable grows multiplicatively, the complexity is O(log n), not O(n).
Loop Analysis in Code
Hands-On Exercises
A function runs one loop over n items, then (not nested — sequentially afterward) a second loop over a separate, fixed-size list of exactly 50 items, then a third loop over the same n items again. Using this chapter's own sum rule, determine the overall Big-O class, and explain why the 50-item loop doesn't change the final answer.
For a triangular loop identical in structure to this chapter's own example, compute the exact total operation count at n = 10 both by direct summation and by the closed-form formula n(n−1)/2, confirming they match. State the resulting Big-O class.
A loop starts with i = 1 and triples i (i *= 3) each iteration until i ≥ n. State the Big-O class of this loop, and compute the exact number of iterations it takes to reach or exceed n = 1,000,000.
Chapter 3 Quick Reference
- Method: count operations per line as a function of n, sum them up, simplify with Chapter 2's dominant-term rule
- Sum rule: sequential (non-nested) blocks add their complexities — the slowest-growing one wins
- Product rule: nested blocks multiply their complexities —
n × n = n²for independent bounds - Triangular loops (dependent inner bound) use the summation formula
Σi = n(n−1)/2— still O(n²), just with about half the operations - Gotcha 1: a nested loop with a constant (non-n-dependent) bound stays O(n), not O(n²)
- Gotcha 2: a loop variable that grows multiplicatively (doubling, tripling) gives O(log n), not O(n)
- Next chapter: Big-Omega and Big-Theta — best, worst, and average case
Big-Omega & Big-Theta: Best, Worst & Average Case
Algorithms & Complexity
Chapter 4 · Big-Omega & Big-Theta: Best, Worst & Average Case
Chapter 2 gave Big-O an upper bound and a promise: "Chapter 4's Big-Theta gives this convention a fully formal footing." Here it is — the other two asymptotic bounds, and a second, genuinely different axis this chapter untangles from them: best, worst, and average case.
Big-Omega — The Lower Bound
f(n) = Ω(g(n)) if there exist positive constants c and n₀ such that f(n) ≥ c·g(n) for all n ≥ n₀.
Where Big-O says "grows no faster than," Big-Omega says "grows no slower than" — a floor under the growth, instead of a ceiling.
Big-Theta — The Tight Bound
f(n) = Θ(g(n)) if f(n) = O(g(n)) and f(n) = Ω(g(n)) — both bounds hold at once, pinning the growth rate down exactly.
This is the formal version of Chapter 2's own convention — "always report the tightest valid bound" really means: whenever possible, report the Θ class, since it's the one that's both an upper and lower bound simultaneously.
A Fully Worked Θ Proof
Reusing Chapter 2's own function, f(n) = 3n² + 5n + 100, already shown to be O(n²) with c=4, n₀=13:
| Bound | Constants | Why it holds |
|---|---|---|
| O(n²) | c=4, n₀=13 | Verified in Chapter 2 — f(n) ≤ 4n² for all n ≥ 13 |
| Ω(n²) | c=3, n₀=1 | 3n² + 5n + 100 ≥ 3n² since 5n + 100 is always positive — trivially true for every n ≥ 1 |
Both bounds hold with real, verified constants — so f(n) = Θ(n²), not just informally "roughly n²." Interestingly, the lower bound here was the easier one to prove.
The Other Axis: Best, Worst & Average Case
Worked Example: Linear Search's Three Cases
| Case | Scenario | Complexity |
|---|---|---|
| Best | Target is the very first element checked | Θ(1) |
| Worst | Target is the last element, or absent entirely | Θ(n) |
| Average | Target equally likely at any of the n positions | Θ(n) |
For the average case, if the target sits at position k (1-indexed) with equal probability 1/n for each position, the expected number of comparisons is (1+2+...+n)/n = (n+1)/2 — verified directly at n=5: (1+2+3+4+5)/5 = 3, matching (5+1)/2 = 3 exactly.
(n+1)/2 looks like "half the work" of the worst case, and in a literal sense it is — but Chapter 2's own dominant-term rule discards constant factors entirely. Both the worst case and the average case land in the exact same Θ(n) class; the factor-of-2 difference is real and worth knowing, but it's invisible to Big-notation by design.
Why Worst Case Matters Even When It's Rare
Verifying Θ in Code
Hands-On Exercises
Inserting a new element into a sorted array (shifting elements to make room) has different complexities depending on where the new element belongs. State the best case (insert at the very end, no shifting needed) and worst case (insert at the very beginning, shifting every existing element) in Θ-notation, and explain why they differ.
📄 View solutionProve f(n) = 2n + 7 is Θ(n) by finding valid constants for both the O(n) bound and the Ω(n) bound, showing the smallest n₀ for which the O(n) bound (with c=3) first holds.
A colleague says "our hash table lookups are O(1), so performance is never a concern here." Using this chapter's own findings about best/worst/average case and the hash-collision example, explain what's missing from this claim.
📄 View solutionChapter 4 Quick Reference
- Big-Omega (Ω): lower bound —
f(n) ≥ c·g(n)for constants c, n₀ - Big-Theta (Θ): tight bound — both O and Ω hold simultaneously; the formal version of "report the tightest bound"
- O/Ω/Θ (bound type) and best/worst/average case (which input scenario) are orthogonal — never conflate them
- Linear search: Θ(1) best case, Θ(n) worst case, Θ(n) average case (even though average is only
(n+1)/2comparisons — constants vanish in Big-notation) - Worst-case bounds matter even when rare — they describe what an adversary or unlucky data can force, not just typical behavior (hash-collision DoS as a real example)
- Next chapter: Recursive algorithms and recurrence relations
Recursive Algorithms & Recurrence Relations
Algorithms & Complexity
Chapter 5 · Recursive Algorithms & Recurrence Relations
Chapters 3–4 analyzed code that repeats through loops. Recursive code repeats by calling itself — and analyzing its complexity needs a genuinely different tool: a recurrence relation, an equation defining a function's cost in terms of its own cost at smaller inputs.
From Code to Recurrence
T(n) = [number of recursive calls] × T([size of each subproblem]) + [non-recursive work per call], with a base case like T(0) = O(1) to anchor it.
Example 1: Linear Recursion
One recursive call, on an input one smaller, plus O(1) work: T(n) = T(n−1) + c, T(0) = c.
Unrolling: T(n) = T(n−1)+c = T(n−2)+2c = ... = T(0)+nc = c + nc = c(n+1) — O(n). Verified directly: countdown(10) makes exactly 11 calls (n=10 down through n=0) — matching n+1.
Proving the Solution — Exactly Discrete Mathematics Fundamentals Chapter 8's Own Technique
Claim: T(n) = c(n+1) for all n ≥ 0. This is proven by induction — the same base-case-plus-inductive-step structure that course used to prove a recursive function correct, now applied to prove a recursive function's cost:
| Step | Working |
|---|---|
| Base case | T(0) = c(0+1) = c — matches the definition exactly |
| Inductive hypothesis | Assume T(k) = c(k+1) for some k ≥ 0 |
| Inductive step | T(k+1) = T(k) + c = c(k+1) + c = c(k+2) = c((k+1)+1) — matches the formula at n=k+1 |
Both steps hold, so T(n) = c(n+1) is proven for every n — not just checked on a few examples.
Example 2: Naive Fibonacci — Exponential Blowup
Two recursive calls, each on a slightly smaller input, plus O(1) work: T(n) = T(n−1) + T(n−2) + c.
T(n−2) < T(n−1), replacing both terms with the larger one gives T(n) ≤ 2T(n−1) + c, which unrolls the same way as Example 1's pattern to O(2ⁿ). This is a valid, easily-derived upper bound — the actual tight bound is Θ(φⁿ) where φ ≈ 1.618 (the golden ratio), genuinely smaller than 2ⁿ but harder to derive by hand. O(2ⁿ) is honest and sufficient for this course's own scope; it just isn't the tightest possible statement.
Real call counts confirm the exponential shape, even below the loose 2ⁿ ceiling:
| n | Actual calls | 2ⁿ (upper bound) |
|---|---|---|
| 10 | 177 | 1,024 |
| 15 | 1,973 | 32,768 |
| 20 | 21,891 | 1,048,576 |
Example 3: Divide-and-Conquer — A Forward Reference
Recurrences in Code
Hands-On Exercises
A recursive power function computes xⁿ as x * power(x, n-1), with base case power(x, 0) = 1. Write its recurrence relation, then solve it by unrolling, following this chapter's own Example 1 method exactly.
A recursive function makes 3 recursive calls, each on a third (n/3) of the original input, plus O(n) work to combine the results. Write this function's recurrence relation, following the exact shape of this chapter's own merge sort example.
A recurrence is defined as T(n) = T(n−1) + 2, T(0) = 5. Using this chapter's own induction method, prove that T(n) = 2n + 5 for all n ≥ 0, showing the base case and the full inductive step.
Chapter 5 Quick Reference
- Recurrence relation:
T(n) = [calls] × T([subproblem size]) + [non-recursive work], anchored by a base case - Linear recursion:
T(n) = T(n−1) + cunrolls toO(n)— provable exactly by Discrete Mathematics Fundamentals Chapter 8's own induction technique - Naive double recursion (like Fibonacci):
T(n) = T(n−1) + T(n−2) + c— a quick, honest upper bound isO(2ⁿ), though the tight boundΘ(φⁿ)is smaller - Divide-and-conquer:
T(n) = 2T(n/2) + O(n)(merge sort's own shape) — Chapter 6's Master Theorem solves this pattern directly - Next chapter: Solving recurrences — substitution and the Master Theorem
Solving Recurrences: Substitution & the Master Theorem
Algorithms & Complexity
Chapter 6 · Solving Recurrences: Substitution & the Master Theorem
Chapter 5 set up merge sort's own recurrence, T(n) = 2T(n/2) + O(n), and left it unsolved. This chapter solves it two ways: the slow, honest way (full substitution), and the fast way every divide-and-conquer recurrence of this shape actually gets solved in practice — the Master Theorem.
Solving by Substitution — Merge Sort's Recurrence, Fully Unrolled
Starting from T(n) = 2T(n/2) + cn, repeatedly substituting the recurrence into itself:
| Step | Expression |
|---|---|
| 1 level | 2T(n/2) + cn |
| 2 levels | 4T(n/4) + 2cn |
| 3 levels | 8T(n/8) + 3cn |
| k levels | 2ᵏT(n/2ᵏ) + k·cn |
The pattern bottoms out when n/2ᵏ = 1, i.e. k = log₂n. Substituting: T(n) = 2^(log₂n)·T(1) + (log₂n)·cn = n·T(1) + cn·log₂n — Θ(n log n).
Verified directly (c=1, T(1)=1):
| n | T(n) | n + n·log₂n |
|---|---|---|
| 4 | 12 | 12.0 |
| 8 | 32 | 32.0 |
| 16 | 80 | 80.0 |
The Master Theorem — A Fast-Path Formula
For any recurrence of the shape T(n) = aT(n/b) + f(n), compare f(n) against n^(log_b a):
| Case | Condition | Result |
|---|---|---|
| 1 | f(n) grows slower than n^(log_b a) | T(n) = Θ(n^(log_b a)) — the recursion dominates |
| 2 | f(n) grows at the same rate as n^(log_b a) | T(n) = Θ(n^(log_b a) · log n) — perfectly balanced |
| 3 | f(n) grows faster than n^(log_b a) | T(n) = Θ(f(n)) — the non-recursive work dominates |
Applying It: Merge Sort, Confirmed Instantly
T(n) = 2T(n/2) + cn: a=2, b=2, f(n) = Θ(n). n^(log_b a) = n^(log₂2) = n¹ = n. Since f(n) = Θ(n) matches n^(log_b a) = n exactly — Case 2: T(n) = Θ(n · log n). Matches the full substitution above exactly, without needing to unroll anything.
Applying It: Binary Search, Confirmed Against Chapter 1
T(n) = T(n/2) + O(1): a=1, b=2, f(n) = Θ(1) = Θ(n⁰). n^(log_b a) = n^(log₂1) = n⁰ = 1. f(n) matches — Case 2: T(n) = Θ(n⁰ · log n) = Θ(log n). Exactly Chapter 1's own opening figure, now derived formally rather than just quoted.
Applying It: A Genuine Case 1 — When Recursion Dominates
T(n) = 4T(n/2) + n: a=4, b=2, f(n) = Θ(n). n^(log_b a) = n^(log₂4) = n². Since f(n) = n grows strictly slower than n² — Case 1: T(n) = Θ(n²), the recursive branching dominates entirely; the linear extra work barely matters.
T(n) directly and dividing by n²: at n=8, ratio ≈ 1.875; at n=32, ≈ 1.969; at n=64, ≈ 1.984 — steadily converging toward a constant, exactly Chapter 2's own signature of a correct Θ classification, not drifting toward 0 or infinity.
Solving Chapter 5's Own Unsolved Exercise
Chapter 5's Exercise 2 asked only to write T(n) = 3T(n/3) + O(n), promising the Master Theorem would solve it here. a=3, b=3, f(n) = Θ(n). n^(log_b a) = n^(log₃3) = n¹ = n. f(n) matches — Case 2: T(n) = Θ(n log n), the exact same class as merge sort, despite splitting into three pieces instead of two.
The Master Theorem in Code
Hands-On Exercises
For T(n) = 4T(n/2) + n, this chapter's own Case 1 example, verify by direct substitution/unrolling that T(8) = 120 (with T(1) = 1), and confirm this is consistent with the Master Theorem's Θ(n²) prediction (compare 120 to 8² = 64 and note the constant-factor gap is expected).
Apply the Master Theorem to T(n) = T(n/2) + n². Compute a, b, n^(log_b a), compare it to f(n) = n², identify which case applies, and state the resulting Θ class.
A recursive algorithm makes 8 recursive calls, each on a half-sized (n/2) subproblem, plus O(n²) non-recursive work. Write its recurrence, apply the Master Theorem, and state the resulting Θ class.
Chapter 6 Quick Reference
- Substitution: unroll
T(n) = 2T(n/2) + cnlevel by level until it bottoms out atk = log₂nlevels — givesΘ(n log n) - Master Theorem: for
T(n) = aT(n/b) + f(n), comparef(n)ton^(log_b a)— three cases, whichever grows faster (or ties) wins - Case 2 (tie) confirms both merge sort (
Θ(n log n)) and binary search (Θ(log n)) without unrolling by hand - Case 1 (recursion dominates) gives
Θ(n²)forT(n)=4T(n/2)+n, verified by a ratio converging to a constant - Chapter 5's own unresolved
3T(n/3)+O(n)resolves toΘ(n log n)— Case 2 again, just with a differentaandb - Next chapter: Common complexity classes in practice — searching and sorting
Common Complexity Classes in Practice: Searching & Sorting
Algorithms & Complexity
Chapter 7 · Common Complexity Classes in Practice: Searching & Sorting
Binary search and sorting have been referenced throughout this course — Chapter 1's opening comparison, Chapter 3's loop gotchas, Chapter 5's recurrence setup, Chapter 6's Master Theorem proof. This chapter finally gives them their own full treatment, with real measured numbers instead of just formulas.
Binary Search — A Real Trace
Searching a sorted 15-element array (values 1–15) for the target 12:
| Step | lo | hi | mid index | value at mid | Decision |
|---|---|---|---|---|---|
| 1 | 0 | 14 | 7 | 8 | 8 < 12 → search right half |
| 2 | 8 | 14 | 11 | 12 | Found |
Just 2 comparisons to find one element among 15 — each step throws away half the remaining candidates, exactly Θ(log n), formally confirmed by the Master Theorem in Chapter 6.
Sorting: O(n²) vs. O(n log n), Measured Directly
Bubble sort compares adjacent elements repeatedly — simple, but Chapter 3's own nested-loop pattern, giving O(n²). Merge sort (Chapters 5–6) is O(n log n). Rather than trust the formulas alone, here are actual comparison counts on random data:
| n | Bubble sort comparisons | Merge sort comparisons | Ratio |
|---|---|---|---|
| 10 | 45 | 24 | 1.9× |
| 100 | 4,950 | 534 | 9.3× |
| 1,000 | 499,500 | 8,708 | 57.4× |
Quicksort: An Honest Average-vs-Worst-Case Gap
Quicksort is, in typical practice, one of the fastest sorting algorithms available — O(n log n) average case. But Chapter 4's own best/worst/average distinction applies directly here:
O(n²), the exact same class as bubble sort. This happens, notably, on already-sorted or specifically-crafted adversarial input if the pivot selection is naive. Real implementations guard against this with randomized or median-of-three pivot selection specifically to make the worst case vanishingly unlikely in practice — but it remains a real, worth-knowing possibility, not a purely theoretical footnote.
How Low Can Sorting Go? The Comparison-Based Lower Bound
Merge sort's O(n log n) isn't just a good result — it's provably close to the best any comparison-based sort can ever achieve. There are n! possible orderings of n distinct elements (Discrete Mathematics Fundamentals' own permutations, Chapter 9 of that course), and each comparison can only rule out at most half the remaining possibilities. Distinguishing among n! orderings therefore needs at least log₂(n!) comparisons in the worst case.
log₂(n!) and n log₂n grow at the same rate — at n=1,000, log₂(1000!) ≈ 8,529 against 1000 × log₂1000 ≈ 9,966, a ratio of about 0.86 and climbing toward a stable constant as n grows. This means no comparison-based sorting algorithm can ever beat Θ(n log n) in the worst case — merge sort isn't just a good algorithm, it's asymptotically optimal among comparison-based approaches.
Searching & Sorting in Code
Hands-On Exercises
Trace binary search on the sorted array [3, 7, 11, 15, 19, 23, 27, 31] (8 elements) searching for the target 23. Show each step's lo, hi, mid, and decision, following this chapter's own trace format, and state the total number of comparisons needed.
Using this chapter's own measured bubble-sort-vs-merge-sort table, estimate roughly how large the comparison-count gap would be at n = 10,000 if the pattern of widening ratios continues (you don't need to run the code — reason from how the ratio grew between the three given data points). Explain your reasoning.
A teammate says "quicksort is always faster than merge sort since it's the industry-standard fast sort." Using this chapter's own quicksort caveat and Chapter 4's own best/worst-case distinction, explain what's missing from this claim, and describe one concrete situation where quicksort's real-world performance could disappoint.
📄 View solutionChapter 7 Quick Reference
- Binary search: Θ(log n), requires sorted input — a 15-element array searched in just 2 comparisons
- Bubble sort: O(n²); merge sort: O(n log n) — measured directly, the gap widened from 1.9× to 57.4× going from n=10 to n=1,000
- Quicksort: O(n log n) average case, but a genuine O(n²) worst case with poor pivot choices — real implementations guard against this, but it's not purely theoretical
- Comparison-based lower bound: Ω(n log n) — no comparison sort can ever beat this, since distinguishing n! orderings needs at least log₂(n!) comparisons, and log₂(n!) is itself Θ(n log n)
- Merge sort is therefore asymptotically optimal among comparison-based sorts, not just "pretty good"
- Next chapter: Space complexity and amortized analysis
Space Complexity & Amortized Analysis
Algorithms & Complexity
Chapter 8 · Space Complexity & Amortized Analysis
Every chapter so far measured time. This chapter measures the other resource that matters just as much in practice — memory — and then covers a genuinely different kind of question: what happens when a single operation is occasionally expensive, but only rarely?
Auxiliary Space vs. Input Space
Bubble sort (Chapter 7) sorts in place — a couple of temporary variables for swapping, regardless of array size: O(1) auxiliary space, even though it touches all O(n) of the input.
Merge Sort's Hidden Cost: O(n) Space
O(n) auxiliary space. Bubble sort's O(n²) time comes with only O(1) space; merge sort's much better O(n log n) time comes at the cost of genuine extra memory. Neither algorithm is simply "better" in every dimension — this is exactly the kind of tradeoff a real engineering decision has to weigh.
Recursive Call Stack Space
Every recursive call adds a frame to the call stack, and that frame occupies real memory until the call returns. Chapter 5's own countdown function makes n nested calls before hitting its base case — O(n) auxiliary space, purely from stacked call frames, even though it allocates nothing else.
fib(20) makes 21,891 total calls over its full execution, yet its call stack never exceeds depth 19. Time (total work done) and space (maximum simultaneous memory) are genuinely different measurements — a function can be catastrophically slow while still being memory-cheap.
Time and Space Side by Side
| Algorithm | Time | Space | Why |
|---|---|---|---|
| Bubble sort | O(n²) | O(1) | Sorts in place, a few temp variables |
| Merge sort | O(n log n) | O(n) | Temporary arrays needed for merging |
| Countdown (Ch.5) | O(n) | O(n) | Call stack depth grows with n |
| Naive Fibonacci (Ch.5) | O(2ⁿ) | O(n) | Exponential total calls, but only linear stack depth at once |
Amortized Analysis: The Dynamic Array
A dynamic array (like Python's own list) grows by doubling its capacity whenever it fills up — copying every existing element into a new, larger array. That single resize is O(n). But it doesn't happen on every append.
Simulating 16 appends, starting from capacity 1 and doubling each time it fills:
| Resize event | Elements copied |
|---|---|
| Capacity 1 → 2 | 1 |
| Capacity 2 → 4 | 2 |
| Capacity 4 → 8 | 4 |
| Capacity 8 → 16 | 8 |
Total copy cost: 1+2+4+8 = 15. Plus 16 individual element placements: 15 + 16 = 31 total operations for 16 appends — ≈1.94 operations per append, even though the single most expensive append (the one triggering the capacity-8-to-16 resize) cost 9 operations on its own.
n=100: 227 total operations, ≈2.27 per append. At n=1,000: 2,023 total operations, ≈2.02 per append. The per-append cost doesn't grow with n — it hovers around 2, confirming O(1) amortized time, even though any single append can, rarely, cost O(n).
Space & Amortized Analysis in Code
Hands-On Exercises
A function reverse_new(arr) builds and returns a brand-new list containing arr's elements in reverse order. A second function reverse_in_place(arr) swaps elements within the same array using two index pointers moving toward each other. State the auxiliary space complexity of each, and explain the difference using this chapter's own input-space-vs-auxiliary-space distinction.
Using this chapter's own dynamic-array-doubling method, compute the total operation count and the amortized cost per append for n = 32 appends (starting from capacity 1, doubling at each resize). Show each resize event's own copy cost.
Explain, in your own words, why naive recursive Fibonacci uses only O(n) auxiliary space despite making O(2ⁿ) total function calls over its full execution. Ground your answer in this chapter's own distinction between total work done (time) and maximum simultaneous memory in use (space).
📄 View solutionChapter 8 Quick Reference
- Auxiliary space (extra memory beyond the input) is what space complexity almost always measures — input space is usually not counted
- Recursive calls consume stack space proportional to recursion depth, not total calls made — naive Fibonacci is O(2ⁿ) time but only O(n) space
- Merge sort's O(n log n) time comes with a real O(n) space cost, versus bubble sort's O(1) space at O(n²) time — a genuine tradeoff, not a strict improvement
- Amortized analysis: the average cost per operation across a long sequence, even when individual operations vary wildly
- Dynamic array doubling: occasional O(n) resizes, but O(1) amortized time per append — verified directly (≈2 operations per append, staying constant as n grows)
- Next chapter: Beyond polynomial time — exponential growth and a taste of P vs. NP
Beyond Polynomial Time: Exponential Growth & a Taste of P vs. NP
Algorithms & Complexity
Chapter 9 · Beyond Polynomial Time: Exponential Growth & a Taste of P vs. NP
Chapter 1 showed O(2ⁿ) overtaking O(n²) by 32,000× at just n=20. This chapter puts real units on that divergence — actual seconds, days, years — and uses it to introduce the honest, practical boundary between problems that are efficiently solvable and problems nobody has ever found an efficient way to solve.
How Long, Really? Real Wall-Clock Estimates
Assuming a computer doing a generous 1 billion operations per second:
| n | O(2ⁿ) operations | Estimated time |
|---|---|---|
| 20 | ≈1.05 million | Under a millisecond |
| 30 | ≈1.07 billion | ≈1.1 seconds |
| 50 | ≈1.13 quadrillion | ≈13 days |
| 100 | ≈1.27 × 10³⁰ | ≈2,911× the age of the universe |
n=100 estimate down to "merely" about 3 million years — still catastrophically longer than any realistic patience. Exponential growth genuinely cannot be out-engineered with faster hardware; only a fundamentally different (polynomial) algorithm changes the picture at all, exactly Chapter 1's own "why choosing a better algorithm matters more than buying speed" point, now with real units attached.
Factorial growth is even more punishing:
| n | O(n!) operations | Estimated time |
|---|---|---|
| 10 | ≈3.6 million | Under a millisecond |
| 15 | ≈1.3 trillion | ≈22 minutes |
| 20 | ≈2.4 × 10¹⁸ | ≈77 years |
| 25 | ≈1.6 × 10²⁵ | ≈492 million years |
A brute-force approach to something like the traveling salesman problem — checking every possible route ordering, an O(n!) approach — becomes physically hopeless before the input even reaches 25 cities.
A Taste of P vs. NP
NP — problems where a proposed solution can be verified in polynomial time, even if nobody knows how to efficiently find one.
Every problem in P is automatically in NP too — if you can solve something quickly, you can obviously check a proposed answer just as quickly (solve it yourself and compare). The genuinely open question, one of the most famous unsolved problems in mathematics and computer science: is P = NP? Is every problem whose solution can be quickly checked also quickly solvable — or are some problems truly, unavoidably harder to solve than to verify? Nobody has ever proven it either way; it's a Millennium Prize Problem with a $1 million reward still unclaimed.
n! possible orderings is exactly the factorial blowup shown above.
Certain problems — called NP-complete — are, in a precise sense, the "hardest" problems in NP: if any NP-complete problem were ever found to have a polynomial-time solution, that would prove P = NP and mean every problem in NP could suddenly be solved efficiently too. Traveling salesman is a classic NP-complete problem.
The Practical Takeaway
n (per this chapter's own tables, that means staying well under 20–30 for factorial-shaped problems), use an approximation algorithm that finds a provably good-enough answer quickly, or use a heuristic that works well in practice without any formal guarantee. Recognizing "this looks NP-complete" is itself a useful, actionable signal.
Real Numbers in Code
Hands-On Exercises
Using this chapter's own 1-billion-operations-per-second assumption, estimate the running time of an O(2ⁿ) algorithm at n = 40. Express the result in a human-readable unit (seconds, minutes, hours, etc.), showing your calculation.
Using the same assumption, estimate the running time of an O(n!) algorithm at n = 15, expressed in a human-readable unit. Compare it to this chapter's own O(2ⁿ) estimate at a similar-magnitude operation count, and comment on which grows faster for a given increase in n.
Using a Sudoku puzzle as a concrete example, explain in your own words the difference between "solving" a problem and "verifying" a proposed solution to it, and why this distinction is exactly what defines the class NP.
📄 View solutionChapter 9 Quick Reference
- At 1 billion ops/sec, O(2ⁿ) reaches 2,911× the age of the universe by
n=100— no realistic hardware speedup rescues this - O(n!) is even more punishing — 20! alone takes ≈77 years at the same rate
- P: solvable in polynomial time. NP: a proposed solution verifiable in polynomial time. Every P problem is in NP; whether the reverse holds (P = NP) is a famous unsolved problem
- NP-complete problems are the "hardest" in NP — solving any one in polynomial time would solve all of NP efficiently
- Sudoku and the traveling salesman problem are accessible, concrete NP examples — easy to verify, hard (as far as anyone knows) to solve
- Practical takeaway: a suspected NP-complete problem calls for approximation or heuristics, not an endless search for a fast exact algorithm
- Next chapter: Capstone — analyzing and comparing real algorithms
Capstone — Analyzing and Comparing Real Algorithms
Algorithms & Complexity
Chapter 10 · Capstone — Analyzing and Comparing Real Algorithms
One continuous worked problem, touching every chapter of this course in the order a real engineer would actually reach for each idea: finding duplicate values in a list of n user IDs, solved three genuinely different ways.
A Full Worked Comparison — Finding Duplicate User IDs
The obvious first approach: check every pair of elements for a match. Before writing any code, Chapter 2's own dominant-term intuition already previews the shape — comparing every element against every other element is going to land in the O(n²) family, the same shape as any nested "compare all pairs" structure.
Avoiding double-counting each pair, the brute-force approach compares element i against every element j > i — exactly Chapter 3's own triangular loop pattern. Total comparisons: n(n−1)/2. On a real test list of n = 1,000 IDs (with 50 genuine duplicates planted in): 499,500 comparisons — matching 1000×999/2 exactly.
Best case: the very first two elements checked happen to be duplicates — found in a single comparison, Θ(1). Worst case: no duplicates exist at all, forcing every one of the n(n−1)/2 comparisons to run to completion — Θ(n²). The 499,500-comparison run above is the worst case, since it never gets to skip ahead once a match streak is found.
A divide-and-conquer idea: split the list in half, recursively find duplicates within each half, then separately handle duplicates that span across the two halves. In the shape Chapter 5 established: T(n) = 2T(n/2) + [cost of the cross-half check] — everything hinges on how cheaply that cross-half step can be done.
If each half is sorted before combining — exactly merge sort's own merge step — checking for duplicates spanning the boundary becomes a single O(n) linear pass. That gives T(n) = 2T(n/2) + O(n) — Chapter 6's own Master Theorem Case 2, resolved instantly: Θ(n log n), with no new derivation needed.
This resolves into a clean two-step algorithm: sort the list (O(n log n), using merge sort specifically — Chapter 7's own measured comparison counts showed it decisively beating bubble sort's O(n²) at any real scale), then a single linear scan checking each element against its neighbor for a match. On the same 1,000-ID test list: the scan itself takes just 999 comparisons, plus roughly 9,966 for the sort — both dramatically below brute force's 499,500.
A hash set offers a third option entirely: scan the list once, checking whether each element is already in a hash set before adding it — average-case O(1) per check (Chapter 4's own hash table discussion), giving O(n) total time. On the same test list: exactly 1,000 operations, one per element — even faster than sort-then-scan. But per Chapter 8's own auxiliary-space accounting, this approach needs a full O(n) hash set alongside the original list — a real memory cost the brute-force approach (needing only O(1) extra space) never pays.
No — and this is provable without any deep complexity theory. Any correct algorithm for this problem must examine every element at least once (an unexamined element could always be the one hiding a duplicate), so Ω(n) is an unavoidable floor for any approach. The hash-set method's O(n) is therefore asymptotically optimal — a genuinely different, tighter lower bound than Chapter 7's own Ω(n log n) for comparison-based sorting, since this problem doesn't require comparison-based sorting at all. And unlike Chapter 9's own traveling-salesman example, this problem sits comfortably in P — efficiently solvable, no NP-completeness in sight.
The Full Comparison, Side by Side
| Approach | Time | Space | Measured ops (n=1,000) |
|---|---|---|---|
| Brute force | Θ(n²) | O(1) | 499,500 |
| Sort then scan | Θ(n log n) | O(n)* | ≈10,965 |
| Hash set | O(n) average | O(n) | 1,000 |
*Using merge sort; an in-place O(n log n) sort would bring this down to O(1), a further tradeoff of its own.
Brute force to hash set: a 499.5× reduction in measured operations, for the exact same correct result — every chapter of this course, pointed at one real problem.
What This Course Doesn't Cover
In the interest of an honest accounting: a full catalog of every named algorithm and data structure, graph algorithms (reserved for this subject's own future Graph Theory course), and deep computational complexity theory beyond Chapter 9's light introduction were all named in Chapter 1 as deliberately out of scope. This course built the mathematical toolkit for analyzing any algorithm's efficiency, not an exhaustive tour of algorithms themselves.
This Course's Throughline, Restated
Where This Course Connects
This course is the direct mathematical foundation under every other course on this site that writes real code — Technical Support's own perfdiag1 diagnoses slowness after the fact; this course predicts it beforehand. Within this subject's own future courses, Graph Theory will build directly on this course's recursion and Master Theorem material for traversal and pathfinding algorithms, and Number Theory & Cryptographic Math will lean on this course's own complexity-class vocabulary to explain why certain cryptographic problems are chosen specifically for their computational hardness.
Hands-On Exercises
For a list of n = 200 elements with no duplicates at all, compute the exact number of brute-force comparisons (Step 2's own formula) and the exact number of hash-set operations (Step 7's own approach). Compute the ratio between them.
A teammate proposes a fourth approach: for each element, use binary search (Chapter 7) to check whether it already exists in a separately maintained sorted list, inserting it if not. Using this chapter's own space/time framework, analyze this approach's time complexity (consider the cost of both the binary search itself and of inserting into a sorted list — Chapter 4's own sorted-array insertion exercise is directly relevant) and compare it honestly to the hash-set approach.
📄 View solutionFor each of the eight steps in this chapter's own worked comparison, name the specific topic it relied on, without looking back at the step labels — just from the description of what each step actually does.
📄 View solutionChapter 10 Quick Reference
- Full worked comparison: growth-rate intuition (Ch.2) → precise loop counting (Ch.3) → best/worst case (Ch.4) → a recursive alternative (Ch.5) → the Master Theorem (Ch.6) → sort-then-scan formalized (Ch.7) → a hash-set tradeoff (Ch.8) → an inherent lower bound (Ch.9)
- Brute force (Θ(n²)) → sort-then-scan (Θ(n log n)) → hash set (O(n) average) — a measured 499.5× reduction in operations at n=1,000, all solving the identical problem correctly
- Time and space are genuinely separate axes — the fastest approach (hash set) isn't automatically the cheapest in memory
- Different problems have different inherent lower bounds — Ω(n) here, versus Ω(n log n) for comparison-based sorting — neither is a universal floor
- Out of scope: a full algorithms catalog, graph algorithms (a future course), and deep complexity theory beyond Chapter 9's light introduction
- Course complete — Algorithms & Complexity, 10 chapters, from Big-O to P vs. NP