3.6.7Sorting & Searching

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

2,259 words10 min readdifficulty · medium4 backlinks

WHAT is Radix Sort?

Key vocabulary:

  • nn = number of elements.
  • kk = the radix / number of possible digit values (e.g. k=10k=10 for decimal, k=256k=256 for bytes).
  • dd = number of digit positions = logk(max key)\lceil \log_k(\text{max key}) \rceil.

WHY does LSD radix sort work? (Derivation from scratch)

We need an invariant. Claim:

After processing digit positions 0,1,,i0,1,\dots,i (least significant first), the array is sorted by the number formed by those low i+1i+1 digits.

Proof by induction.

Base case (i=0i=0): we stably sort by digit 0. The array is sorted by the lowest digit. ✓

Inductive step: Suppose after pass ii the array is sorted by digits 0..i0..i. In pass i+1i+1 we stably sort by digit i+1i+1.

  • Two keys with different digit i+1i+1 end in the correct relative order, because the higher digit dominates the value.
  • Two keys with the same digit i+1i+1 keep their previous relative order — because the sort is stable — and that previous order was already correct on digits 0..i0..i. ✓

So after all dd passes the array is fully sorted. ∎


HOW: the algorithm (LSD with counting sort per digit)

def lsd_radix(a, k=10):
    if not a: return a
    maxv = max(a)
    exp = 1                       # 1, k, k^2, ...  selects the digit
    while maxv // exp > 0:        # loop runs d times
        a = counting_sort_by_digit(a, exp, k)
        exp *= k
    return a

def counting_sort_by_digit(a, exp, k):
    n = len(a)
    out = [0]*n
    count = [0]*k
    for x in a:                       # 1) tally this digit
        count[(x // exp) % k] += 1
    for i in range(1, k):             # 2) prefix sums -> end positions
        count[i] += count[i-1]
    for x in reversed(a):             # 3) place RIGHT-to-LEFT => STABLE
        d = (x // exp) % k
        count[d] -= 1
        out[count[d]] = x
    return out

WHY the running time is O(d(n+k))O(d(n+k)) (Derivation)

One counting-sort pass costs:

  • tally loop: O(n)O(n)
  • prefix-sum loop: O(k)O(k)
  • placement loop: O(n)O(n)

So one pass =O(n+k)= O(n+k). We do exactly dd passes.

T(n)=dO(n+k)=O(d(n+k))\boxed{T(n) = d \cdot O(n+k) = O\big(d(n+k)\big)}

When is this beating O(nlogn)O(n\log n)? If keys are bounded so dd is constant (e.g. 32-bit ints with k=28k=2^8d=4d=4) and k=O(n)k=O(n), then T=O(n)T=O(n)linear.


MSD vs LSD

LSD MSD
Direction right → left left → right
Structure flat loop, dd passes recursive, divide into buckets
Always examines all dd digits? Yes No — can stop early on distinguishing prefix
Stable? Yes Yes (if buckets kept stable)
Best for fixed-width ints variable-length keys / strings, partial sort
Touches whole array each pass Yes Only the relevant bucket
Figure — Radix sort — LSD, MSD; O(d(n+k))

Worked Example 1 — LSD on [170,45,75,90,802,24,2,66][170, 45, 75, 90, 802, 24, 2, 66], base 10

Pass exp=1 (units digit): digits → 0,5,5,0,2,4,2,6. Stable counting sort gives [170, 90, 802, 2, 24, 45, 75, 66]. Why this step? We sort by the least significant digit first so later passes (which dominate) can rely on stability to preserve this order among ties.

Pass exp=10 (tens digit): digits → 7,9,0,0,2,4,7,6 → [802, 2, 24, 45, 66, 170, 75, 90]. Why this step? Among keys with equal tens digit (170 & 75 both have tens 7) the previous unit order is preserved by stability.

Pass exp=100 (hundreds digit): digits → 8,0,0,0,0,1,0,0 → [2, 24, 45, 66, 75, 90, 170, 802]. ✅ Sorted. Why this step? Max value 802 has 3 digits, so d=3d=3 passes suffice; after the top digit the array is fully ordered.


Worked Example 2 — MSD on strings ["bat","apple","ant","bad"]

Sort by char 0: bucket a = {apple, ant}, bucket b = {bat, bad}. Why this step? The most significant character splits the data; buckets are independent.

Recurse in bucket a on char 1: both have n/p... actually "apple"[1]=p, "ant"[1]=n → ant before apple. Bucket a = [ant, apple]. Why this step? Only these two elements are touched — MSD localizes work.

Recurse in bucket b on char 1: "bat"[1]=a, "bad"[1]=a (tie) → recurse char 2: d < t[bad, bat]. Why this step? We descend only as deep as needed to break ties.

Concatenate: [ant, apple, bad, bat]. ✅


Worked Example 3 — choosing the radix for 32-bit ints

Keys up to 23212^{32}-1. Choose k=28=256k=2^{8}=256 (sort one byte per pass). d=32/8=4d = 32/8 = 4 passes. Cost =4(n+256)=O(n)= 4(n + 256) = O(n) for large nn. Why this step? Bigger kk → fewer passes (d=32/log2kd=\lceil 32/\log_2 k\rceil) but bigger count array. k=256k=256 balances both; the count array (256 ints) is negligible vs nn.


Active Recall

Recall Feynman — explain to a 12-year-old

Imagine sorting a stack of player cards by a 3-digit number. You make 10 trays labelled 0–9. First you drop each card into the tray matching its last digit, then scoop the trays back up in order 0→9 (keeping cards within a tray in the same order). Repeat using the middle digit, then the first digit. After three rounds the cards are magically in order — and you never once compared two cards directly! The trick: always keep ties in the order they were already in (that's "stable"), and the earlier rounds quietly stay sorted.


Flashcards

Radix sort core idea
Sort keys digit-by-digit using a stable bucket (counting) sort per digit, no whole-key comparisons.
Why must the per-digit sort be stable
So that elements equal on the current digit keep the order established by previous (lower) digits, preserving the inductive invariant.
LSD processing direction
Least significant digit first, rightmost → leftmost; flat loop of dd passes.
MSD processing direction
Most significant digit first, leftmost → recurse into each bucket.
Time complexity of radix sort
O(d(n+k))O(d(n+k)) where dd=number of digits, nn=elements, kk=radix.
Space complexity
O(n+k)O(n+k) for the output and count arrays.
Formula for d
d=logk(max key)d = \lceil \log_k(\text{max key}) \rceil.
Cost of one counting-sort pass
O(n+k)O(n+k): tally O(n)O(n) + prefix sum O(k)O(k) + placement O(n)O(n).
When does radix sort run in true O(n)
When dd is constant and k=O(n)k=O(n), i.e. keys are short relative to nn.
Why radix can be worse than quicksort
If keys are huge, d=logk(max)=Θ(logn)d=\log_k(\max)=\Theta(\log n), giving Θ(nlogn)\Theta(n\log n) plus large constants/memory.
LSD vs MSD: which reads all digits
LSD always reads all dd digits; MSD can stop early on a distinguishing prefix.
Best use of MSD
Variable-length keys / strings, or partial sorting where short prefixes separate keys.
Why place elements right-to-left in counting sort
Walking input in reverse with the prefix-sum end indices yields a stable placement.

Connections

  • Counting Sort — the stable engine inside each radix pass.
  • Comparison Sort Lower Bound — the Ω(nlogn)\Omega(n\log n) wall radix sidesteps.
  • Stability in Sorting — the property the correctness proof relies on.
  • Bucket Sort — cousin that distributes by value range instead of by digit.
  • Big-O Notation — interpreting O(d(n+k))O(d(n+k)) and the hidden log\log.
  • Tries — MSD radix sort is essentially building a trie level by level.

Concept Map

escaped by

works via

because it uses

yields

each pass needs

implemented by

pass order

pass order

maintains

justified by

relies on

Comparison sorts bound n log n

Radix sort

No whole-key compares

Exploits digit structure base k

Linear O of d times n plus k

Stable per-digit sort

Counting sort

LSD right to left

MSD left to right

Invariant: sorted by low i digits

Proof by induction

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Dekho, normal sorting (quicksort, mergesort) mein hum do numbers ko compare karte hain "kaun bada?" — aur isi compare wajah se woh kabhi bhi nlognn\log n se tez nahi ho sakte. Radix sort ka jugaad alag hai: woh numbers ko digit ke hisaab se sort karta hai, ek baar mein sirf ek digit. Har digit ke liye ek stable counting sort chalta hai. Stable ka matlab — agar do numbers ka current digit same hai, toh unka pehle wala order waisa ka waisa rehta hai. Yahi stability sab kuch sambhalti hai.

LSD matlab sabse aakhri (units) digit se shuru karo, fir tens, fir hundreds. Har pass ke baad array us digit tak sorted ho jaata hai, aur kyunki sort stable hai, purane passes ka kaam kharab nahi hota. MSD ulta hai — sabse bade (left) digit se shuru, fir har bucket ke andar recursion. MSD ka faayda: strings aur alag-alag length ke keys mein, agar pehla hi letter alag hai toh baaki padhne ki zarurat hi nahi — kaam jaldi khatam.

Time complexity O(d(n+k))O(d(n+k)) hai: dd digits ki ginti, nn elements, kk radix (base). Ek pass O(n+k)O(n+k) ka hota hai aur aise dd passes. Yaad rakho — yeh "always linear" nahi hai. Agar numbers bahut bade ho gaye toh d=logk(max)d=\log_k(\max) bada ho jaata hai aur phir wahi nlognn\log n wapas. Isliye kk ko nn ke aas-paas chuno (jaise 32-bit int ko 1-byte chunks mein, k=256k=256, sirf 4 passes) — tab radix sort sach mein linear aur fast nikalta hai.

Go deeper — visual, from zero

Test yourself — Sorting & Searching

Connections