3.6.7 · D4Sorting & Searching

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

3,587 words16 min readBack to topic

Vocabulary reminder (all defined in the parent, restated here so you never leave this page):

  • = how many keys we sort.
  • = the radix = how many different values one digit can take ( for decimal).
  • = how many digit positions a key has = .
  • = the place-value divisor that selects which digit we are looking at right now. It marches through picks the units digit, the next digit up, and so on. Concretely the digit we read is : dividing by slides the wanted digit down into the units slot, and chops off everything above it.
  • means integer division throughout (throw away the remainder): . In the code blocks Python's // operator means the same thing.
  • Zero-padding of short keys. If a key has fewer than digits, its higher positions are implicitly . This is automatic in the formula: once , integer division gives , so . So in a 3-digit array behaves as — its tens and hundreds digits both read as with no special case needed.
  • A sort is stable if equal keys keep the order they arrived in.

Level 1 — Recognition

L1.1 — Read the digit

Problem. For the key in base , what is the digit selected by ? Use the digit-extraction formula (where is integer division), with as defined in the vocabulary box above.

Recall Solution

picks the hundreds place. What we did: integer-divided by to shift the hundreds digit down to the units slot, then took to chop off everything above it. Answer: .

L1.2 — Count the passes

Problem. LSD radix sort on decimal keys whose maximum is . How many passes run?

Recall Solution

(802 has three digits: units, tens, hundreds). The loop condition is , where maxv is the largest key in the array (here ) and is integer division. It stays true for (giving ) and becomes false at because . So the loop body — one counting-sort pass — runs exactly three times. Answer: passes.

L1.3 — Spot the stable placement

Problem. In counting sort's placement loop, why do we walk the input array from right to left rather than left to right?

Recall Solution

The prefix-sum count[d] holds the last index a key with digit may occupy. Walking right-to-left and decrementing count[d] before each placement puts the rightmost equal-digit element at the highest free slot — so equal-digit elements land in their original relative order. That is exactly stability, which the LSD correctness proof needs. (See the plum arrow in the bottom row of figure s01.) Answer: to preserve stability.


Level 2 — Application

L2.1 — One pass by hand (full mechanics)

Problem. Run one LSD pass (units digit, , ) of counting sort on . Show the tally, the prefix-sum table, and the decrementing-count placement — then give the resulting array.

Recall Solution

Step 1 — tally each units digit. Units digits are . So digit 0 appears once, digit 3 three times, digit 6 once, digit 9 once.

Step 2 — prefix sums → "last free slot". Replace each count[i] by the running total count[i] += count[i-1]. This tells us the index just past where digit 's block ends: Read it as: digit 0 owns slot up to index 1 (slot 0), digit 3 owns up to index 4 (slots 1–3), digit 6 up to 5 (slot 4), digit 9 up to 6 (slot 5).

Step 3 — place right-to-left, decrementing first. We walk the input from the right so equal-digit elements keep their original order (stability):

  • (d=3): count[3] , place at index 3.
  • (d=3): count[3] , place at index 2.
  • (d=6): count[6] , place at index 4.
  • (d=0): count[0] , place at index 0.
  • (d=9): count[9] , place at index 5.
  • (d=3): count[3] , place at index 1.

Slots (0..5): . Note landed in indices — their original left-to-right order preserved, exactly because we placed right-to-left.

Answer: .

L2.2 — Full LSD trace (watch stability chain the passes)

Problem. Fully sort with LSD radix, base 10. Show all three passes and explain how each pass's prefix-sum placement preserves the previous pass's order among ties.

Recall Solution

Pass (units). Units digits . Tally: digit 0→1, 5→1, 6→1, 7→2, 9→2. Prefix sums give block ends . Placing right-to-left: (Among the two digit-7 keys, then keep their input order because , met first going right-to-left, takes the higher of the two digit-7 slots.)

Pass (tens). Tens digits of the current array . Tally: 2→2, 3→2, 5→3. Prefix sums . Placing right-to-left: Among the three tens-5 keys, the incoming order was (from the units pass), and the stable placement keeps exactly that — this is the units order riding along inside the tie.

Pass (hundreds). Hundreds digits . Tally: 3→2, 4→2, 6→1, 7→1, 8→1. Prefix sums . Placing right-to-left: Answer: .

L2.3 — Plug into the cost formula

Problem. Sort keys, each up to , in base . Let be the total number of basic operations the algorithm performs (the raw work count that Big-O later summarises). Using with , compute .

Recall Solution

Here counts basic operations across all passes: each pass does tally + placement

  • prefix work units of work, and there are passes, so . . Then Answer: basic operations.

Level 3 — Analysis

L3.1 — Why not selection sort per digit?

Problem. Suppose you replace counting sort with selection sort (which is not stable) as the per-digit sort. Give a concrete 2-element, 2-digit input where the final result comes out wrong, and explain the failure.

Recall Solution

The failure must involve a tie on the higher (tens) digit but a difference on the lower (units) digit — that's the only place stability does work. The smallest such pair is : both have tens digit (the tie), units digits and (the difference).

  • Units pass: digits → correct order is .
  • Tens pass: both tens digits are — a tie. A stable sort must keep . A non-stable selection sort, seeing two "equal" tens digits, is free to swap them back to wrong output .

Answer: (any pair equal on the top digit, unequal on a lower digit) breaks a non-stable per-digit sort.

L3.2 — MSD early stop counting

Problem. MSD radix sorts the strings ["dog","cat","cow","dot"]. How many character comparisons/reads does MSD make in total, and which characters are never read?

Recall Solution

Char 0: d,c,c,d → buckets c={cat,cow}, d={dog,dot}. (4 reads) Bucket c char 1: a vs o → distinguishes immediately → [cat, cow]. (2 reads) Bucket d char 1: o vs o → tie → recurse char 2: g vs t[dog, dot]. (2 + 2 reads) Total reads: . Never read: cat[2]='t' and cow[2]='w' — bucket c was resolved at char 1, so char 2 of those two strings is never examined. Answer: 10 reads; cat[2], cow[2] skipped.

L3.3 — Choosing radix to minimise passes

Problem. 32-bit unsigned keys. Compare (number of passes) for radices . Which minimises , and what is the trade-off?

Recall Solution

:

  • : passes, count array size .
  • : passes, count array size .
  • : passes, count array size .

minimises passes () but its count array of entries dwarfs the data unless is huge; each pass also costs so the term () becomes non-negligible. Answer: is smallest at (); trade-off is a -slot count array vs the -slot one at .


Level 4 — Synthesis

L4.1 — Sorting with negatives

Problem. LSD radix as written assumes non-negative keys. Design a fix to sort an array that contains negative integers too, then apply it to .

Recall Solution

Idea: shift all keys by a constant so the minimum becomes , radix-sort, then shift back. Let . Add to every key: — all non-negative. Sort with ordinary LSD radix (max is , base 10, ):

  • units (digits ): stable placement gives . (digit 0: ; digit 1: ; digit 2: ; digit 5: ; digit 7: )
  • tens (digits of are ): .

Subtract back: . Answer: . (Shifting preserves order because it's monotonic.)

L4.2 — Radix sort meets stability requirement

Problem. You must sort records by age, and within equal age keep the input order of names. Explain why LSD radix on the age field automatically satisfies this, using the parent's stability result. Then order these records by age: [(30,"A"), (12,"B"), (30,"C"), (12,"D")].

Recall Solution

LSD radix's per-digit sort (counting sort) is stable, and the whole LSD chain preserves the relative order of keys equal on the sort field. So equal ages retain their input order — exactly the requirement, no extra work needed. Ages: . Sorting stably by age:

  • age 12: B, D (input order)
  • age 30: A, C (input order)

Answer: [(12,"B"), (12,"D"), (30,"A"), (30,"C")].

L4.3 — Cross-connect: bucket vs radix

Problem. Bucket Sort and radix sort both scatter items into groups. State the essential difference in what determines the group, and why radix needs multiple passes while bucket sort often needs one.

Recall Solution

Bucket sort groups by value range (e.g. ), then sorts inside each bucket with some comparison sort — one scatter pass, assuming uniform-ish data. Radix sort groups by a single digit value (), which only resolves ordering on that one digit; it needs passes (one per digit) to resolve the whole key, and relies on stability to chain the passes. Answer: bucket = group by value range (one pass); radix = group by one digit (needs stable passes to cover all digit positions).


Level 5 — Mastery

L5.1 — Prove the pass invariant

Problem. State and prove the LSD invariant: after processing digit positions , the array is sorted by the number formed by those low digits. (Reproduce the induction; you may cite stability.)

Recall Solution

Invariant. After the passes for digit positions (least significant first), the array is sorted in non-decreasing order by the number formed from those low digits.

Base (): we stably counting-sort by digit ; the array is now sorted by the lowest digit — the number "formed by digit 0" is just that digit. ✓

Inductive step. Assume the array is sorted by digits . Pass stably sorts by digit . Take any two keys that appear in the output in order before :

  • Different digit : the stable sort placed the smaller digit- key first, and the digit in position carries more place-value than all of positions combined, so it dominates the value of the -digit number — the order is correct.
  • Equal digit : stability keeps their previous relative order untouched; by the inductive hypothesis that order was already correct on digits , and since the top digit is equal, it stays correct on digits .

Both cases give the correct order, so the invariant holds for . By induction it holds for all ; at every digit has been processed and the array is fully sorted.

Answer: proof complete — the two-way case split (different vs equal higher digit) plus stability is the whole engine.

L5.2 — Derive the crossover with quicksort

Problem. Keys are integers in (so max ). Using with , find and the resulting . For what does radix beat ?

Recall Solution

With : (a constant). This is linear whenever is a fixed constant, so radix beats for every constant (any polynomially-bounded key range). It loses only when keys grow faster than any polynomial (e.g. , giving ). Answer: , ; radix wins for all constant . This dodges the Comparison Sort Lower Bound because radix never compares whole keys.

L5.3 — Design an MSD string sorter with tie-descent

Problem. Write the recursion (pseudocode) for MSD radix sort of variable-length strings, handling the case where one string is a prefix of another (e.g. "an" vs "ant"). Then order ["an", "ant", "and", "a"].

Recall Solution

Treat "no more characters" as a special bucket that sorts before every real character (shorter-prefix comes first lexicographically):

def msd(strings, pos):
    if len(strings) <= 1: return strings
    buckets = {}          # key: char at pos, or END sentinel
    for s in strings:
        key = END if pos >= len(s) else s[pos]
        buckets.setdefault(key, []).append(s)
    result = []
    for key in sorted(buckets):        # END first, then 'a'..'z'
        group = buckets[key]
        result += group if key is END else msd(group, pos+1)
    return result

Trace on ["an","ant","and","a"], pos 0 (all start a) → recurse pos 1:

  • a → END (len 1) : ["a"]
  • an,ant,and → char 1 = n : recurse pos 2:
    • an → END : ["an"]
    • ant char 2 = t, and char 2 = dd < t["and","ant"]

Concatenate END-first: ["a", "an", "and", "ant"]. Answer: ["a", "an", "and", "ant"]. The END sentinel (sorting first) is what makes a prefix precede its extensions — the same trick Tries use for word boundaries.


Active Recall

Recall Rapid checkpoints

Digit at of ::: Passes for max key , base 10 ::: for keys in with ::: , since Input breaking a non-stable per-digit sort ::: (tie on tens digit) Sentinel that makes "an" < "ant" in MSD ::: END, sorted before every character