Занятие 19. Квадратичные сортировки
Первые честные сортировки курса: два алгоритма, у каждого — инвариант, который можно выписать и доказать (занятия 5 и 12 нас к этому готовили). Квадратичные сортировки — не музей: они система отсчёта для n log n ближайшего семинара сортировок, база рекурсии внутри промышленных сортировок и чемпионы на почти отсортированных данных. Замеры реальные (Apple M-серия, clang++ -O2).
Сортировка выбором
Найти минимум остатка — поставить в конец готового префикса — забыть о нём.
Инвариант сильный: готовый префикс — окончательный. Сохранение: минимум остатка не меньше любого элемента префикса, значит, поставить его в конец префикса безопасно. Завершение: $i$ дошло до $n-1$ — массив отсортирован. Схема доказательства — та же, что у бинпоиска в занятии 12.
Портрет: слепой, но экономный на записи
Сравнений — ровно $n(n-1)/2$ на любом входе: 499 500 и на отсортированном, и на перевёрнутом. Алгоритм слеп к порядку — он в любом случае сканирует весь хвост. Зато обменов не больше $n-1$: меньше записей не делает почти никто. Ниша — дорогая запись при дешёвом чтении: флеш-память с износом, огромные объекты с дешёвым сравнением.
Нестабильна: на $[5_1, 5_2, 3, 1]$ первый же обмен уносит $5_1$ за $5_2$: $[1, 5_2, 3, 5_1]$ — дальний swap рвёт порядок равных (почему это важно — занятие 17, каскадные сортировки).
Сортировка вставками
Как раскладывают карты: очередной элемент вдвигается в отсортированную часть на своё место.
Инвариант слабее, чем у выбора: префикс отсортирован, но его элементы ещё подвинутся. Все перемещения — соседние: из этой скромности вырастут и стабильность, и оптимальность.
Портрет: адаптивный и стабильный
Диапазон огромен: от $n-1$ сравнений и нуля сдвигов на отсортированном входе до $n(n-1)/2$ на перевёрнутом. Время зависит от входа — алгоритм адаптивен. Стабилен: меняются местами только соседи, образующие инверсию, — равные элементы никогда не перепрыгивают друг друга. И бонус: элементы можно получать по одному — сортировка «на лету» (инвариант «всё пришедшее отсортировано» держится после каждой вставки).
Инверсии: валюта беспорядка
Инверсия
Пара позиций $i < j$, для которой $a[i] > a[j]$. Массив отсортирован ⟺ инверсий ноль.
Максимум — перевёрнутый массив, все $n(n-1)/2$ пар. Случайный — в среднем половина пар испорчена: $n(n-1)/4$. Проба на $n = 1000$: насчитали 249 152 при теоретических 249 750. Inv(a) превращает «на глаз почти отсортирован» в конкретное число.
Теорема о соседях
Обмен соседних элементов меняет число инверсий ровно на ±1 — затрагивается только их собственная пара, взаимный порядок с остальными не меняется. Следствие: любой алгоритм, переставляющий только соседей, делает не меньше Inv(a) перестановок.
Это нижняя граница на целый класс алгоритмов — тем же жанром, что нижние оценки занятия 2: не «мы не придумали быстрее», а «быстрее не бывает».
Вставки платят ровно по счёту
Каждый сдвиг убирает ровно одну инверсию — счётчики совпали до единицы, теорема живьём. Итого $\Theta(n + \operatorname{Inv}(a))$: $n$ на проход, Inv на сдвиги. В классе «соседних» алгоритмов вставки оптимальны — меньше, чем Inv(a), не заплатит никто.
Отсюда же ответ, как побеждать квадрат: только выйдя из класса — переставлять далёкие элементы, убирая много инверсий за одно перемещение. Случайный вход несёт $\sim n^2/4$ инверсий, поэтому средний случай любых «соседних» сортировок неизбежно квадратичен; merge sort и quicksort (семинар занятия 21) потому и быстры, что дальнобойны.
Замеры
Увидеть Θ(n²) глазами
Удвоение $n$ — время ×3–4: квадрат виден в живых числах (идеальные ×4 смазывают кэши; методика замеров — занятие 16). Вставки при равном «O» быстрее выбора в разы: их работа соседняя, кэш и предсказатель переходов довольны (занятие 8). А std::sort на 20 000 элементах быстрее обоих на два-три порядка: n log n против n² — не спор, а разгром.
Гвоздь: почти отсортированные данные
$n = 100\,000$: отсортированный массив плюс 20 случайных обменов — инверсий мало.
Вставки на почти отсортированном линейны и играют в одной лиге с промышленным std::sort. Выбор слеп: свои $n(n-1)/2$ сравнений он отработает при любом входе — разница со вставками ×20 000. Почти отсортированные данные — не экзотика: дозаписанные логи, обновлённые рейтинги, вчерашний отсортированный файл плюс новые записи. И проверка стоит копейки: std::is_sorted — O(n).
Вставки живут внутри каждого sort
std::sort — это introsort: quicksort + страховка heapsort + сортировка вставками для коротких кусков (порядка пары десятков элементов). Почему именно вставки: после разбиений куски маленькие и почти упорядоченные — мало инверсий; работа соседняя — кэшу хорошо; накладные расходы рекурсии дороже честного маленького квадратика. Timsort (Python, Java) построен на той же идее: находит отсортированные куски и доращивает их вставками.
Итог одной фразой: «квадратичная» ≠ «бесполезная» — это специалисты узкого профиля. Выбор — когда запись дорога; вставки — когда беспорядка мало. Универсалы n log n — на семинаре занятия 21.
Задачи семинара
-
Трассировка. Отсортируйте вставками {5, 2, 4, 6, 1, 3} на бумаге: массив после каждого $i$; сколько всего сдвигов? Проверьте, пересчитав инверсии входа.
-
Суд над стабильностью. На $[5_1, 5_2, 3, 1]$ покажите по шагам, где выбор рвёт порядок пятёрок, и объясните, почему у вставок это невозможно в принципе.
-
Экстремали. Постройте входы длины $n$, где вставки делают: (1) минимум работы; (2) максимум; (3) ровно $(n-1) + k$ сравнений при $k$ сдвигах для небольшого $k$.
Домашнее задание (сдача через Git)
- Обе сортировки со счётчиками сравнений и перемещений; воспроизведите таблицы семинара (n = 1000, входы sorted/random/reversed) и убедитесь: сдвиги вставок = инверсии входа.
- Квадрат на графике: времена обеих сортировок для $n = 1000 \cdot 2^k$ по методике занятия 16; таблица «n → время → отношение к предыдущему».
- Порог гибрида: найдите экспериментально длину B, до которой ваши вставки обгоняют
std::sortна случайных данных вашей машины; таблица замеров вокруг порога. - Число инверсий наивно за $O(n^2)$ + стресс-тест против счётчика сдвигов вашей insertionSort.
- * Расследование: найдите в исходниках libc++ (или libstdc++) порог переключения sort на вставки; приложите ссылку на строку и само число — и сравните с вашим порогом из задания 3.
Следующее занятие — std::variant и std::any: типобезопасный наследник union, обещанный занятием 18, и полиморфизм без наследования. А сортировки за n log n вернутся на семинаре 21: слиянием и быстрая, «разделяй и властвуй» из занятия 6 в главной роли.