Ch.6: Why Timsort Beats Quicksort in Real Code

Outline

Transcript

0:00 Welcome to Learning Podcasts. Algorithms: sorting, stability, and why Timsort wins. Most engineers think sorting is a solved problem because dot sort exists. It is solved, until the default makes the wrong choice for your data. Right. And the wrong choice is rarely a crash. It is usually a 10x slowdown, or a multi-key pipeline that quietly shuffles records you needed in order. Here is the part most people forget. Big-O says comparison sorts cannot beat n log n comparisons in the general case. Right, the textbook lower bound.

0:36 Fine. But two algorithms with the same big-O can differ by 10x in wall-clock time, and that gap is where the real engineering lives. The constants hide three things, That big-O ignores. How much extra memory the sort needs. Whether it is friendly to the CPU cache. And whether it preserves the order of equal items. Pick the wrong default and one of those three quietly breaks something downstream. Start with the simplest one. Insertion sort is what you do with a hand of playing cards. Walk left to right, and for each new card, slide it backward into its correct position among the cards you have already arranged.

1:16 On paper that sounds slow. And it is, in the worst case. Each new item can shove every earlier item one slot to the right, so the total work is O of n squared. Right. But on small inputs or nearly-sorted inputs, the constant factor is tiny and the inner loop barely moves anything. Which is why every serious sorting library still uses insertion sort somewhere inside, usually for short subarrays where the fancier algorithms are not worth their setup cost. Mergesort is the textbook divide-and-conquer answer.

1:50 Split the array in half. Sort each half by the same logic, recursively. Then merge the two sorted halves into one sorted result. So the recursion does the splitting, and the merge step is where the actual ordering happens. Exactly. The merge walks both sorted halves with two pointers and writes the smaller front element each step. That gives you O of n log n work, guaranteed, no matter what the input looks like. Quicksort goes the other way. Pick one element as the pivot. Partition the array in place so that everything smaller than the pivot ends up on the left, and everything larger ends up on the right.

2:27 And the pivot itself lands in its final position. Right. Then recurse on each side. On average, also O of n log n. But quicksort has a worst case mergesort does not. Pick the smallest or largest element as the pivot every time, and the partitions are wildly unbalanced. You end up doing O of n squared work on a sorted input. I will be honest, I did not think of sorting as an attack vector until I saw a service take 4 seconds on a request because someone fed it a presorted list. So why does anybody still use quicksort?

3:00 On random data, it is faster than mergesort. The next slide is why. Quicksort moves data inside one array. Every swap is local, and the partition walks the array once per level of recursion. The CPU cache loves that. Whole chunks of the array stay hot in cache while quicksort works on them. Mergesort, by contrast, copies between two arrays. Every level of recursion is another full copy of n elements. Even though the big-O is the same, the absolute number of memory writes is higher, and the access pattern is less cache-friendly.

3:32 That is the cache-locality argument in one paragraph. It is exactly the kind of constant-factor difference that big-O is designed to ignore but production systems are not allowed to ignore. The pivot is the whole game in quicksort. A balanced partition gives you log n levels of recursion. A skewed partition gives you n levels and the O of n squared worst case. Real implementations never use a fixed pivot. They randomize, or they take the median of three samples, or they switch strategies based on input size. That is why your language's quicksort is robust against the adversarial inputs that broke the textbook version. Well, that is the part most engineers do not realize they are getting for free.

4:13 The dot sort in your standard library is not a clean textbook quicksort. It is a quicksort plus a sampled pivot, plus an insertion-sort fallback for small subarrays, plus often a switch to heap sort if the recursion gets too deep. Now the property nobody mentions in interviews and everybody hits in production. Stability. Mm-hmm. Define it. A stable sort preserves the original order of records that compare equal. An unstable sort is allowed to shuffle them. Why does that matter? Because real records have multiple keys. Imagine a list of orders, and you want them sorted by customer, then by timestamp.

4:52 With a stable sort, you can do that in two passes. Sort by timestamp first. Then sort by customer. Inside each customer group, the timestamps still come out in order, because the second sort did not disturb their relative position. With an unstable sort, that two-pass trick breaks. The customer-level pass silently scrambles the timestamps inside each customer, and you end up with a multi-key result that is wrong in a way that does not throw any error. Memory is the other axis. Quicksort is in-place.

5:23 It only needs a small amount of extra space for the recursion. Mergesort needs O of n extra space for the merge buffer. Timsort sits in between, with a temporary buffer that is a fraction of the array. On a single laptop sorting a million rows, that difference is invisible. On a streaming pipeline sorting batches of a hundred million records, the choice between in-place and out-of-place is the difference between fitting in RAM and spilling to disk, which is a 100x slowdown right there. Which brings us to Timsort, the algorithm Python and Java actually use for their built-in sort.

5:58 It is not a clean textbook algorithm. It is a hybrid built around one observation: real-world data is rarely random. So real data has runs, and Timsort scans for them first? Exactly. Stretches already sorted or reverse-sorted. It detects those runs, extends short ones using insertion sort, then merges them in a careful order that keeps the buffer small. So you get mergesort's worst-case guarantee, plus insertion sort's speed on small segments. Plus a smarter merge order that exploits structure already in the data.

6:37 So calling dot sort in Python is not running one algorithm. It is running three, blended for whatever the input looks like. The key insight is that "uniformly random input" is mostly a benchmark fiction. Real arrays come from databases, log files, event streams, sensor readings, IDs assigned in order. Those inputs almost always have structure. Timestamps arrive monotonically increasing most of the time. Database rows come back in primary-key order most of the time. User IDs from an autoincrement counter are already sorted.

7:12 Once you notice that order, Timsort can use it instead of pretending the array is random. Right. That is why Timsort tends to beat a clean quicksort on the inputs production code actually sees. One thing that gets lost in big-O analysis is that a single comparison is not free. Every sort calls your comparator function once per pair it inspects, and that is millions of calls for nontrivial inputs. So a slow comparator turns any sort into a slow sort. A function that allocates objects, or pulls a field out of a deep dictionary, or does string parsing inside the compare path, is the actual bottleneck. I have lost an afternoon to a comparator that was hitting JSON parse a million times.

7:55 The fix is usually to precompute the sort key once into a flat array, then sort that. Python's key argument does exactly this. It is almost always the right move when the comparator is doing real work. And then there are the cases where dot sort is the wrong tool entirely. If you only need the top 10 items from a million, sorting the whole array is wasted work. A heap gives you the top K in O of n log K, not O of n log n. Same with nearly-sorted streams. If new items arrive that mostly belong near the end, a small insertion-style update is cheaper than re-sorting the whole array.

8:31 And if you have several already-sorted batches, merging them is much cheaper than concatenating and re-sorting. The pattern is ask whether you actually need a fully sorted array, or whether you only need the top of one, or the order maintained as items arrive. The practical checklist is short. Do you need stability for a multi-key sort? Is memory tight enough that an extra buffer hurts? Is your comparator expensive enough to dominate the runtime? Is the input small or nearly sorted? And do you actually need everything sorted, or just the top few?

9:04 If the answer is just "sort this array of numbers in memory," the built-in is fine and you should not think about it. The moment one of those questions becomes yes, the choice of algorithm becomes a real decision, not a default. Sorting is solved when your data is small, random, and one-key. The moment any of those changes, the right algorithm changes with it. Next chapter, we look at binary search, bisect, and how sorted data unlocks O of log n lookups in places you would not expect. Thanks for listening to Learning Podcasts.