Занятие 21. Сортировки за n log n
Занятие 19 закончилось приговором: алгоритмы, переставляющие только соседей, платят Inv(a), то есть в среднем $\sim n^2/4$. Выход — дальнобойные перестановки: одно перемещение через полмассива гасит тысячи инверсий разом. Сегодня два классических способа делать это за $n \log n$ — фон Нейман (1945) и Хоар (1959), оба на каркасе «разделяй и властвуй» из занятия 6. Обозначения — как в весеннем конспекте сортировок; замеры реальные (Apple M-серия, clang++ -O2).
Две симметричные философии: merge sort делит тупо (пополам), а трудится при сборке; quicksort делит умно (по пивоту), и вся работа происходит при делении.
Сортировка слиянием
Дерево — то самое из занятия 6: $T(n) = 2\,T(n/2) + \Theta(n)$, случай $c = \log_b a$ мастер-теоремы, итого $\Theta(n \log n)$ на любом входе — у merge sort нет худшего случая.
merge: два указателя из занятия 12
На каждом шаге забираем меньшую из двух голов — метод двух указателей, Θ(n). Стабильность спрятана в одном символе: сравнение a[i] > a[j] строгое, поэтому при равных берём слева — порядок равных элементов не рвётся (занятие 19 объясняло, зачем это нужно). Плата — буфер: сливать на месте за Θ(n) нельзя.
Замер — и бонус, закрывающий звёздочку
746 мс на $10^7$ — против ~8 часов экстраполяции для вставок: $n \log n$ против $n^2$ в полный рост. И бонус: тот же каркас считает инверсии за $n \log n$ — при взятии элемента из правой половины он меньше всех оставшихся слева, значит inv += m - i разом:
Звёздочка из ДЗ занятия 19 закрыта.
Быстрая сортировка
Идея Хоара — и школьная версия
Выбираем пивот, раскладываем всё на три части «меньше — равно — больше», рекурсивно сортируем края; середина уже стоит на месте — окончательно, как элемент у сортировки выбором, только сразу вся кучка равных.
Честно и понятно, но три вектора на каждом уровне: замер — 2114 мс на $10^7$.
In-place партиция: инвариант трёх зон
Каждый шаг цикла сохраняет инвариант: если a[j] < p — обмен расширяет зелёную зону, иначе жёлтая растёт сама. Доказательство корректности — привычная схема занятия 12: инициализация, сохранение, завершение.
Средний случай
Случайный пивот в среднем делит массив в пропорции не хуже 1:3 — глубина $O(\log n)$, на каждом уровне суммарно Θ(n) работы. In-place быстрее школьной версии в 3.5 раза: ноль аллокаций, кэш доволен (занятие 8). Нестабильна: дальние свопы партиции рвут порядок равных — тот же механизм, что у сортировки выбором.
Худший случай: два killer-входа
Killer № 1: наивный пивот + отсортированный вход
Пивот-максимум отрезает по одному элементу: $T(n) = T(n-1) + \Theta(n)$ — знакомый квадрат, плюс глубина рекурсии $n$ с угрозой переполнения стека (занятие 5). Отсортированный вход — не экзотика, а самый частый вход в продакшене; наивный quicksort квадратичен ровно там, где данных больше всего. Случайный пивот делает худший случай астрономически маловероятным — и защищает даже от злонамеренно подобранного входа.
Killer № 2: все элементы равны
Сюрприз: случайность не спасает — при равных элементах никто не «меньше p», левая зона не растёт, партиция вновь отрезает по одному. А «наивная» школьная версия здесь линейна: трёхчастное разбиение отправляет всех в кучку «равно», рекурсии достаются пустяки. Мораль: «оптимизированный» и «правильный» — разные оси; взрослое лекарство — трёхчастная in-place партиция (задача голландского флага, звёздочка в ДЗ). Дубликаты — норма жизни: возрасты, оценки, статусы.
Три лекарства — и страховка std::sort
- Случайный пивот — худший случай становится невоспроизводимым; цена — вызов генератора на партицию.
- Медиана трёх (первый, средний, последний) — дёшево, детерминированно, убивает «отсортированный» killer; от подобранного входа не защищает.
- Страховка introsort — считать глубину рекурсии; глубже $2 \log n$ — переключиться на heapsort с гарантией $n \log n$. Так
std::sortобещает $n \log n$ всегда (занятие 19 упоминало эту конструкцию — теперь видно, зачем она).
Инженерный паттерн шире сортировок: быстрый в среднем алгоритм + дешёвый детектор беды + медленный, но гарантированный запасной.
Сводный замер
Наши честные реализации — в 4 раза медленнее std::sort: у него база-вставки на коротких кусках (занятие 19), медиана трёх и отточенные ветвления; асимптотика та же, константа вылизана десятилетиями.
| вставками (з. 19) | слиянием | быстрая | std::sort | |
|---|---|---|---|---|
| лучший | n | n log n | n log n | n |
| средний | n + Inv(a) | n log n | n log n | n log n |
| худший | n² | n log n | n² | n log n |
| память | O(1) | O(n) | O(log n) стек | O(log n) |
| стабильность | да | да | нет | нет* |
| ниша | мало беспорядка, база | гарантии, списки, инверсии | среднее, in-place | по умолчанию |
* нужен порядок равных — std::stable_sort (merge-семейство). Сквозная мысль: merge продаёт память за гарантию, quick продаёт гарантию за память — introsort покупает обе, комбинируя три алгоритма.
Задачи семинара
-
Дерево слияний. Прогоните mergeSort на {5, 2, 4, 6, 1, 3}: дерево рекурсии и массив после каждого merge. Сколько всего сравнений? Сравните с $n \log_2 n$.
-
Партиция под микроскопом. Вход {3, 8, 2, 5, 1, 4}, пивот 4 (уже в конце). Таблица i, j и зон после каждого шага; где окажется пивот?
-
Сломайте медиану трёх. Постройте вход из 7 элементов, где пивот-медиана(первый, средний, последний) делит массив хуже, чем 1:5.
Домашнее задание (сдача через Git)
- Обе сортировки со счётчиками сравнений; сверьте счётчики с $n \log_2 n$ на $n = 10^3, 10^4, 10^5$.
- Инверсии за n log n через merge — та самая звёздочка занятия 19; стресс-тест против наивного $O(n^2)$.
- Killer-лаборатория: таблица времён вашего quicksort на четырёх входах (random / sorted / reversed / все равны) × двух пивотах (последний / случайный); объясните каждую клетку.
- Стабильность: покажите на парах (ключ, номер), что ваш merge стабилен, а quicksort — нет; укажите строку кода, отвечающую за каждое.
- * Трёхчастная партиция (задача голландского флага) in-place:
< p | = p | > p; замерьте на массиве из 10 различных значений против обычной партиции.
Следующее занятие — move-семантика: lvalue и rvalue, ссылки со «вторым амперсандом», std::move и куда на самом деле «переезжает» вектор. А сортировки без сравнений — k-я статистика, нижняя оценка Ω(n log n), counting и radix — вернутся на семинаре 23.