3.6.7 · D3Sorting & Searching

Worked examples — Radix sort — LSD, MSD; O(d(n+k))

2,234 words10 min readBack to topic

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.
Figure — Radix sort — LSD, MSD; O(d(n+k))

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.

  1. 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.
  2. 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.
  3. 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 .

  1. ? max → 3 digits → 3 passes. Why this step? for all , and — so "no digit there" is automatically read as digit . Nothing special needed.
  2. Pass (units): digits .
  3. Pass (tens): digits . Why this step? all have tens-digit → they keep prior order (stability); goes last.
  4. 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.

  1. 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.
  2. 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.
  3. LSD sort the shifted array (, max ): → .
  4. 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 .

  1. 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.
  2. 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".

Figure — Radix sort — LSD, MSD; O(d(n+k))
  1. Char 0: buckets a={at, ant, an, az}, z={zebra}. Why this step? MSD splits on the most significant char; bucket z has one element → done immediately, reading only "zebra"[0]. That's the early-stop win.
  2. Recurse bucket a on char 1: t→{at}, n→{ant, an}, z→{az}. Order so far by char 1: n-group, then t, then z. Why this step? Only bucket a is touched — MSD localizes work to the relevant slice.
  3. Recurse {ant, an} on char 2: "an" has no char 2 (shorter) → treat "end of string" as smaller than any letter → an before ant. Why this step? Variable length: a key that ends sorts before one that continues, matching dictionary order ("an" < "ant").
  4. 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 .

  1. : bits per digit , so . Work . Why this step? Each pass processes one 8-bit slice; 32 bits ÷ 8 = 4 slices.
  2. : bits per digit , so . Work . Why this step? Bigger halves the passes, and so the count array is still cheap.
  3. 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.

  1. Compute : . Why this step? exactly — choosing makes each digit "worth" a full factor of , so only 3 digits are needed.
  2. Total cost: . Linear! ✅ Why this step? With the count array costs , same order as the data — no penalty.
  3. 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.

  1. Model the key. Treat HHMMSS as 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.
  2. Choose LSD, , . Why this step? Fixed-width numeric keys are LSD's sweet spot (Cell A of the matrix, scaled up).
  3. 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.

  1. 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.
  2. 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.
  3. 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.


Flashcards

How to radix-sort negative integers
Add to every key (order-preserving) to make all non-negative, LSD sort, then subtract the offset back.
Why can MSD read fewer characters than LSD on strings
MSD splits on the most significant char first; a bucket of size 1 or a distinguishing prefix stops recursion, so trailing chars are never read.