Ch.8: How Sorting Beats n log n

Outline

Transcript

0:00 Counting sort, radix sort, and bucket sort are three ways sorting can beat n log n when comparison sorting becomes the bottleneck. The trick is not better comparing. It is using structure the key already exposes. Wait, beating it? That sounds like cheating. Yes. The math is real, and that is why the claim sounds suspicious. But the lower bound comes with a contract. If the only question your algorithm can ask is "is this item less than that item," then yes, the lower bound owns you. The escape hatch is data that lets you ask better questions.

0:31 That is the contract boundary. So the comparison lower bound is really a decision-tree argument. Think of every comparison as a fork. Is A less than B? If yes, go left. If no, go right. At the end, the algorithm has to land on one complete ordering. Yeah. So if there are n distinct items, there are n factorial possible orders, and the tree needs enough leaves to represent all of them. Exactly. And a binary tree with that many leaves has height about n log n. That is the lower bound. Not because sorting is magically expensive, but because pairwise comparisons leak information one yes-or-no bit at a time.

1:09 That is the lesson. From that lower-bound contract, the loophole is not cheating. It is using information that is already sitting inside the value. Right. If the value is a small integer, you do not need to ask whether 7 is less than 12. You can walk straight to bucket 7 and increment a count. Yeah. That is the core move. These non-comparison sorts stop treating values as opaque objects. They exploit structure: bounded ranges, fixed-width digits, or a distribution that lets you split the work into small pieces.

1:42 That is the unlock. Counting sort is the cleanest version. Imagine sorting exam scores from 0 to one hundred. You do not compare one score to another. You make one counter for each possible score. Then you scan the input once. Every time you see an 87, increment counter 87. Every time you see a 92, increment counter 92. At the end, the count array is a histogram of the data. And honestly, that is the part I used to underappreciate. The histogram is already almost the sorted result. You have replaced "which item is smaller" with "how many of each value do I have."

2:19 That is the payoff. From that histogram, reconstructing the sorted output is just walking the counters from low value to high value. For example, if counter 0 says three, emit three zeros. If counter one says none skip it. If counter two says 5, emit 5 twos. Wait, really? That is the whole trick? Exactly. The runtime is O of n plus k. n is the number of records. k is the number of possible values. If k is one hundred and n is 10 million, that is basically linear. The comparison lower bound never gets involved, because we are not comparing pairs.

2:55 That is the shift. But after that linear-time payoff, the range term can quietly become ridiculous. If I have 10 user IDs, and the largest possible ID is 9 billion, I am not allocating 9 billion counters. Oh, wow, no. That is a memory disaster. At that point, k swallows n. Counting sort wins when the key range is dense enough that k stays close to n. Tiny range, huge input: great. Huge sparse range, tiny input: terrible. Right. The hidden cost is not comparison count anymore. It is memory, cache behavior, and walking empty counters that never corresponded to real data. That is the boundary.

3:36 That leads to one extra step that turns counting sort from a toy histogram into a real building block: stability. Oh, right! Same stability from sorting records by timestamp and then customer. Same property. Stable counting sort first converts counts into prefix sums, so each value knows where its block begins and ends in the output. Then it walks the input in order and places each record into the next open slot for that key. So equal keys keep their original order. That sounds like a detail, but it is what lets counting sort become one pass inside a larger sort instead of just a way to sort plain integers.

4:12 So radix is the part people tend to describe like a magic trick. Let's hear it. Radix sort is the one that, Always felt strange to me. Sort by the last digit, then the next digit, and somehow the whole number ends up sorted. It feels weird until you remember, The word stable. Suppose the numbers have three decimal digits. First sort by the ones digit, stably. Then sort by the tens digit, stably. Then sort by the hundreds digit, stably. Because the later pass groups by the bigger digit, but inside each group it preserves the order created by the earlier pass.

4:48 Exactly. The low-order work is not destroyed. It is carried forward. Local digit order accumulates into global key order. Oh, right! Carried forward is the key. The later pass does not start over. It wraps around the smaller ordering that already exists. That is the trick. Now the cost is not free, though. Radix sort is roughly d times n plus k. d is the number of digit passes. k is the alphabet size per pass. So for fixed-width integers, d is bounded. 4 byte passes, or 8 byte passes, depending on the key. That is why it can beat comparison sort.

5:25 Right. But if the keys are long strings with no useful bound, or if the alphabet is huge and sparse, the advantage evaporates. Radix sort did not break big-O. It changed the accounting by using key structure directly. That is the shift. From radix's fixed-width assumption, the idea stops being classroom trivia. Production systems are full of structured keys. For example: IP addresses. Fixed-width timestamps. Numeric IDs. Short status codes. Packed binary fields. Yeah. A generic comparator treats those values like sealed boxes.

6:01 It asks pairwise questions over and over. A non-comparison sort opens the box and uses the bytes, digits, or range directly. I got burned by that default-path mistake once. We had a small fixed-range field and still pushed everything through a comparator because that was the standard pipeline. What did that cost you? Mostly latency and attention. The system worked, but it spent the whole day asking questions the data had already answered. That is the lesson. Now bucket sort uses a different structure.

6:34 Instead of exact counts or fixed digit passes, it spreads values into ranges. Values from 0 to 9 go in bucket 0. 10 to 19 in bucket one. And so on. Then each bucket is small enough to sort cheaply, if the spread is honest. Right. If the distribution is roughly uniform, you have turned one big sorting problem into many tiny ones. The total work can get close to linear because no bucket becomes a second giant sort hiding inside the first one. That is the payoff. But "roughly uniform" is doing a lot of work there.

7:09 It really is. Bucket sort lives or dies on the distribution assumption. If all the values collapse into one bucket, you are back to sorting almost the whole input inside that one bucket, plus you paid the overhead of making all the other buckets. Take a concrete case. The benchmark uses evenly spread random values. Then production traffic is not that polite. Exactly. For example, after launch, real traffic has one hot customer, one hot timestamp range, or one country code dominating the dataset, and the bucket layout slows into one giant bucket wearing a costume.

7:44 That is the production-shaped failure: wishful distribution. That is the lesson. After counting the catches, the built-in comparison sort still wins constantly. It handles arbitrary objects. It does not need a known range. It does not need fixed-width keys. It does not need a friendly distribution. Right. And the code reviewer is right to ask, why are we replacing the built-in sort with a custom thing? It usually has excellent engineering behind it already. Timsort, introsort, stable library sorts, specialized key extraction. You get years of edge cases for free.

8:18 Exactly. Non-comparison sorting is specialization. Powerful specialization, but still specialization. If the structure is real, use it. If the structure is imaginary, you are just writing a slower, more fragile dot sort. That is the boundary. So the practical checklist is: what structure does the key expose? Is the range small and dense? Is the key fixed width? Do you need stability? Can you afford the extra memory? What is the latency tradeoff? And is the distribution assumption true in production, not just in the sample file?

8:52 That last part is what I underline. My first thought is built-in sort too. The checklist keeps that reflex honest. A clever non-comparison sort either uses real data structure, or it is complexity cosplay. Right. That last question is the one that saves you in real systems. The question is not "can I beat n log n." The question is "did the data give me something more useful than comparisons, and do we have a runbook if the custom path gets weird?" That is the decision boundary. So the n log n lower bound is not wrong.

9:24 It is telling you what happens when comparisons are your only source of information. The specialized linear-time family wins only when the data gives you more structure than that. Next time, we look at binary trees, balanced trees, and B-trees: how ordered data stays useful when inserts, deletes, and range scans keep happening. Thanks for listening to Learning Podcasts.