Алгоритмы сортировки: пузырьком, слиянием, быстрая и другие
Основы3 минРедакция Innovialabs
Коротко
Простые сортировки (пузырьком, вставками) работают за O(n²) и годятся для малых массивов, а слиянием, быстрая и пирамидальная — за O(n log n); в реальном коде почти всегда лучше встроенная сортировка языка.
Короткий ответ
Сортировка — это упорядочивание элементов по ключу. Алгоритмы различаются тремя свойствами:
- Временная сложность — как растёт время работы с ростом числа элементов n.
- Память — нужен ли дополнительный массив или всё делается на месте (in-place).
- Устойчивость (stability) — сохраняют ли равные элементы исходный порядок.
Для учёбы полезно понимать каждый алгоритм. Для работы почти всегда достаточно встроенной функции sort вашего языка.
Сортировка пузырьком
Проходим по массиву и меняем местами соседние элементы, если они стоят неправильно. После каждого прохода самый большой элемент «всплывает» в конец.
- Сравнить
a[0]иa[1], при необходимости поменять. - Сравнить
a[1]иa[2]и так до конца. - Повторять проходы, пока за проход не было ни одного обмена.
Сложность — O(n²), устойчива. На практике почти не используется: это учебный пример.
Сортировка вставками
Берём элементы по одному и вставляем на правильное место в уже отсортированной левой части — как карты в руке.
def insertion_sort(a):
for i in range(1, len(a)):
key = a[i]
j = i - 1
while j >= 0 and a[j] > key:
a[j + 1] = a[j]
j -= 1
a[j + 1] = key
return a
В худшем случае O(n²), но на почти отсортированных данных близка к O(n). Устойчива и работает на месте. Поэтому её используют внутри гибридных алгоритмов для маленьких кусков.
Сортировка выбором
На каждом шаге ищем минимум в неотсортированной части и ставим его в начало. Всегда O(n²) сравнений, но мало обменов. В обычной реализации неустойчива.
Сортировка слиянием (merge sort)
Принцип «разделяй и властвуй»:
- Делим массив пополам.
- Рекурсивно сортируем каждую половину.
- Сливаем две отсортированные половины в одну, сравнивая их первые элементы.
Всегда O(n log n), устойчива. Минус — нужна дополнительная память O(n). Хорошо подходит для связных списков и внешней сортировки больших файлов.
Быстрая сортировка (quicksort)
- Выбираем опорный элемент (pivot).
- Переставляем элементы: меньшие — влево, большие — вправо.
- Рекурсивно сортируем обе части.
В среднем O(n log n) и очень быстро на практике благодаря работе с памятью. Худший случай — O(n²), если pivot постоянно оказывается крайним (например, на уже отсортированном массиве при выборе первого элемента). Лечится случайным pivot или медианой из трёх. Неустойчива.
Пирамидальная сортировка (heapsort)
Строим из массива двоичную кучу (heap), затем многократно извлекаем максимум. Гарантированно O(n log n), работает на месте, но неустойчива и на практике обычно медленнее quicksort.
Сравнение
| Алгоритм | Средний случай | Худший случай | Доп. память | Устойчивость |
|---|---|---|---|---|
| Пузырьком | O(n²) | O(n²) | O(1) | Да |
| Вставками | O(n²) | O(n²) | O(1) | Да |
| Выбором | O(n²) | O(n²) | O(1) | Нет |
| Слиянием | O(n log n) | O(n log n) | O(n) | Да |
| Быстрая | O(n log n) | O(n²) | O(log n) | Нет |
| Пирамидальная | O(n log n) | O(n log n) | O(1) | Нет |
Что используют встроенные сортировки
Стандартные библиотеки применяют гибридные алгоритмы:
- Timsort — слияние плюс вставки, умеет находить уже упорядоченные участки. Используется в Python и для сортировки объектов в Java. Устойчив.
- Introsort — начинает как quicksort, при слишком глубокой рекурсии переключается на heapsort, а маленькие куски досортировывает вставками. Типичен для
std::sortв C++. - В JavaScript спецификация требует, чтобы
Array.prototype.sortбыла устойчивой; движки обычно используют Timsort.
Частые ошибки
- Писать свою сортировку в продакшене — встроенная почти всегда быстрее и протестирована.
- Забывать про устойчивость при сортировке по нескольким полям.
- Неверный компаратор: в JavaScript
[10, 9, 1].sort()сравнивает элементы как строки. Нужноsort((a, b) => a - b).