2.2.4.1 — quicksort, которому больше не нужна вторая половина2.2.4.1 — quicksort that no longer needs the other half

Quickselect

Quicksort (2.2.1.4) после partition рекурсивно сортирует обе половины — но если нужен только k-й наименьший элемент, а не полный порядок, после partition достаточно понять, в какой из двух половин лежит k, и рекурсивно спускаться только туда, полностью отбрасывая вторую половину без какой-либо обработки.Quicksort (2.2.1.4) recursively sorts both halves after partitioning — but if only the k-th smallest element is needed, not the full order, after partitioning it's enough to determine which half contains k and recurse only into that one, discarding the other half entirely with no further processing.

avg O(n) worst O(n²) in-place

Идея и инвариантIdea & invariant

Поддерживается диапазон [lo, hi], про который известно: искомый k-й (по глобальному индексу) элемент лежит внутри него. На каждом шаге выполняется тот же partition, что и в quicksort — выбирается pivot, массив переставляется так, что все элементы меньше pivot оказываются слева от него, все большие — справа, и pivot встаёт на свою финальную позицию p. Дальше сравнивается p с k: если p == k, pivot и есть искомый элемент — поиск завершён; если k < p, ответ гарантированно слева, и рекурсия (или следующая итерация цикла) идёт только в [lo, p-1]; если k > p, ответ справа, [p+1, hi]. Вторая половина не трогается вообще — ни рекурсии, ни какой-либо дальнейшей обработки над ней не происходит.A range [lo, hi] is maintained, about which it's known: the (globally indexed) k-th element lies inside it. Each step runs the same partition as quicksort — a pivot is chosen, the array is rearranged so every element less than the pivot ends up to its left and every larger one to its right, and the pivot lands at its final position p. Then p is compared to k: if p == k, the pivot is the answer — search over; if k < p, the answer is guaranteed to be on the left, and recursion (or the next loop iteration) goes only into [lo, p-1]; if k > p, the answer is on the right, [p+1, hi]. The other half isn't touched at all — no recursion, no further processing of any kind happens on it.

искомое k=2 (0-индексация), массив длины 8 меньше pivot p=5 больше pivot — отброшено целиком k=2 < p=5 — рекурсия только в [0, 4], правая часть больше не читается каждый шаг отбрасывает часть массива без обработки — как бинарный поиск, но с partition вместо сравнения

quicksort рекурсирует в обе стороны; quickselect — только в ту, что содержит k, отсюда и ускорение в среднем до O(n)quicksort recurses both ways; quickselect only into the side containing k, hence the average speedup to O(n)

int quickselect(std::vector<int>& a, int k) {
    int lo = 0, hi = (int)a.size() - 1;
    while (true) {
        if (lo == hi) return a[lo];
        int pivot_idx = lo + rand() % (hi - lo + 1);  // случайный pivot против adversarial входа// random pivot against adversarial input
        int p = partition(a, lo, hi, pivot_idx);       // тот же partition, что в quicksort// the same partition as quicksort
        if (k == p) return a[k];
        else if (k < p) hi = p - 1;  // ответ слева — правая часть больше не нужна// answer is on the left — the right side is no longer needed
        else lo = p + 1;             // ответ справа — левая часть больше не нужна// answer is on the right — the left side is no longer needed
    }
}

Демонстрация вживуюLive demonstration

Оранжевая рамка — текущий рабочий диапазон [lo, hi]. Затемнённая часть — уже отброшенная сторона, которая больше никогда не будет прочитана. Жёлтый столбик — pivot после partition; зелёный — найденный k-й элемент.The orange outline is the current working range [lo, hi]. The dimmed part is the already-discarded side, never read again. The yellow bar is the pivot after partitioning; green is the found k-th element.

сравнений:comparisons: 0

нажмите «Вперёд» или «Играть», чтобы начатьpress "Forward" or "Play" to begin

Почему в среднем O(n), а не O(n log n)Why average case is O(n), not O(n log n)

В quicksort суммарная стоимость всех partition на одном "уровне" рекурсии — O(n), а уровней в среднем O(log n), что даёт O(n log n). В quickselect работа выполняется только над одной убывающей веткой: partition на диапазоне размера n стоит O(n), но следующий диапазон в среднем сжимается в константу раз (например, вдвое при удачном pivot), поэтому суммарная стоимость — геометрическая прогрессия n + n/2 + n/4 + ... = O(n) (сумма сходится к 2n). Именно отбрасывание "лишней" рекурсии, а не более быстрый partition, даёт переход от O(n log n) к O(n).In quicksort, the total cost of all partitions at one recursion "level" is O(n), and there are O(log n) levels on average, giving O(n log n). In quickselect, work happens only along one shrinking branch: partitioning a range of size n costs O(n), but the next range on average shrinks by a constant factor (e.g. halves with a lucky pivot), so the total cost is the geometric series n + n/2 + n/4 + ... = O(n) (the sum converges to 2n). It's discarding the "extra" recursion, not a faster partition, that produces the drop from O(n log n) to O(n).

Худший случай остаётся Θ(n²) по той же причине, что у quicksort: неудачный (например, детерминированный) выбор pivot на adversarial входе может каждый раз отсекать только один элемент, оставляя диапазон размера n-1, n-2, ... — сумма n + (n-1) + (n-2) + ... = Θ(n²). Решение той же природы, что и в 2.2.1.4 — случайный выбор pivot делает такой сценарий статистически невозможным, но не устраняет его как теоретическую возможность; полное устранение худшего случая — тема следующей статьи, медианы медиан (2.2.4.2).The worst case remains Θ(n²) for the same reason as quicksort: an unlucky (e.g. deterministic) pivot choice on adversarial input can cut off just one element each time, leaving ranges of size n-1, n-2, ... — the sum n + (n-1) + (n-2) + ... = Θ(n²). The fix is the same as in 2.2.1.4 — random pivot selection makes this scenario statistically impossible, but doesn't eliminate it as a theoretical possibility; fully eliminating the worst case is the topic of the next article, median of medians (2.2.4.2).

Где встречаетсяWhere it shows up

01

numpy.partition и аналогичные функции без полной сортировкиand similar no-full-sort functions

Многие численные библиотеки предоставляют partition/quickselect как отдельную, более быструю операцию, отличную от полной сортировки — именно потому, что задачи вроде "найти медиану массива" или "топ-k элементов" не требуют O(n log n).Many numerical libraries expose partition/quickselect as a separate, faster operation distinct from full sorting — precisely because tasks like "find the array's median" or "top-k elements" don't require O(n log n).

02

Быстрое вычисление медианы в статистических пайплайнахFast median computation in statistics pipelines

Вычисление медианы (k = n/2) — самый частый частный случай quickselect; при многократном вычислении медианы на больших наборах данных разница между O(n) и O(n log n) становится заметной на практике, а не только асимптотически.Computing the median (k = n/2) is quickselect's most common special case; with repeated median computation over large datasets, the difference between O(n) and O(n log n) becomes noticeable in practice, not just asymptotically.