Maths for Algorithms: Counting & Probability

By the end of this lesson you will be able to count arrangements and choices without listing them out (permutations, combinations, Pascal's triangle), reason correctly about chance with dice, coins and disease tests (sample spaces, conditional probability, Bayes' theorem), use linearity of expectation — the single most powerful trick in algorithm analysis — to predict how many hires a random-order interview process makes, and understand exactly why shuffling an array the "obvious" way is wrong while Fisher–Yates is provably fair.

1. Counting: rule of sum & rule of product

Imagine a vending machine. If it has a fruit shelf (3 kinds) and a separate drink shelf (2 kinds), and you're only buying one item total — either a fruit or a drink — there are 3+2 = 5 things you could walk away with. That's the rule of sum: when choices come from disjoint (non-overlapping) piles and you pick from exactly one pile, ADD the sizes. But if you're assembling an outfit — one shirt (3 kinds) AND one pair of pants (2 kinds), both together — there are 3×2 = 6 possible outfits, because every shirt can be paired with every pair of pants. That's the rule of product: when choices are made independently, one after another, and you need ALL of them, MULTIPLY the counts.

These two rules are the foundation of every counting formula in this lesson. Getting "and" vs "or" right — multiply vs add — is the single most common source of counting mistakes.

Rule of sum: |A ∪ B| = |A| + |B| when A and B don't overlap (choosing from ONE of several disjoint groups). Rule of product: |A × B| = |A| × |B| (choosing one item from EACH of several independent groups). Both generalize to any number of groups.

In Dart: the two counting rules on real lists: Set union of disjoint piles for "or" (3 + 2 = 5), a nested for over both piles for "and" (3 × 2 = 6), and three groups multiplied (3·2·4 = 24).

Input size → what's feasible: group sizes up to ~10⁷ combinations can be enumerated; beyond that just multiply the sizes (a 64-bit int holds products up to 9.2·10¹⁸).

2. Permutations: arranging things in order

A permutation is just "an ordering" — like lining up runners at the start of a race, or arranging books on a shelf. If you have n distinct people and want to line up ALL of them, think of it as filling n empty seats one at a time: any of the n people can sit in seat 1, then any of the remaining n−1 can sit in seat 2, and so on down to exactly 1 person left for the last seat. By the rule of product, the total number of orderings is n × (n−1) × (n−2) × … × 1, written n! ("n factorial").

What if you only want to arrange k of the n people (say, who gets gold/silver/bronze out of 5 runners)? Same logic, but you stop after k seats: n × (n−1) × … × (n−k+1), which is n! / (n−k)!. This is called a k-permutation.

In Dart: n! as a loop, every ordering generated by recursion (the "fill seat 1, then arrange the rest" argument) and counted for n = 0…7, the k-permutation n!/(n−k)! as a product of k terms, and the gold/silver/bronze count P(5,3) = 60 by brute force.

Input size → what's feasible: listing all orderings only for n ≤ 8–9 (9! = 362,880); counting them with n! works up to n = 20 in a 64-bit int (native Dart; in JavaScript-compiled web builds int is a double and loses exactness above 2⁵³).

Don't confuse "how many orderings" (permutations — order matters: gold ≠ silver) with "how many groups" (combinations, next section — order doesn't matter: a team of 3 is the same team no matter who you list first). Mixing these up is the #1 counting bug.

3. Combinations & Pascal's triangle

A combination is a k-permutation with the ordering thrown away. Picking "gold, silver, bronze" cares about order (3 different medal assignments per trio); picking "which 3 people make the relay team" does not — {Alice, Bob, Carol} is the same team as {Carol, Alice, Bob}. Since every unordered team of k people corresponds to exactly k! different orderings (permutations) of that same team, the number of k-combinations is the number of k-permutations divided by k!.

This gives the famous formula for "n choose k", written C(n,k) or ⁿCₖ:

In Dart: "n choose k" three ways: kPerm / k!, the factorial form n!/(k!(n−k)!), and by actually counting teams (every bitmask of n bits with exactly k ones is one team), plus symmetry and the "k! orderings per team" argument.

Input size → what's feasible: n ≤ 20 → factorials still fit a 64-bit int (21! overflows); n ≤ 60 → use the multiplicative form (Q6); beyond that → BigInt or work mod a prime.

Pascal's triangle is a beautifully simple way to build every C(n,k) using only addition: each entry is the sum of the two entries diagonally above it. Row n, position k is exactly C(n,k) — because when you choose k people from n, the last person is either IN (then choose the other k−1 from the remaining n−1: C(n−1,k−1) ways) or OUT (then choose all k from the remaining n−1: C(n−1,k) ways). This "either the last item is IN the group or it's OUT" split is Pascal's identity: C(n,k) = C(n−1,k−1) + C(n−1,k).

In Dart: Pascal's triangle built by addition only (nextRow): row n position k is C(n,k), Pascal's identity, the left-right symmetry and the row sum 2ⁿ are all checked for rows 1…30. Math notation and Dart both start the row index at 0 here, so no index shift is needed.

Input size → what's feasible: rows up to 60 fit a 64-bit int (C(60,30) ≈ 1.2·10¹⁷); a full table up to n = 2000 costs ≈ 2·10⁶ additions (use a modulus, Q12).

C(n,k) = C(n, n−k) (choosing who's IN is the same as choosing who's OUT) — Pascal's triangle is symmetric left-to-right. Every row sums to a power of 2 (see the next section for why).

4. The binomial theorem, strings & subsets

Pascal's triangle isn't just a counting curiosity — the same numbers are the coefficients when you expand (x+y)ⁿ:

Binomial theorem: (x+y)ⁿ = Σk=0n C(n,k) xᵏ yn−k. Why? Expanding (x+y)(x+y)…(x+y) (n copies) means picking "x" or "y" from each of the n factors and multiplying — exactly like flipping n coins. The number of ways to end up with exactly k copies of x (and n−k copies of y) is precisely C(n,k), the number of ways to choose WHICH k of the n factors contributed an x.

Setting x = y = 1 gives a nice special case: 2ⁿ = Σ C(n,k) — every row of Pascal's triangle sums to a power of 2. That's not a coincidence: it's also exactly the number of subsets of an n-element set, because every subset is decided by n independent yes/no choices ("is element i in the subset?") — a rule-of-product count of 2×2×…×2 (n times). Grouping subsets by their SIZE k gives C(n,k) subsets of size k, and summing over all sizes must give all 2ⁿ subsets.

A closely related idea: a string over an alphabet of size n and length k (e.g. a k-character password using n possible characters, with repeats allowed) has exactly n^k possibilities — rule of product applied k times, once per position.

Subsets of an n-set: 2ⁿ. k-strings over an n-letter alphabet: nᵏ. k-permutations (no repeats, order matters): n!/(n−k)!. k-combinations (no repeats, order doesn't matter): C(n,k) = n!/(k!(n−k)!). Same numbers n and k, four different questions, four different answers — always ask "does order matter?" and "can things repeat?" first.

In Dart: the binomial theorem expanded term by term and compared with (x+y)ⁿ for several (x, y, n); the subsets of a 5-set via bitmasks, counted by size (1,5,10,10,5,1) and in total 2⁵ = 32; and the strings count nᵏ by building all 3⁴ strings.

Input size → what's feasible: subsets by bitmask → n ≤ 25 (2²⁵ ≈ 3·10⁷ masks); strings over 62 symbols, length 8 = 2.2·10¹⁴ → count with ipow, never enumerate.

5. Sample space, events & the probability axioms

The sample space S is the list of every possible outcome of a random experiment — like listing every face a die could land on. An event is just some subset of those outcomes you care about, e.g. "the die shows an even number" = {2,4,6}. A uniform distribution means every individual outcome is equally likely — a fair die, a fair coin.

The probability axioms are almost embarrassingly simple, yet everything else is built from them: (1) probabilities are never negative, (2) the whole sample space has probability 1 (something always happens), (3) for outcomes that can't happen together (mutually exclusive), probabilities add. For a uniform sample space, Pr{event} = (number of outcomes in the event) / (total number of outcomes) — which is exactly why the counting techniques above (Sections 1–4) are the engine that powers probability calculations.

Rolling two fair dice: the sample space has 6×6 = 36 equally likely outcomes (rule of product!). Pr{sum = 7} = 6/36 = 1/6, because exactly 6 of the 36 pairs sum to 7: (1,6),(2,5),(3,4),(4,3),(5,2),(6,1).

In Dart: the sample space of two dice as a list of records, events as predicates, Pr = count / 36, and the three axioms checked on it (non-negative, Pr{S} = 1, additive for the mutually exclusive events "sum = 7" and "sum = 11").

Input size → what's feasible: 36 outcomes → exact integer counts; counts stay integers so no rounding until the final division.

6. Complement, union & inclusion–exclusion

The complement of event A (written Ā, "not A") is everything in the sample space that ISN'T in A. Since S is split entirely between A and Ā, their probabilities must add to 1: Pr{Ā} = 1 − Pr{A}. This is often the easiest way to compute a probability — "at least one" questions are usually much easier to answer as "1 − none".

In Dart: "at least one six in 4 rolls" by enumerating all 6⁴ = 1296 outcomes (671 have a six, 625 do not) and by 1 − (5/6)⁴.

Input size → what's feasible: up to ~10⁷ outcomes can be enumerated; "1 − none" needs only a power, so it scales to any number of rolls.

For two events that CAN overlap, you can't just add their probabilities — that would double-count the overlap. The fix is inclusion–exclusion:

// Inclusion-exclusion for two events:
Pr(A union B) == Pr(A) + Pr(B) - Pr(A intersect B)

In Dart: A = "sum is even", B = "sum is above 7" on the 36 two-dice outcomes, built as Dart Sets: union, intersection and the identity |A ∪ B| = |A| + |B| − |A ∩ B|, plus the mutually exclusive special case.

Input size → what's feasible: sample space of 36 → enumerate and count; with 10⁶ outcomes you would still just count with sets (O(size)).

Forgetting to subtract the overlap is the classic mistake: Pr{A} + Pr{B} always OVER-counts Pr{A∪B} unless A and B are mutually exclusive (Pr{A∩B}=0), in which case inclusion–exclusion correctly reduces to the plain rule of sum from Section 1.

7. Conditional probability & independence

Conditional probability answers: "given that I already know B happened, how likely is A now?" Once you know B happened, the sample space effectively SHRINKS to just the outcomes inside B — so you divide by Pr{B} instead of by the whole sample space.
Pr(A given B) == Pr(A intersect B) / Pr(B)     // requires Pr(B) != 0

In Dart: conditioning as "shrink the sample space to B": the two-coin and two-dice examples computed by the definition Pr{A∩B}/Pr{B} and by dividing counts directly.

Input size → what's feasible: a sample space up to ~10⁷ outcomes can be enumerated in a second; for larger spaces use the counting formulas.

Two events are independent when knowing one happened tells you NOTHING about the other: Pr{A∩B} = Pr{A}·Pr{B}, equivalently Pr{A|B} = Pr{A}. A subtle trap: it's possible for three events to be pairwise independent (every PAIR multiplies correctly) while still not being mutually independent (the probability of all three together is wrong). Flip two fair coins and define A₁ = "first is heads", A₂ = "second is heads", A₃ = "the two coins differ". Every pair of these multiplies correctly (each pair has probability 1/4 = 1/2 × 1/2), but A₁, A₂ and A₃ can never ALL be true simultaneously (both heads means they can't differ) — so Pr{A₁∩A₂∩A₃} = 0, not the 1/8 that mutual independence would require.

In Dart: independence as Pr{A∩B} == Pr{A}·Pr{B}: the three coin events A₁, A₂, A₃ are independent in every pair (1/4 each) but the triple has probability 0, not 1/8; and "first die even" / "second die even" on 36 outcomes.

Input size → what's feasible: 4 outcomes (coins) and 36 outcomes (dice) → exact; the same test works on any finite space you can list.

Pairwise independence does NOT imply mutual independence. Algorithms that rely on independence (randomized quicksort's pivot choices, hash functions) usually need to be explicit about which kind they actually have.

8. Bayes' theorem: Monty Hall & the medical test

Bayes' theorem lets you FLIP a conditional probability around: if you know Pr{B|A}, it tells you how to get Pr{A|B}. It's derived from nothing more than the definition of conditional probability applied twice: Pr{A∩B} = Pr{A|B}·Pr{B} = Pr{B|A}·Pr{A}, so dividing through gives:
Pr(A given B) == Pr(A) * Pr(B given A) / Pr(B)

In Dart: Bayes' theorem derived on real events (A = "dice sum to 8", B = "first die is 6"): the two products Pr{A|B}·Pr{B} and Pr{B|A}·Pr{A} are both Pr{A∩B}, so dividing gives the theorem.

Input size → what's feasible: 36 outcomes → exact counts; probabilities are doubles, so compare with a tolerance.

The Monty Hall problem: there's a car behind one of 3 doors, goats behind the other two. You pick a door. The host — who KNOWS where the car is — opens a different door, always revealing a goat. Should you switch to the remaining door?

Exact reasoning without Bayes' formula: your first pick has a 1/3 chance of being the car and a 2/3 chance of being a goat — that never changes no matter what the host does afterward, because the host's action doesn't affect where the car actually is. If your first pick WAS the car (1/3 of the time), switching loses. If your first pick was a goat (2/3 of the time), the host is forced to reveal the OTHER goat, so the remaining unopened door must hold the car — switching wins. So switching wins with probability 2/3, staying wins with probability 1/3 — switching DOUBLES your odds.

In Dart: Monty Hall three ways: all 9 (car, pick) cases (switching wins 6, staying 3), Bayes with the likelihoods 1/2, 1, 0, and a seeded 20,000-game simulation.

Input size → what's feasible: 9 exact cases; simulation error ≈ 0.47/√trials → 20,000 games give ±0.003.

Now a case where Bayes' theorem is genuinely needed: a disease affects 1% of a population. A test correctly detects it 99% of the time (sensitivity) when someone has it, and correctly comes back negative 95% of the time (specificity) when someone doesn't. If your test comes back POSITIVE, what's the chance you actually have the disease?

Intuition screams "99% accurate test, so ~99% chance I'm sick" — that's wrong, and it's one of the most consequential statistics mistakes in the real world (medicine, spam filters, fraud detection). Because the disease is rare, the (small) false-positive rate applied to the (huge) healthy population produces more false alarms than true detections. The actual answer, worked out with Bayes' theorem above, is only about 1/6 ≈ 16.7%.

In Dart: the rare-disease test twice: with Bayes and the total-probability denominator (Pr{sick | positive} = 1/6), and by counting a town of 10,000 people (99 true alarms against 495 false ones).

Input size → what's feasible: exact with 4 numbers; the town is just integer arithmetic (use ~/ so nothing becomes a float).

9. Random variables, expectation & linearity

A random variable X is just a rule that assigns a number to every outcome — "the value showing on the die", "the number of heads in 10 flips". Its expectation E[X] is the long-run average value if you repeated the experiment forever: multiply every possible value by its probability and add them up.
E[X] == sum over every possible value x of ( x * Pr(X == x) )

In Dart: E[X] = Σ x·Pr{X = x} as a loop over the faces (and over the 36 outcomes of two dice), including a value→count table.

Input size → what's feasible: finite sample spaces up to ~10⁷ outcomes are fine to enumerate; otherwise use linearity (next).

Here is the single most useful fact in this entire lesson for analysing algorithms:

Linearity of expectation: E[X + Y] = E[X] + E[Y] — ALWAYS, even if X and Y are dependent, even if they influence each other in complicated ways. You never need independence to add expectations. This is the superpower: instead of reasoning about one complicated random quantity, break it into many simple pieces (usually 0/1 indicator variables), find each piece's easy expectation, and just add.

In Dart: E[X+Y] = E[X]+E[Y] as a function e(f) averaging over all 36 outcomes: independent dice, a dependent Y (the running total), Y = X², and E[3X] = 3E[X] — no independence used anywhere.

Input size → what's feasible: 36 outcomes → exact (use a tolerance for double averages); the point is that you never need the joint distribution in practice.

10. Indicator random variables: hiring the best so far

An indicator random variable XA for an event A is 1 if A happens and 0 otherwise. Its expectation is beautifully simple: E[XA] = 1·Pr{A} + 0·Pr{Ā} = Pr{A}. So "the expected number of times something happens" always reduces to "sum up the probability of it happening at each opportunity" — no matter how tangled the dependencies between opportunities are, thanks to linearity.

In Dart: an indicator variable as a 0/1 list: E[X_A] equals Pr{A} for "the die shows 5 or 6" (1/3), and the number of sixes in 5 rolls (a sum of 5 indicators) averages 5·(1/6) over all 6⁵ = 7776 outcomes.

Input size → what's feasible: 7776 outcomes → exact enumeration; for 100 rolls you use linearity (100/6) instead of enumerating 6¹⁰⁰.

The always-hire-the-best-so-far problem: you interview n candidates one at a time, in a random order, always hiring whoever is the best so far (replacing your current hire). How many times do you expect to hire, total? Let Xi = 1 if candidate i is a "record" (better than everyone interviewed before them). Candidate i is a record exactly when the best of the first i candidates (a uniformly random subset in random order) happens to BE candidate i — which, by symmetry, happens with probability exactly 1/i. By linearity:

E[number of hires] == sum(i = 1..n) of Pr(candidate i is a record)
                    == sum(i = 1..n) of (1 / i)
                    == H(n)   // the n-th harmonic number

In Dart: E[hires] = Σ 1/i = Hₙ checked exactly: all 720 arrival orders of 6 candidates are generated, the number of hires is counted for each, and the average is exactly 49/20 = H₆ = 2.45; then H₁₀₀ ≈ 5.19.

Input size → what's feasible: all n! orders only for n ≤ 8 (8! = 40,320); for n = 100…10⁶ use the formula or a seeded simulation (Q30).

where Hn = 1 + 1/2 + 1/3 + … + 1/n is the n-th harmonic number, which grows like ln n (see lesson M01). So out of, say, 100 candidates interviewed in random order, you expect only about H100 ≈ 5.19 hires — dramatically better than the worst case of hiring all 100 times (if they happened to arrive in increasing order of quality).

Same trick, different question: what is the expected number of fixed points of a uniformly random permutation of n items (positions where item i stays in slot i)? Let Xi = 1 if position i is a fixed point. Pr{Xi=1} = 1/n (item i is equally likely to land in any of the n slots). By linearity, E[total fixed points] = n · (1/n) = 1, for every n — even though the Xi are NOT independent (if positions 1..n−1 are all fixed, position n must be too).

In Dart: all n! permutations for n = 1…7: the total number of fixed points equals n!, so the average is exactly 1; and the dependence between the indicators for n = 3 (Pr{slots 0 and 1 fixed} = 1/6 ≠ 1/9) while linearity still holds.

Input size → what's feasible: n ≤ 8 for enumeration; the proof by indicators works for every n.

11. Variance — how spread out is X?

Expectation tells you the average; variance tells you how much individual outcomes typically stray from that average. A die roll and a coin that pays 3.5 every single time both have E[X]=3.5, but they feel completely different — variance is what captures that difference.
Var[X] == E[(X - E[X]) ^ 2]  ==  E[X^2] - E[X]^2

In Dart: Var[X] = E[X²] − E[X]² and the definition E[(X − E[X])²] computed side by side for a die (35/12), a constant (0) and the sum of two independent dice (35/6).

Input size → what's feasible: sums over ≤ 36 outcomes here; for n samples it is one pass keeping the sum and sum of squares, O(n) time, O(1) space.

For a fair die: E[X]=3.5, E[X²] = (1+4+9+16+25+36)/6 = 91/6 ≈ 15.17, so Var[X] = 91/6 − 3.5² = 35/12 ≈ 2.92. A useful fact used later for tail bounds: variance of a SUM of pairwise-independent variables is the sum of the variances — no cross terms, unlike the mean, which needs no independence at all.

12. Geometric & binomial distributions

The geometric distribution answers "how many independent tries until the FIRST success?" — like flipping a biased coin until it finally lands heads. The binomial distribution answers a different question: "out of a FIXED number of tries, how many succeed?" — like counting heads in 20 coin flips.
Geometric:  Pr(X == k) == (1-p)^(k-1) * p          E[X] == 1 / p
Binomial:   Pr(X == k) == C(n,k) * p^k * (1-p)^(n-k)   E[X] == n * p

In Dart: geometric and binomial formulas as functions: Pr{X = k} tables summing to 1, the means 1/p and np, the variance np(1−p), the exactly-2-heads-in-3 = 3/8 value, and a seeded simulation.

Input size → what's feasible: geometric: truncating at 2000 terms is exact enough for p = 1/4; binomial with n ≤ 60 → choose fits an int; for larger n use logs.

The binomial distribution's mean is simply np — provable instantly by linearity: write X as the sum of n independent 0/1 indicators (one per trial, each with E=p), so E[X] = n·p, no combinatorics needed at all (a second, harder proof exists using the raw formula and Pascal's identity, but the indicator-variable proof is the one worth remembering).

In Dart: a Galton board as 10 coin flips per ball: 20,000 seeded balls fall into bins whose frequencies match C(10,k)/2¹⁰ to within 1.5 percentage points.

Input size → what's feasible: 10 pegs × 20,000 balls = 2·10⁵ flips; error ≈ 1/√balls, so 20,000 balls give about ±0.3% per bin.

A Galton board (a grid of pegs a ball bounces through, going left or right at each peg with probability ½) is a physical binomial-distribution generator: after m pegs, the ball's final bin = number of "right" bounces ~ Binomial(m, 0.5). Balls pile up into the familiar bell-shaped histogram purely from independent coin flips.

13. The birthday paradox

In a room of just 23 random people, there's a better-than-even chance that TWO of them share a birthday — far fewer people than the "365 days, so I'd need ~183 people" intuition suggests. The trick: you're not checking whether anyone shares YOUR birthday (that's a much harder coincidence to hit) — you're checking whether ANY of the C(23,2) = 253 possible pairs match, and 253 chances is a lot.
Pr(at least one shared birthday among k people)
  == 1 - ( 365/365 * 364/365 * 363/365 * ... * (365-k+1)/365 )

In Dart: the exact product 1 − Π(365−i)/365 as a loop, the smallest group above one half (23), the pigeonhole case (366 → 1.0) and a seeded simulation using a Set to detect a repeat.

Input size → what's feasible: k ≤ 365 → at most 365 multiplications; the product never underflows (365!/365³⁶⁵ ≈ 10⁻¹⁵⁷ is still a normal double).

Approximate reasoning via indicator variables: let Xij = 1 if people i and j share a birthday. E[Xij] = 1/365. There are C(k,2) pairs, so by linearity, E[number of matching pairs] = C(k,2)/365. Setting this ≈ 1 gives k ≈ √(2·365) ≈ 27 — the same order of magnitude as the exact answer (23), obtained without any messy products of fractions.

In Dart: the indicator-variable birthday argument: C(23,2) = 253 pairs, E[matching pairs] = C(k,2)/365 verified exactly on a tiny 4-day year with 3 people (all 64 outcomes), and k(k−1)/730 = 1 ⇒ k ≈ √730 ≈ 27.

Input size → what's feasible: k ≤ 10⁵ → C(k,2) ≈ 5·10⁹ fits a 64-bit int; the tiny-year check enumerates only days^people outcomes.

14. Balls, bins & the coupon collector

Throwing n balls independently and uniformly into b bins (think: hashing n keys into b hash-table slots) is the same math as birthdays. A related question: if a cereal box gives you ONE random coupon (out of n distinct kinds) each time, how many boxes do you expect to buy before you've collected ALL n kinds? This is the coupon collector's problem.
E[boxes needed to collect all n coupons] == n * H(n)

In Dart: E = Σ n/(n−i) = n·Hₙ as the stage-by-stage sum, compared with n·Hₙ for several n and with a seeded simulation for n = 8.

Input size → what's feasible: n ≤ 10⁶ → the formula is O(n); a simulation costs ≈ n·Hₙ draws per run (≈ 1.4·10⁷ for n = 10⁶), so keep runs small.

Why n·Hn? Split the process into n stages: stage i starts right after you collect your i-th distinct coupon and ends when you get your (i+1)-th new one. While you already have i distinct coupons out of n, each new box is "useful" with probability (n−i)/n — a geometric distribution with expectation n/(n−i). Summing these n expected stage-lengths for i=0..n−1 gives exactly n(1/n + 1/(n−1) + … + 1/1) = n·Hn.

15. Randomized algorithms: shuffling fairly

A shuffle is "fair" (produces a uniform random permutation) when every one of the n! possible orderings is equally likely to come out. This sounds obvious to implement, but the natural first attempt most people write is actually biased — some orderings come out more often than others — and the bug is invisible unless you specifically test for it.

The correct algorithm is the Fisher–Yates shuffle (in-place random permutation): walk the array left to right; at each position i, swap it with a uniformly random position chosen from the SHRINKING remaining suffix [i, n−1] — never from the whole array.

In Dart: The in-place shuffle with the 1-indexed → 0-indexed shift written out: math-style RANDOM(i, n) becomes i + rng.nextInt(n − i); then a seeded check that each element lands in slot 0 about 1/5 of the time.

Input size → what's feasible: n ≤ 10⁷ elements → n swaps ≈ 10⁷ steps (roughly a tenth of a second on a laptop); nextInt(max) needs max ≤ 2³².

Why this is exactly uniform (a loop-invariant argument): just before position i (counting from 0) is processed, the already-fixed prefix of i slots holds each possible ordered choice of i distinct items with the same probability, (n−i)!/n!. Step i then has exactly (n−i) equally likely choices for the swap partner, and induction shows that this keeps the prefixes equally likely, until at the end every full permutation has probability 1/n!. Counting argument: there are exactly n × (n−1) × … × 1 = n! possible sequences of random choices the algorithm can make, and it turns out each of the n! final permutations is produced by EXACTLY ONE such sequence — a perfect bijection, hence perfectly uniform.

Now the common bug: swap position i with a random index chosen from the WHOLE array every time, not the shrinking suffix.

This naive version is provably NOT uniform. For an array of 3 elements there are 3³ = 27 equally likely sequences of random choices, but only 3! = 6 possible orderings — and 27 doesn't divide evenly by 6, so some orderings MUST come out more often than others (exact counts: three orderings occur 4/27 of the time, the other three occur 5/27 of the time — verified by exhaustively enumerating all 27 sequences in verify/m02.dart). Always use the shrinking range [i, n−1], never [0, n−1].

In Dart: the fairness test for shuffles: every sequence of random choices for n = 3 is enumerated; Fisher–Yates (3·2·1 = 6 sequences) gives each of the 6 orders exactly once, the naive version (3³ = 27 sequences) gives three orders 4 times and three orders 5 times.

Input size → what's feasible: exhaustive enumeration only for n ≤ 8 (n^n sequences: 8⁸ ≈ 1.7·10⁷); for bigger n use the counting argument.

16. Randomized quicksort & tail bounds

Quicksort's worst case is Θ(n²) — but only for specific, adversarial input orders (already sorted, if you always pick the first element as pivot). The Quicksort lesson shows that picking the pivot uniformly at random at every step (instead of trusting the input to be "nicely shuffled") makes the Θ(n²) worst case astronomically unlikely for ANY fixed input, because the randomness is now inside the algorithm, not an assumption about outside data. The expected number of comparisons works out to a clean closed form, asymptotically 2n ln n — the same Θ(n lg n) as merge sort, just with a random pivot instead of a guaranteed-balanced split.

E[comparisons in randomized quicksort] == 2*(n+1)*H(n) - 4*n  ~=  2 * n * ln(n)

In Dart: the exact expectation from the recurrence E(n) = (n−1) + (2/n)ΣE(q) against the closed form 2(n+1)H(n) − 4n (n ≤ 200), then a seeded randomized-pivot run on 1000 keys compared with the formula and with 2n ln n.

Input size → what's feasible: n = 10⁶ → ≈ 2.5·10⁷ expected comparisons (a fraction of a second); the Θ(n²) worst case would be 5·10¹¹.

"Expected case" (randomized algorithm, no assumption on input) is a fundamentally stronger guarantee than "average case" (deterministic algorithm, assumption that input is drawn from some nice distribution) — an adversary who knows your algorithm can construct a worst-case input for the latter, but not for the former, because the algorithm's own coin flips (not the input) decide what happens.

How do we know the Θ(n²) case is "astronomically unlikely" rather than merely "not the average"? Tail bounds quantify exactly how unlikely a random variable is to stray far from its mean.

Markov's inequality (the simplest tail bound, needs only non-negativity): for X ≥ 0 and any t > 0, Pr{X ≥ t} ≤ E[X] / t. Intuition: if a big chunk of the probability mass sat far above t, the average would have to be pulled up too. Example: a fair die has E[X]=3.5, so Markov guarantees Pr{X ≥ 6} ≤ 3.5/6 ≈ 0.583 — true, if loose (the real answer is 1/6 ≈ 0.167). Markov bounds are cheap to compute but often far from tight.

Chernoff bounds (mentioned here; the full derivation is beyond this lesson) tighten this dramatically for sums of independent 0/1 variables (like quicksort's comparison count, or the number of heads in n coin flips): instead of a bound that shrinks like 1/t, Chernoff bounds shrink exponentially in how far you stray from the mean, which is exactly why randomized algorithms' worst cases are not just unlikely but vanishingly, negligibly unlikely.

In Dart: Markov's inequality checked on a die for t = 1…6, and the true tail of "heads in 100 flips" against the Markov bound: Pr{X ≥ 75} ≈ 2.8·10⁻⁷ while Markov only promises ≤ 0.667 — the tail collapses exponentially, Markov barely moves.

Input size → what's feasible: 100 flips → exact tail via a running binomial coefficient in double (C(100,50) ≈ 10²⁹ fits); for n in the thousands work in logs.

Quiz

Interview questions

Cheat sheet

ConceptFormulaNotes
Rule of sum|A∪B| = |A|+|B|disjoint groups, pick from ONE
Rule of product|A×B| = |A|·|B|independent groups, pick ONE from EACH
Permutationsn!arrange all n, order matters, no repeats
k-permutationsn!/(n−k)!arrange k of n, order matters
CombinationsC(n,k) = n!/(k!(n−k)!)choose k of n, order doesn't matter
k-stringsnklength-k strings over n-letter alphabet, repeats OK
Subsets2n= Σ C(n,k) (binomial theorem, x=y=1)
ComplementPr{Ā} = 1 − Pr{A}"at least one" ⇒ compute the complement
Inclusion–exclusionPr{A∪B} = Pr{A}+Pr{B}−Pr{A∩B}subtract the double-counted overlap
Conditional probabilityPr{A|B} = Pr{A∩B}/Pr{B}shrinks the sample space to B
IndependencePr{A∩B} = Pr{A}·Pr{B}pairwise ≠ mutual (3-event trap)
Bayes' theoremPr{A|B} = Pr{A}·Pr{B|A}/Pr{B}Monty Hall: switch wins 2/3; rare-disease tests can mislead
Linearity of expectationE[X+Y] = E[X]+E[Y]always true, no independence needed — THE tool for algorithm analysis
Indicator variableE[XA] = Pr{A}turns "expected count" into "sum of probabilities"
Best-so-far hiringE[hires] = Hn ≈ ln nrandom order beats worst-case Θ(n) hires
Fixed pointsE[fixed points] = 1true for every n, via linearity
VarianceVar[X] = E[X²] − E[X]²sums for pairwise-independent variables
GeometricE[trials to 1st success] = 1/pfixed success prob p per trial
BinomialE[successes] = np, Var = np(1−p)fixed n trials; Galton board ~ Binomial(m,½)
Birthday paradox1 − Π(365−i)/365≈50.7% collision chance at 23 people
Coupon collectorE[trials] = n·Hncollecting all n distinct items
Fisher–Yatesswap i with random j∈[i,n−1]uniform; swapping with [0,n−1] every time is BIASED
Randomized quicksortE[comparisons] ≈ 2n ln nexpected case beats Θ(n²) worst case for ANY input
Markov's inequalityPr{X≥t} ≤ E[X]/tcheap but loose; Chernoff bounds are exponentially tighter