Worked examples — Radix sort — LSD, MSD; O(d(n+k))
The scenario matrix
Before touching a single number, let us list every distinct kind of situation a radix sort can face. If we cover one example per row, the reader never meets a case we skipped.
| Cell | Case class | What makes it tricky | Covered by |
|---|---|---|---|
| A | Plain non-negative ints, LSD | the "textbook" happy path | Ex 1 |
| B | Zero + a single-digit value mixed with multi-digit | short keys have "missing" high digits | Ex 2 |
| C | Negative numbers | digits/mod go wrong on negatives | Ex 3 |
| D | All keys equal (degenerate) | every pass is a no-op; stability under total ties | Ex 4 |
| E | Variable-length strings, MSD, early stop | MSD reads only a prefix | Ex 5 |
| F | Radix choice trade-off ( vs ) | picking changes pass count & memory | Ex 6 |
| G | Limiting behaviour: huge keys () | the hidden — radix loses its win | Ex 7 |
| H | Word problem (real world) | mapping a real task onto digits | Ex 8 |
| I | Exam twist: stability sabotage | swap the stable sort for an unstable one | Ex 9 |
Symbols reused from the parent, restated so nothing is assumed:
- = how many elements we sort.
- = the radix = how many different values one digit can take (base 10 → ).
- = number of digit positions we must process .
- A digit-selector: for value , digit at position (counting from 0 at the right) is — "chop off the low digits, then take the remainder mod ". We lean on Counting Sort for each pass and on Stability in Sorting to keep ties in order.

Ex 1 — Cell A: textbook LSD, base 10
Forecast: the answer must be the numeric sort . Guess how many passes: max key is → 3 digits → 3 passes.
- Pass (units). digits . Stable counting sort → . Why this step? We start at the least significant digit so that later, heavier digits can overwrite this ordering while stability protects the ties.
- Pass (tens). digits of the current array: → . Why this step? Among the three keys with tens-digit () the previous units order is preserved — that is stability doing the induction's job.
- Pass (hundreds). digits: → . ✅
Verify: compare to Python's sorted() of the same list — identical. Every key has 3 digits or
fewer, so 3 passes were exactly .
Ex 2 — Cell B: zero and short keys among long ones
Forecast: shorter numbers behave as if their missing high digits are zero. Answer should be .
- ? max → 3 digits → 3 passes. Why this step? for all , and — so "no digit there" is automatically read as digit . Nothing special needed.
- Pass (units): digits → .
- Pass (tens): digits → . Why this step? all have tens-digit → they keep prior order (stability); goes last.
- Pass (hundreds): digits → . ✅
Verify: never moves relative to the other zero-high-digit keys except when a real higher
digit appears — matches sorted([0,9,105,7,88]) = [0,7,9,88,105].
Ex 3 — Cell C: negative numbers
Forecast: is the smallest, so the answer is . But misbehaves — negatives need a fix.
- The problem. Radix's digit extraction assumes non-negative integers. Why this step? If we naively took in most languages that's fine, but the magnitude ordering is backwards for negatives: has a bigger magnitude than yet must sort earlier. Ordinary radix would place larger magnitudes later.
- The offset trick. Find . Add to every key, making all values : . Why this step? Shifting by a constant is order-preserving (), so sorting the shifted keys sorts the originals — and now every value is non-negative, so plain LSD works.
- LSD sort the shifted array (, max ): → .
- Subtract 42 back: . ✅
Verify: sorted([-5,3,-1,0,8,-42]) = [-42,-5,-1,0,3,8]. Offset in, offset out — bijection kept.
Ex 4 — Cell D: all keys equal (degenerate)
Forecast: the array is already sorted; every pass is a no-op; the index tags stay in order .
- Pass : all units digits are . Counting sort's prefix array sends them all to the same bucket. Why this step? The placement loop walks right to left and decrements, so lands after after after → order preserved.
- Passes : identical no-ops. Degenerate but correct.
Verify: stability means a totally-tied input comes out unchanged. Tags remain —
stable_sort([7,7,7,7]) keeps original order. This is the boundary case that proves stability, not
just "sorted correctly", matters for Stability in Sorting.
Ex 5 — Cell E: MSD on variable-length strings, early stop
Forecast: answer alphabetical: ["an","ant","at","az","zebra"]. And MSD should read very few
characters of "zebra".

- Char 0: buckets
a={at, ant, an, az},z={zebra}. Why this step? MSD splits on the most significant char; bucketzhas one element → done immediately, reading only"zebra"[0]. That's the early-stop win. - Recurse bucket
aon char 1:t→{at},n→{ant, an},z→{az}. Order so far by char 1:n-group, thent, thenz. Why this step? Only bucketais touched — MSD localizes work to the relevant slice. - Recurse
{ant, an}on char 2:"an"has no char 2 (shorter) → treat "end of string" as smaller than any letter →anbeforeant. Why this step? Variable length: a key that ends sorts before one that continues, matching dictionary order ("an" < "ant"). - Concatenate:
["an","ant","at","az","zebra"]. ✅
Verify: equals sorted(["zebra","at","ant","an","az"]). Characters read: zebra→1, at→2,
ant→3, an→2 (ends), az→2. LSD would have scanned all 5 chars of zebra. MSD read 1 —
the Tries-like prefix descent pays off.
Ex 6 — Cell F: choosing the radix
Forecast: more bits per digit → fewer passes but a bigger count array. Guess which wins for .
- : bits per digit , so . Work . Why this step? Each pass processes one 8-bit slice; 32 bits ÷ 8 = 4 slices.
- : bits per digit , so . Work . Why this step? Bigger halves the passes, and so the count array is still cheap.
- Compare: → wins here. Why this step? The rule favours large as long as stays small relative to . Since , we pay almost nothing extra for the count array.
Verify: the two totals are and ; the second is smaller. (For tiny , a huge would lose because dominates — this is the Big-O trade-off in .)
Ex 7 — Cell G: limiting behaviour, huge keys
Forecast: the parent warned hides a . Guess: no, it becomes -ish.
- Compute : . Why this step? exactly — choosing makes each digit "worth" a full factor of , so only 3 digits are needed.
- Total cost: . Linear! ✅ Why this step? With the count array costs , same order as the data — no penalty.
- The catch — what if we'd fixed ? Then and — the Comparison Sort Lower Bound is matched, not beaten. Why this step? The linear result depends on scaling with . A fixed small radix reintroduces the hidden logarithm.
Verify: (independent of ); . And confirms when is fixed.
Ex 8 — Cell H: word problem
Forecast: it's a 6-digit decimal key → LSD, base 10, 6 passes. Answer: chronological order.
- Model the key. Treat
HHMMSSas the integer (e.g.093045→ ). Why this step? Lexicographic order of fixed-width digit strings equals numeric order, so sorting the integer sorts the times. - Choose LSD, , . Why this step? Fixed-width numeric keys are LSD's sweet spot (Cell A of the matrix, scaled up).
- Cost: for large — beats comparison sort. Why this step? and are constants, so linear time regardless of .
Verify: for orders, work basic ops — vs a comparison sort's . Radix is fewer ops here, and every timestamp fits in 6 digits so 6 passes suffice.
Ex 9 — Cell I: exam twist, stability sabotage
Forecast: correct answer is . An unstable pass can scramble ties on the tens digit. Guess: it breaks.
- Pass (units), stable would give: units → keys with unit : (order kept), unit : → correct partial … but suppose the unstable sort reorders ties → (units still grouped, but ties flipped). Why this step? An unstable sort may permute equal-key elements arbitrarily; that's allowed by its contract.
- Pass (tens): tens digits of are . Unstable again may output, among tens- group and tens- group , e.g. . Why this step? The lower-digit order from pass 0 is not protected — the induction in the parent proof required stability, and it's now gone.
- Result: — not sorted ( before ). ✗ Why this step? This is exactly the collapse the parent's "steel-man" warned about.
Verify: the correct output is sorted([21,11,22,12]) = [11,12,21,22]. Our unstable trace gives
, so unstable-per-digit radix is provably wrong. Stability is not
optional.
Active Recall
Recall Which matrix cell needed the
offset trick, and why? Cell C (negatives) ::: because digit extraction assumes non-negative ints; adding is order-preserving and makes every key .
Recall In Ex 6, why did
beat ? Fewer passes ::: dropped from 4 to 2, while stayed , so the count array stayed cheap; total fell from to .
Recall When does the "
" claim quietly become ? When is fixed but keys grow ::: then for ; scale with to stay linear.