Алгоритм сортировки подсчетом (Counting Sort): принцип работы
Время работы: O(N + K), где N - количество элементов, а K - диапазон значений.
Идея метода сортировки
Существует математически доказанная теорема: любой алгоритм сортировки, основанный на сравнениях, не может работать быстрее, чем за O(N log N) в худшем случае. Это фундаментальное ограничение.
Но что, если не сравнивать элементы вообще? Сортировка подсчетом обходит это ограничение, используя сами сортируемые значения как индексы вспомогательного массива.
Алгоритм работы
- Шаг 1. Создаем вспомогательный массив
countразмером K (где K - максимальное возможное значение элемента), заполняем его нулями. - Шаг 2. Проходим по исходному массиву. Встретив число
x, увеличиваемcount[x]на единицу. - Шаг 3. Проходим по массиву
countслева направо. Еслиcount[x] = 3, мы записываем числоxтри раза подряд в исходный массив, двигая указатель.
На схеме ниже можно посмотреть, как сортировка подсчетом находит правильные позиции чисел вообще без сравнений - только за счет счетчиков.
Тренажер: алгоритм сортировки подсчетом
Отсортируем 3 1 4 1 5 2 подсчетом (диапазон K = 5).
Ограничения алгоритма
Сортировка подсчетом кажется идеальной (линейное время!), но у нее есть строгие ограничения:
- Требует памяти O(K): Если нужно отсортировать массив из пяти чисел
[1, 1000000, 5, 2, 8], нам придется создать вспомогательный массив размером в миллион элементов! Алгоритм эффективен, только когда K сопоставимо с N. - Только целые неотрицательные числа: Алгоритм напрямую использует значения как индексы массива, поэтому он не работает с дробными числами (float). Отрицательные числа можно поддержать, найдя минимум и сделав сдвиг на это значение, но это требует дополнительных операций.
Реализация на 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), так как он пересоздает элементы с нуля. Если нам нужно сортировать сложные объекты (например, студентов по оценкам) с сохранением их изначального порядка при равенстве оценок, алгоритм усложняется: массив счетчиков трансформируют в массив "префиксных сумм", а элементы расставляются с конца. Эта версия используется как составная часть Поразрядной сортировки (Radix Sort).
Коротко: главное
- Сортировка подсчетом не использует сравнение элементов друг с другом.
- Алгоритм работает за линейное время O(N + K), где K - диапазон значений.
- Для работы требуется вспомогательный массив размером K, что делает алгоритм неприменимым для чисел с огромным разбросом.
- В базовом виде сортировка применима только к целым неотрицательным числам.
- Устойчивая версия алгоритма с префиксными суммами используется как составная часть Radix Sort.
Проверьте себя
В каком случае Сортировка подсчетом будет работать крайне неэффективно (или даже приведет к ошибке нехватки памяти)?