Quicksort variant that selects the pivot using Tukey's ninther, the median of three medians of three.
Quicksort ninther improves pivot selection by using a stronger sampling strategy called Tukey’s ninther. Instead of taking the median of three elements, it takes the median of three medians, each computed from three elements.
This increases the likelihood of selecting a pivot close to the true median, which leads to more balanced partitions.
Problem
Given an array of length , reorder it such that:
Idea
Select nine elements from the range and group them into three triples:
Compute the median of each triple:
Then choose the pivot as:
This is called the ninther.
Algorithm
ninther_quicksort(A, lo, hi):
if lo >= hi:
return
pivot_index = ninther(A, lo, hi)
swap A[pivot_index] and A[hi]
p = partition(A, lo, hi)
ninther_quicksort(A, lo, p - 1)
ninther_quicksort(A, p + 1, hi)ninther(A, lo, hi):
step = (hi - lo) // 8
i1 = lo
i2 = lo + step
i3 = lo + 2 * step
i4 = lo + 3 * step
i5 = lo + 4 * step
i6 = lo + 5 * step
i7 = lo + 6 * step
i8 = lo + 7 * step
i9 = hi
m1 = median_of_three(A, i1, i2, i3)
m2 = median_of_three(A, i4, i5, i6)
m3 = median_of_three(A, i7, i8, i9)
return median_of_three(A, m1, m2, m3)Example
Let:
Divide into triples:
- [9,1,8] → median 8
- [2,7,3] → median 3
- [6,4,5] → median 5
Final pivot:
Partition around :
Correctness
The ninther method changes only pivot selection. Partitioning and recursion follow standard quicksort rules, so correctness remains unchanged.
Complexity
| case | time |
|---|---|
| best | |
| average | |
| worst |
The probability of highly unbalanced partitions is significantly reduced compared to simple pivot selection.
Space:
| case | space |
|---|---|
| average | |
| worst |
Properties
| property | value |
|---|---|
| stable | no |
| in-place | yes |
| pivot quality | high |
| overhead | higher than median of three |
Notes
Ninther improves partition balance and reduces recursion depth variance. It is particularly useful for large arrays where pivot quality strongly affects performance.
However, it introduces extra comparisons during pivot selection, so it is usually used only for sufficiently large subarrays.
Implementation
def ninther_quicksort(a):
def sort(lo, hi):
if lo >= hi:
return
pivot_index = ninther(lo, hi)
a[pivot_index], a[hi] = a[hi], a[pivot_index]
p = partition(lo, hi)
sort(lo, p - 1)
sort(p + 1, hi)
def median_of_three(i, j, k):
vals = [(a[i], i), (a[j], j), (a[k], k)]
vals.sort()
return vals[1][1]
def ninther(lo, hi):
step = max((hi - lo) // 8, 1)
i1 = lo
i2 = lo + step
i3 = lo + 2 * step
i4 = lo + 3 * step
i5 = lo + 4 * step
i6 = lo + 5 * step
i7 = lo + 6 * step
i8 = lo + 7 * step
i9 = hi
m1 = median_of_three(i1, i2, i3)
m2 = median_of_three(i4, i5, i6)
m3 = median_of_three(i7, i8, i9)
return median_of_three(m1, m2, m3)
def partition(lo, hi):
pivot = a[hi]
i = lo
for j in range(lo, hi):
if a[j] <= pivot:
a[i], a[j] = a[j], a[i]
i += 1
a[i], a[hi] = a[hi], a[i]
return i
sort(0, len(a) - 1)
return afunc NintherQuickSort(a []int) {
var sort func(int, int)
sort = func(lo, hi int) {
if lo >= hi {
return
}
pivotIndex := ninther(a, lo, hi)
a[pivotIndex], a[hi] = a[hi], a[pivotIndex]
p := partitionNinther(a, lo, hi)
sort(lo, p-1)
sort(p+1, hi)
}
sort(0, len(a)-1)
}
func medianOfThreeIdx(a []int, i, j, k int) int {
if a[i] > a[j] {
i, j = j, i
}
if a[j] > a[k] {
j, k = k, j
}
if a[i] > a[j] {
i, j = j, i
}
return j
}
func ninther(a []int, lo, hi int) int {
step := (hi - lo) / 8
if step == 0 {
step = 1
}
i1 := lo
i2 := lo + step
i3 := lo + 2*step
i4 := lo + 3*step
i5 := lo + 4*step
i6 := lo + 5*step
i7 := lo + 6*step
i8 := lo + 7*step
i9 := hi
m1 := medianOfThreeIdx(a, i1, i2, i3)
m2 := medianOfThreeIdx(a, i4, i5, i6)
m3 := medianOfThreeIdx(a, i7, i8, i9)
return medianOfThreeIdx(a, m1, m2, m3)
}
func partitionNinther(a []int, lo, hi int) int {
pivot := a[hi]
i := lo
for j := lo; j < hi; j++ {
if a[j] <= pivot {
a[i], a[j] = a[j], a[i]
i++
}
}
a[i], a[hi] = a[hi], a[i]
return i
}