1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# конспект · шаг 7 из 7

Конспект: Timsort, powersort, introsort и pdqsort

выжимка - её можно унести в заметки

Понятия

Серия - кусок, уже упорядоченный по возрастанию (неубывающий) или по убыванию (в Python 3.12 - строго, с 3.13 - нестрого); убывающую разворачивают.

minrun - до какой длины короткая серия добивается вставками с двоичным поиском; 32-64.

Галоп - экспоненциальный поиск длины куска при слиянии: позиции 0, 1, 3, 7, 15…, потом двоичный поиск.

Сила границы (powersort) - номер первого уровня деления массива пополам, который разделяет середины двух соседних серий.

Introsort - быстрая сортировка со счётчиком глубины и переходом на пирамидальную.

Что унести

TIMSORT: ПРОХОД
серия: сравнить два первых; убывает - пока строго меньше, развернуть
короткая серия -> до minrun вставками (сравнений log₂, сдвиги дешёвы)
minrun = старшие 6 битов n (+1, если отброшена единица); n < 64 -> n
2112 -> 33 (64 серии), 10⁶ -> 62 (16 130 серий, чуть меньше 2¹⁴)
sorted на 10⁵: готовые - 99 999 сравнений; случайные - 1 528 982
нижняя граница log₂ 100000! ≈ 1 516 704 - перерасход 0,8%
СЛИЯНИЕ
только соседей: иначе ломается устойчивость
обрезка краёв: начало A ≤ B[0] и хвост B ≥ A[-1] уже на месте
буфер - на меньшую часть; при равенстве берётся элемент A
галоп после 7 побед подряд: кусок k за 2⌊log₂ k⌋ + 2 сравнения
проигрывает при k = 2 и 4, выигрывает с k = 6; порог плавает
две серии блоками по 100: 27 992 сравнения на слияние вместо ~200 000
ПОРЯДОК СЛИЯНИЙ
цена слияния = сумма длин
до 3.11: len[-3] > len[-2] + len[-1], len[-2] > len[-1]
  2015: проверяли 3 верхних -> инвариант рушился глубже, Java падала
с 3.11: powersort - сливать на стеке всё с силой > силы новой границы
гарантия powersort: ≤ n·H + 2n, n·H = Σ ℓᵢ log₂(n / ℓᵢ)
[1, 200, 200, 2, 2]: слева 1410, старое 1211, powersort = оптимум 814
[200, 5, 3, 200]: старое = оптимум 624, powersort 816
оптимум при известных длинах: динамика O(r³), Ху - Таккер O(r log r)
БЫСТРАЯ СО СТРАХОВКАМИ
противник Макилроя (1999): ломает быструю с дешёвым детерм. опорным
медиана трёх, n = 200: случайные 1574, убийца 10 135, с защитой 4664
introsort (libstdc++): запас 2⌊log₂ n⌋ -> пирамидальная; куски ≤ 16 -
  вставками одним проходом в конце
pdqsort: плохое разбиение (< 1/8) -> перемешать; > log₂ n плохих ->
  пирамидальная; без обменов -> добить вставками; блоки без ветвлений
КТО ЧТО ИСПОЛЬЗУЕТ
CPython     sorted, list.sort       Timsort + powersort, устойчиво
C++         std::sort               introsort; stable_sort - слиянием
Java        объекты / примитивы     Timsort / двухопорная быстрая
Go 1.19+    sort.Sort               pdqsort
Rust 1.81+  sort / sort_unstable    driftsort / ipnsort

Ловушки джокера

NaN в сортируемых числах. Сравнение с NaN ложно, результат не отсортирован, ошибки нет.

Своя быстрая сортировка в сервисе. Противник строит вход-убийцу один раз и шлёт его снова и снова.

Неустойчивая сортировка для многоключевого порядка. std::sort, sort.Slice, sort_unstable переставляют равные элементы.

Слияние не соседей «потому что дешевле». Порядок равных записей перепутается.

Инвариант, проверенный только сверху. Размер буфера, выведенный из рассуждения, проверяют доказательством, а не тестом.

«Introsort так же быстр на убийце». Страховка даёт n log n с константой побольше, не скорость случайного входа.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.