HardТеория8 min

Counting и Radix Sort

Не-сравнительные сортировки: линейная сложность для целых чисел

Нижняя граница сортировок сравнением

Любая сортировка, основанная на сравнениях, не может быть быстрее O(n log n). Это доказано через дерево решений.

Но если мы знаем что-то о данных (например, это целые числа в ограниченном диапазоне), можно обойти это ограничение.

Counting Sort (сортировка подсчётом)

Идея: подсчитать количество каждого элемента, затем восстановить массив.

Ограничение: работает только для целых неотрицательных чисел в известном диапазоне.

<?php
declare(strict_types=1);

/** @return list<int> */
function countingSort(array $arr): array
{
    if ($arr === []) {
        return [];
    }

    $maxVal = max($arr);
    $count = array_fill(0, $maxVal + 1, 0);

    // Count occurrences
    foreach ($arr as $num) {
        $count[$num]++;
    }

    // Reconstruct
    $result = [];
    for ($num = 0; $num <= $maxVal; $num++) {
        for ($i = 0; $i < $count[$num]; $i++) {
            $result[] = $num;
        }
    }

    return $result;
}

// [4, 2, 2, 8, 3, 3, 1]
// count = [0, 1, 2, 2, 1, 0, 0, 0, 1]
//          0  1  2  3  4  5  6  7  8
// result = [1, 2, 2, 3, 3, 4, 8]
### Стабильная версия Counting Sort
<?php
declare(strict_types=1);

/** @return list<int> */
function countingSortStable(array $arr): array
{
    if ($arr === []) {
        return [];
    }

    $maxVal = max($arr);
    $count = array_fill(0, $maxVal + 1, 0);

    foreach ($arr as $num) {
        $count[$num]++;
    }

    // Prefix sum for positions
    for ($i = 1; $i <= $maxVal; $i++) {
        $count[$i] += $count[$i - 1];
    }

    // Fill result in reverse order (for stability)
    $n = count($arr);
    $result = array_fill(0, $n, 0);

    for ($i = $n - 1; $i >= 0; $i--) {
        $num = $arr[$i];
        $count[$num]--;
        $result[$count[$num]] = $num;
    }

    return $result;
}
**Сложность:** O(n + k), где k — диапазон значений. Память: O(k).

Radix Sort (поразрядная сортировка)

Идея: сортировать числа поразрядно, начиная с младшего разряда, используя стабильную сортировку (Counting Sort) для каждого разряда.

Массив: [170, 45, 75, 90, 802, 24, 2, 66]

По единицам (1-й разряд):
170, 90, 802, 2, 24, 45, 75, 66

По десяткам (2-й разряд):
802, 2, 24, 45, 66, 170, 75, 90

По сотням (3-й разряд):
2, 24, 45, 66, 75, 90, 170, 802  <- отсортировано!
<?php
declare(strict_types=1);

/** @param list<int> $arr */
function radixSort(array &$arr): void
{
    if ($arr === []) {
        return;
    }

    $maxVal = max($arr);
    $exp = 1; // Current digit place (1, 10, 100, ...)

    while (intdiv($maxVal, $exp) > 0) {
        countingSortByDigit($arr, $exp);
        $exp *= 10;
    }
}

function countingSortByDigit(array &$arr, int $exp): void
{
    $n = count($arr);
    $output = array_fill(0, $n, 0);
    $count = array_fill(0, 10, 0); // Digits 0-9

    // Count by current digit
    foreach ($arr as $num) {
        $digit = intdiv($num, $exp) % 10;
        $count[$digit]++;
    }

    // Prefix sum
    for ($i = 1; $i < 10; $i++) {
        $count[$i] += $count[$i - 1];
    }

    // Fill output (in reverse order for stability)
    for ($i = $n - 1; $i >= 0; $i--) {
        $digit = intdiv($arr[$i], $exp) % 10;
        $count[$digit]--;
        $output[$count[$digit]] = $arr[$i];
    }

    // Copy back
    $arr = $output;
}
**Сложность:** O(d * (n + k)), где d — количество разрядов, k = 10 для десятичных. Для n чисел в диапазоне [0, n^c]: O(c * n) = **O(n)**.

Визуализация Radix Sort

Исходный: [329, 457, 657, 839, 436, 720, 355]

Разряд единиц (exp=1):
Bucket 0: [720]
Bucket 5: [355]
Bucket 6: [436]
Bucket 7: [457, 657]
Bucket 9: [329, 839]
-> [720, 355, 436, 457, 657, 329, 839]

Разряд десятков (exp=10):
Bucket 2: [720, 329]
Bucket 3: [436, 839]
Bucket 5: [355, 457, 657]
-> [720, 329, 436, 839, 355, 457, 657]

Разряд сотен (exp=100):
Bucket 3: [329, 355]
Bucket 4: [436, 457]
Bucket 6: [657]
Bucket 7: [720]
Bucket 8: [839]
-> [329, 355, 436, 457, 657, 720, 839]

Bucket Sort (для полноты)

Распределяем элементы по «корзинам», сортируем каждую, объединяем.

<?php
declare(strict_types=1);

/** @return list<float> */
function bucketSort(array $arr): array
{
    if ($arr === []) {
        return [];
    }

    $n = count($arr);
    $minVal = min($arr);
    $maxVal = max($arr);

    if ($minVal === $maxVal) {
        return $arr;
    }

    // Create buckets
    $bucketCount = $n;
    $bucketRange = ($maxVal - $minVal) / $bucketCount;
    $buckets = array_fill(0, $bucketCount + 1, []);

    foreach ($arr as $num) {
        $idx = (int) (($num - $minVal) / $bucketRange);
        $buckets[$idx][] = $num;
    }

    // Sort each bucket and merge
    $result = [];
    foreach ($buckets as &$bucket) {
        sort($bucket); // Insertion sort for small buckets
        foreach ($bucket as $val) {
            $result[] = $val;
        }
    }

    return $result;
}
**Сложность:** O(n) в среднем при равномерном распределении, O(n²) в худшем.

Сравнение не-сравнительных сортировок

Алгоритм Время Память Стабильная Ограничения
Counting Sort O(n + k) O(k) Да Целые числа, малый k
Radix Sort O(d * (n + k)) O(n + k) Да Целые числа
Bucket Sort O(n) средн. O(n + k) Да Равномерное распределение

Общая таблица всех сортировок

Алгоритм Лучший Средний Худший Память Стабильная
Bubble Sort O(n) O(n²) O(n²) O(1) Да
Selection Sort O(n²) O(n²) O(n²) O(1) Нет
Insertion Sort O(n) O(n²) O(n²) O(1) Да
Merge Sort O(n log n) O(n log n) O(n log n) O(n) Да
Quick Sort O(n log n) O(n log n) O(n²) O(log n) Нет
Heap Sort O(n log n) O(n log n) O(n log n) O(1) Нет
Counting Sort O(n+k) O(n+k) O(n+k) O(k) Да
Radix Sort O(dn) O(dn) O(dn) O(n+k) Да

Запомни: Counting Sort = O(n+k), идеален когда k (диапазон) невелик. Radix Sort = O(dn), для чисел с фиксированным количеством разрядов. Оба обходят нижнюю границу O(n log n) потому что НЕ основаны на сравнениях. На интервью: если данные — целые числа в известном диапазоне, предложи Counting Sort.

Итоги

  1. Counting Sort: O(n+k), для малых целых чисел
  2. Radix Sort: O(d*n), сортирует поразрядно через Counting Sort
  3. Обе обходят O(n log n) за счёт знаний о данных
  4. Counting Sort — основа для Radix Sort (стабильная)
  5. На практике: для строк, дат, IP-адресов — Radix Sort

Проверь себя

Массив [329, 457, 657, 839, 436, 720, 355]. Каков результат после первого прохода Radix Sort (сортировка по разряду единиц)?

Counting Sort для массива из n элементов в диапазоне [0, 1 000 000]. Что произойдёт?

У вас 10 миллионов целых чисел в диапазоне [0, 999]. Какой алгоритм сортировки оптимален?

Почему Radix Sort сортирует начиная с МЛАДШЕГО разряда (LSD), а не со старшего?

Почему Counting Sort и Radix Sort могут работать быстрее O(n log n)?