CS 411 Fall 2025 > Outline & Supplemental Notes for October 1, 2025
CS 411 Fall 2025
Outline & Supplemental Notes
for October 1, 2025
Outline
Quicksort [L 5.2]
- Idea
- The Hoare Partition algorithm
- Implement Quicksort
- Analyze
- Optimizing Quicksort (see Supplemental Notes)
Supplemental Notes
Optimizing Quicksort
Introduction
Quicksort has an excellent average-case time, but it is poor in most other ways:
- Unoptimized, its worst performance is on nearly sorted data.
- It has linear maximum recursion depth.
- Its time efficiency is quadratic: \(\Theta(n^2)\).
- It is not stable.
- Implementations generally require random-access data.
We are stuck with the last two, but the first three can be dealt with, using some of the same optimizations we covered for Quickselect.
Note that, as with Quickselect, there is a tradition that Quicksort’s worst-case inefficiency can be ignored, since it is “rare”. This is not the way to think. See my notes on Quickselect for details.
Pivot Choice: Median-of-Three
As with Quickselect, we can greatly improve performance on nearly sorted data by choosing the pivot more wisely. The standard method is median-of-three. With this method, there are still killer sequences, but they are not nearly sorted.
Tail-Recursion Elimination
Quicksort makes two recursive calls. We do the one handling the larger part of the data last, and do tail-recursion elimination on it. This leaves one actual recursive call, but this call always handles less than half of the original list. Thus we go from linear recursion depth to logarithmic recursion depth.
Note that the recursion is the only significant source of additional-space usage in Quicksort; the partition is generally in-place. Thus, our additional space requirements go from linear to logarithmic as well.
Introsort
As with Quickselect, Musser’s introspection idea works well with Quicksort. We keep track of the recursion depth—with eliminated tail calls counted, too. If the depth exceeds a chosen limit—Musser recommends \(2\log_2 n\)—then we switch to an algorithm with a better worst case. The usual alternate algorithm is Heap Sort, a sorting algorithm with a \(\Theta(n\log n)\) worst-case time, and \(\Theta(1)\) additional space usage. We will cover Heap Sort later in the semester.
The resulting algorithm is called Introsort. Its worst case time is \(\Theta(n\log n)\).
When the Introsort optimization is used, the actual recursion depth never exceeds our chosen limit (\(2\log_2 n\)?), which means the additional space usage is \(\Theta(\log n)\) as well. Thus, while we still do tail-recursion elimination on one of the two recursive calls, it is no longer important which call we eliminate.
Final Insertion Sort Pass
Quicksort handles small sublists last. (This is in contrast to Merge Sort, which handles them first.) We can modify Quicksort—or Introsort—so that, when a recursive call is made to sort a small sublist, we simply return. Then, when all the calls return, the list is not sorted. However, it is nearly sorted. We finish the job by calling Insertion Sort, which is very efficient on nearly sorted data.
This optimization does not change the order of anything; it simply makes the algorithm a bit faster, we hope.
There is a bit of a mystery (to me) in this optimization. It has been known since at least 1997, and perhaps much earlier, that the Insertion-Sort-pass optimization tends to have an adverse effect on Quicksort’s cache hits. Thus, the wider the gap between cache performance and main memory performance, the worse this optimization becomes, to the point where it may be better not to do it.
In recent years, the cache friendliness of algorithms
has become important.
It seems to me that this optimization should have faded away.
And yet apparently it has not.
For example, in the most recent version
(as of October 1, 2025)
of the GNU C++ Library,
the code for std::sort
,
implemented using Introsort,
still does the Insertion-Sort-pass optimization.
I cannot explain this.
Summary
The table below summarizes the four Quicksort optimizations we have covered.
Before | Optimization | After |
---|---|---|
Worst performance is on (nearly) sorted data. | Pivot selection via median-of-three, or other smart method. | Performance on nearly sorted data same as average case. |
Recursion depth and space usage \(\Theta(n)\). | Tail-recursion elimination on larger of two recursive calls. | Recursion depth and space usage \(\Theta(\log n)\). |
Worst case time \(\Theta(n^2)\). | Introspection: track recursion depth: both real and tail-call-eliminated. If depth exceeds \(2\log_2 n\), then switch to Heap Sort for current sublist. | Introsort. Worst case time \(\Theta(n \log n)\). |
Recursive calls to sort small sublists do nothing. Finish entire sort with a single Insertion Sort pass. | Faster? |