Алгоритм сортировки подсчетом (Counting Sort): принцип работы

Модуль "Сортировки" • урок 12 из 17 •

Суть урока. Существуют алгоритмы сортировки, которые не используют сравнение элементов. Один из них - сортировка подсчетом. Если мы заранее знаем, что все числа в массиве лежат в небольшом диапазоне (от 0 до K), мы можем просто посчитать, сколько раз встречается каждое число, а затем перезаписать исходный массив.

Время работы: O(N + K), где N - количество элементов, а K - диапазон значений.

Идея метода сортировки

Существует математически доказанная теорема: любой алгоритм сортировки, основанный на сравнениях, не может работать быстрее, чем за O(N log N) в худшем случае. Это фундаментальное ограничение.

Но что, если не сравнивать элементы вообще? Сортировка подсчетом обходит это ограничение, используя сами сортируемые значения как индексы вспомогательного массива.

Алгоритм работы

Алгоритм сортировки подсчетом: подсчет частоты каждого значения и перезапись исходного массива
Сортировка подсчетом (Counting Sort): подсчет частоты каждого значения и перезапись исходного массива без попарных сравнений.

На схеме ниже можно посмотреть, как сортировка подсчетом находит правильные позиции чисел вообще без сравнений - только за счет счетчиков.

Тренажер: алгоритм сортировки подсчетом

Отсортируем 3 1 4 1 5 2 подсчетом (диапазон K = 5).

ячейка значение = индекс count собрано в массив
Сравнений: 0 Собрано: 0 / 6 Шаг: 0 / 0
++
Counting Sort
Сортировка подсчетом
Сортировка без единого сравнения: считаем, сколько раз встречается каждое значение, и собираем результат.
Разобрать по шагам в большой версии →

Ограничения алгоритма

Сортировка подсчетом кажется идеальной (линейное время!), но у нее есть строгие ограничения:

Реализация на C

// Функция сортировки подсчетом. 
// k - максимальное значение в массиве (диапазон [0..k])
void CountingSort(int arr[], int n, int k) {
    // Выделяем память под массив счетчиков, инициализируем нулями
    int* count = (int*)calloc(k + 1, sizeof(int));
    
    // Шаг 1: Подсчет частоты каждого элемента
    for (int i = 0; i < n; i++) {
        count[arr[i]]++;
    }
    
    // Шаг 2: Восстановление отсортированного массива
    int index = 0;
    for (int i = 0; i <= k; i++) {
        while (count[i] > 0) {
            arr[index] = i;
            index++;
            count[i]--;
        }
    }
    
    free(count); // Освобождаем память
}
Устойчивая (Stable) версия
Представленный выше вариант не является "устойчивым" (Stable), так как он пересоздает элементы с нуля. Если нам нужно сортировать сложные объекты (например, студентов по оценкам) с сохранением их изначального порядка при равенстве оценок, алгоритм усложняется: массив счетчиков трансформируют в массив "префиксных сумм", а элементы расставляются с конца. Эта версия используется как составная часть Поразрядной сортировки (Radix Sort).

Коротко: главное

  • Сортировка подсчетом не использует сравнение элементов друг с другом.
  • Алгоритм работает за линейное время O(N + K), где K - диапазон значений.
  • Для работы требуется вспомогательный массив размером K, что делает алгоритм неприменимым для чисел с огромным разбросом.
  • В базовом виде сортировка применима только к целым неотрицательным числам.
  • Устойчивая версия алгоритма с префиксными суммами используется как составная часть Radix Sort.

Проверьте себя

В каком случае Сортировка подсчетом будет работать крайне неэффективно (или даже приведет к ошибке нехватки памяти)?

Как вам урок? Спасибо за отзыв!