Quicksort partitions an array around a pivot. A crafted input can force the worst split at every level: one element on one side and everything else on the other. When a standard library sorts untrusted data, this repeated degeneration creates a denial-of-service risk.

Introsort, introduced by David Musser in 1997, keeps quicksort’s partitioning and tracks recursion depth. Once a partition spends its budget of 2⌊log₂ n⌋ levels, it stops recursing and finishes that range with Heap Sort. Small partitions, often around 16 elements, are left partially ordered for one final Insertion Sort pass. This avoids recursive overhead on tiny ranges. Common std::sort implementations and .NET’s Array.Sort use related hybrids, though their depth formulas and small-partition rules differ.

Core condition: quicksort partitioning + a depth counter that hands off to Heap Sort past 2⌊log₂ n⌋ → insertion sort finishes tiny partitions.

Visualization

This compact trace deliberately lowers the depth limit to 1 and the insertion cutoff to 3. Those are illustrative values, not runtime defaults: they keep quicksort, the heap fallback, and a visible insertion cleanup inside nine bars.

Complexity
n
number of elements in the input array

The depth budget is 2⌊log₂ n⌋. Balanced partitions bottom out after about ⌊log₂ n⌋ levels; the factor of two tolerates ordinary imbalance. Reaching the budget means partitions have stayed lopsided level after level — the signature of a run drifting toward O(n²). At that point the current partition is finished with Heap Sort instead of recursing further.

The small-partition cutoff is a separate optimization. Ranges below ~16 elements are left unsorted during recursion; because each such range is bounded by pivots already in their final positions, no element sits more than about 16 slots from where it belongs. A single Insertion Sort pass over the whole array afterward closes those local gaps in near-linear time. Skipping that pass leaves the array unsorted; recursing on the tiny ranges instead pays the recursion overhead the cutoff exists to avoid.

The depth multiplier (2) and the small-partition threshold (~16) are tunable and implementation-specific. Raising the multiplier tolerates deeper imbalance before heap sort intervenes; lowering the cutoff recurses further on small ranges before the final pass. Both shift constant factors and the point where the switch fires; neither changes the O(n log n) asymptotic guarantee, because that guarantee rests on the switch existing, not on its exact threshold.

Introsort does not preserve equal-key order once quicksort partitioning or heap sort runs. A small input may happen to use only the insertion-sort finish, but that does not make the algorithm stable.

Diagram and C# Implementation

References