We are optimizing the quicksort partitioning step in a low-latency trading engine where arrays contain a huge number of duplicate keys (e.g. 0s, 1s, and 2s representing order statuses).
Standard Lomuto or Hoare partitioning degrades to O(N^2) when all elements are duplicates. Dijkstra’s Dutch National Flag (3-way partition) groups elements into [< pivot, == pivot, > pivot] in strict O(N) time and O(1) space. What is the clean pointer invariant, and why do we not increment the middle pointer when swapping with high?
The Dutch National Flag algorithm (invented by Edsger W. Dijkstra) is the secret weapon that makes 3-way QuickSort resilient against duplicate keys.
1. The 3 Pointer Invariant
We divide the array into 4 distinct regions using 3 pointers:
low,mid, andhigh:Initially,
low = 0,mid = 0, andhigh = n - 1. The entire array is initially inside theUNKNOWNregion.2. The 3 State Transitions
While
mid <= high, inspectnums[mid]:nums[mid] == 0: Swapnums[low]withnums[mid]. Increment BOTHlow++andmid++.Why can we increment
midhere? Because whatever was sitting atlowwas already processed (it was guaranteed to be a1).nums[mid] == 1: It’s already in the right spot! Just incrementmid++.nums[mid] == 2: Swapnums[mid]withnums[high]. Decrementhigh--.THE CRITICAL CATCH: Do NOT increment
midhere! Whatever came fromhighwas unknown—it might be a0, a1, or another2! We must inspect it on the next loop iteration!Clean Python 3.12 Implementation
Complexity Breakdown
O(N). In every single step, eithermidincreases orhighdecreases. The unknown window(high - mid)strictly shrinks to zero in at mostNsteps.O(1). No extra memory allocated.Here is the in-place C++20 Dutch National Flag algorithm. Notice that
std::swapcompiles to a singleXCHGor register mov instruction on x86_64.Complexity: Single pass
O(N)withO(1)space.