Context:FIT2004_MOC · the fix for Counting Sort’s M blow-up — stop treating the key as one huge number, treat it as K narrow columns · still non-comparison, so the Ω(NlogN) floor in Sorting Problem does not bind
Quick Revision
🎯 Objective: run a stableCounting Sort once per digit column, least-significant first ➔ Θ(KN+KM), beating Merge Sort’s Θ(kNlogN) for fixed-width keys.
📦 Core Components:N items ×K columns | base M (digits 10 · bits 2 · letters 26) | stable subsort per column.
⚡ Key Constraint: the subsort must be stable — an unstable column pass destroys all order won by previous passes, and the output is wrong, not merely unsorted.
📝 How It Works
1. The Reframing
Key as a grid ➔ items are rows, digits are K columns; rightmost = least significant.
Why M collapses ➔ each column holds only base-M symbols, so every subsort sees M=10 (decimal), 2 (binary), 26 (alphabet) — never the 981 or 232 that ruined plain Counting Sort.
K is a length, not a count ➔ K=⌈logM(largest key)⌉ ⟹ raising the base shrinksK but grows M; that trade is the exam’s design question.
Lecture notation map — memorise both ➔ this note’s K (columns) is the lecture’s c; this note’s M (base) is the lecture’s b; the lecture reserves M for the largest item. In lecture symbols: c=⌊logbM⌋+1, time O(cN+cb), auxiliary O(N+b).
Digit extraction is arithmetic, not string slicing ➔ column j (from 0 at the least significant end) of key x is ⌊x/bj⌋modb ⟹ O(1) per digit.
2. Least-Significant-Digit Order + Stability
Pass order ➔ sort on column K (rightmost), then K−1, …, then column 1 ⟹ after pass j the list is correctly sorted on the last j digits.
Stability carries the earlier work ➔ when column j ties, a stable subsort preserves the previous pass’s order, which is the ordering on the lower-significance digits ⟹ the invariant extends.
Unstable ⟹ catastrophe ➔ with 200 before 291 from pass 2, an unstable hundreds pass may emit 291 first; both share hundreds digit 2, so nothing later repairs it.
3. Choosing the Base — Where Linearity Comes From
The base is a free design parameter ➔ nothing forces b=10; that choice alone decides whether the sort is linear.
Raise b up to N ➔ per-pass cost is Θ(N+b), so any b≤N leaves it at Θ(N) ⟹ total Θ(cN); b=N buys the smallestc at no asymptotic cost.
Never let b>N ➔ the size-b count array then dominates the N items and the sort is linear in the base, not in N.
The linearity theorem ➔ with b=N, c=⌊logNM⌋+1; so if the largest item satisfies M=O(Nd) for constantd, then c=Θ(1) and radix sort is Θ(N):
b=N,M=O(Nd)⟹c=logNM=Θ(d)⟹T=Θ(cN+cb)=Θ(N)
Where it stops being linear ➔ M super-polynomial in N (NN, N!) makes c grow with N — the distinct-keys argument reached from the base side.
b=N is the asymptotic rule, NOT the constant-factor optimum[D] ➔ for N integers of W bits, c=W/log2b and the operation count f(b)=log2bW(N+b) is minimised well below N: at W=64,N=106 the minimum sits near b≈95,536, so the practical choice is b=216 — the nearest power of two whose exponent divides the word width, giving exactly c=4 passes.
Powers of two win twice ➔ base 2t makes digit extraction a shift and mask instead of a division, and t∣W avoids a ragged final column; timings degrade sharply for b=220 and 224 because 20 and 24 do not divide 64.
4. Ragged Keys
Unequal lengths break the column grid ➔ banhammer and kappa have no common K.
Pad to a common width ➔ insert a filler that sorts below every real symbol; numbers pad with leading zeros (right-aligned), strings with spaces.
Alignment decides the order semantics ➔ right-aligned padding gives numeric order, left-aligned gives lexicographic — choose deliberately, they differ.
5. Optimising for Strings — the Θ(n) Algorithm (applied Problem 4 — [D, HD])
Applied-sheet notation ➔ k = number of strings · ℓ = length of the longest · n=∑ilen(Si) = total characters. The optimal bound is Θ(n) in thatn.
Naive padding is Θ(ℓk) ➔ every string is scanned ℓ times regardless of its own length ⟹ 106 strings of length 10 plus one of length 106 costs 1012 operations against n≈2×107 actual characters.
The fix — never look at a string before it has a character to contribute ➔ a string of length i only matters in the final i iterations of an LSD sweep.
Step 1 — counting-sort by length, ASCENDING ➔ key =len(S), range ℓ ⟹ Θ(ℓ+k). Ascending is load-bearing: shorter strings must precede longer ones that share a prefix (cat before cats) — exactly alphabetical order.
Step 2 — sweep columns i=ℓ down to 1 with a live pointer ➔ maintain j so that S[j…k] are the strings of length ≥i; on each decrement of i, decrement j while len(S[j−1])≥i ⟹ strings enter the window exactly when they acquire a character at position i, and never leave.
Step 3 — run the stable counting subsort on S[j…k] only ➔ base 26, so no pass touches a non-existent character.
Why it totals Θ(n) ➔ string Si participates in exactly len(Si) passes ⟹ work =∑ilen(Si)=n, plus Θ(ℓ+k) ⟹ optimal, since every character must be read once.
This scheme is left-aligned/lexicographic ➔ position 1 is the first character; right-aligned padding is the numeric reading and the wrong model for words ➔ §4.
⚙️ Core Implementation
🔹 LSD radix sort over base M
radix_sort — stable counting subsort per column, raw index manipulation
def radix_sort(my_list, base=10): if len(my_list) == 0: return my_list maximum = my_list[0] for i in range(1, len(my_list)): if my_list[i] > maximum: maximum = my_list[i] exp = 1 # column selector: 1, base, base^2, ... while maximum // exp > 0: # K iterations my_list = counting_pass(my_list, exp, base) exp = exp * base return my_listdef counting_pass(my_list, exp, base): count = [0] * base for i in range(len(my_list)): digit = (my_list[i] // exp) % base count[digit] += 1 position = [0] * base # prefix sum -> block start of each digit for d in range(1, base): position[d] = position[d - 1] + count[d - 1] output = [0] * len(my_list) for i in range(len(my_list)): # INPUT ORDER => stable digit = (my_list[i] // exp) % base output[position[digit]] = my_list[i] position[digit] += 1 return output
💡 Common Mistake:Iterating the placement loop backwards or reusing count as position ➔ silently makes the pass unstable; the first column still looks right, so the bug only surfaces on multi-digit input.
🔹 Θ(n) radix sort for variable-length strings
radix_sort_strings — length sort, then a live-window sweep (applied Problem 4)
def radix_sort_strings(S): # S: non-empty lowercase strings. n = SUM of all lengths. k = len(S) longest = 0 for i in range(k): if len(S[i]) > longest: longest = len(S[i]) S = counting_sort_by_length(S, longest) # ASCENDING; O(longest + k) j = k # invariant: S[j..k-1] have length >= i for i in range(longest, 0, -1): # LSD sweep over character positions while j > 0 and len(S[j - 1]) >= i: j = j - 1 # admit strings that now HAVE a char i count = [0] * 26 for p in range(j, k): count[ord(S[p][i - 1]) - 97] += 1 position = [0] * 26 # prefix sum -> block starts for c in range(1, 26): position[c] = position[c - 1] + count[c - 1] temp = [None] * (k - j) for p in range(j, k): # INPUT ORDER => stable d = ord(S[p][i - 1]) - 97 temp[position[d]] = S[p] position[d] += 1 for p in range(j, k): # write the window back S[p] = temp[p - j] return S
💡 Common Mistake:Sorting the lengths descending ➔ then cats precedes cat, which is not alphabetical. Ascending is what makes a shorter string win when it is a prefix of a longer one — the same rule that makes the pad symbol sort below every real character ➔ §4.
When It Flips: radix beats Merge Sort while K<log2N. Fixed-width keys (32-bit ints, K=10 decimal digits) ⟹ K is a constant ⟹ Θ(N). Distinct keys force K≥logMN, degenerating Θ(KN) back to Θ(NlogN) — so the advantage shrinks exactly when keys become long; raising base M buys K down at Θ(KM) time and Θ(M) space, and stops paying once M approaches N.
Stability check on pass 3:200 and 291 both have hundreds digit 2; pass 2 left 200 before 291, and the stable subsort keeps it ⟹ correct. An unstable pass could emit 151 291 200 … — wrong final answer.
Applied Exercise
Problem: Derive radix sort’s time and auxiliary space from Counting Sort’s Θ(N+M), then specialise to base-10 integers.
TS=columnsK×one stable counting sortΘ(N+M)=Θ(KN+KM)=input, K symbols per itemΘ(KN)+one subsort, reusedΘ(M+N)=Θ(KN+M+N)
Final Extracted Output:Θ(KN+KM) time, Θ(KN+M+N) total space, Θ(M+N)auxiliary — auxiliary carries no K, because the same count/position/output arrays are reused every pass rather than allocated per column. At M=10: T=Θ(KN), Saux=Θ(N).
⚠️ Common Mistakes
💡 Multiplying K into the auxiliary space ➔ the subsort arrays are reused across passes ⟹ auxiliary is Θ(M+N); only the input carries the K factor.
💡 Reusing the two meanings of M ➔ in Counting SortM is the maximum key; here M is the base. Name which one you mean before quoting a bound.
💡 Calling radix sort Θ(N) with no conditions ➔ true only once you state that K and M are capped; distinct keys force K=Ω(logMN), recovering Θ(NlogN).
💡 Sizing the count array from max() ➔ that is Counting Sort’s rule; radix sizes it from the base and uses max only for the column countc=⌊logbM⌋+1. Lecturer-flagged as the recurring code-review error.
🧠 Active Recall
Why must radix sort's per-column subsort be stable, and why is LSD order the one that needs it?
Hint: Ask what the previous passes have already established when a tie occurs.
Answer
Short answer: ties on the current digit must fall back on the order from the lower-significance passes — stability is precisely that fallback.
Why:Invariant ➔ after pass j the list is sorted on the last j digits; a stable pass j+1 preserves that order within each new digit group, so the invariant extends. An unstable pass discards it and no later pass can recover it.
Radix sort is Θ(KN) and merge sort Θ(NlogN) — is radix sort therefore always better?
Hint: Ask how small K can be when all N keys are distinct.
Answer
Short answer: no — N distinct base-M keys need K≥logMN, so Θ(KN) degenerates to Θ(NlogN).
Why:K is only constant when the key width is capped ➔ fixed-width 32-bit integers give K=Θ(1) and a genuine Θ(N); unbounded keys do not, and radix additionally requires integer-decomposable keys where Merge Sort accepts any orderable type.
You may choose the base freely. What choice makes radix sort provably Θ(N), and where does that stop working?
Hint:K and M move in opposite directions; put a ceiling on the base.
Answer
Short answer: choose b=N — per-pass cost stays Θ(N+b)=Θ(N) and c=⌊logNM⌋+1, so if the largest key is O(Nd) for constant d then c=Θ(1) and T=Θ(N).
Why:Past b=N the count array dominates ➔ the sort becomes linear in the base rather than in N; and for M super-polynomial in N, c grows with N and linearity is lost from the other side.