EduBrick

Почему быстрее n log n нельзя

Доказательство нижней оценки через дерево решений — и что именно оно запрещает, а что нет.

4 мин

Про сортировку часто говорят: «быстрее чем nlognn \log n нельзя». Утверждение верное, но неполное — и без второй половины оно противоречит сортировке подсчётом, которая работает за линию.

Разберёмся, что именно доказано.

Что считается сортировкой сравнениями

Договоримся об алгоритме, который узнаёт о данных только через вопросы вида «верно ли, что ai<aja_i < a_j». Он может как угодно переставлять элементы, но заглянуть внутрь значения — узнать, что там 42, а не 43, — не может.

Под это определение попадают все сортировки из предыдущих статей: пузырёк, вставки, выбор, слияние, быстрая, пирамидальная. Не попадает подсчёт: он не сравнивает элементы между собой, а использует само значение как адрес в массиве счётчиков.

Дерево решений

Проследим за работой такого алгоритма на массиве из nn различных элементов. Первый его шаг — какое-то сравнение. В зависимости от ответа он делает второе сравнение, и так далее, пока не выдаст ответ.

Это дерево: узлы — сравнения, две ветви — «да» и «нет», листья — итоговые перестановки.

graph TD
  A["a₁ < a₂ ?"] -->|да| B["a₂ < a₃ ?"]
  A -->|нет| C["a₁ < a₃ ?"]
  B -->|да| D["1 2 3"]
  B -->|нет| E["a₁ < a₃ ?"]
  E -->|да| F["1 3 2"]
  E -->|нет| G["3 1 2"]
  C -->|да| H["2 1 3"]
  C -->|нет| I["..."]

Два наблюдения.

Листьев не меньше n!n!. Входных массивов, различающихся порядком, ровно n!n! штук, и каждому нужен свой ответ — своя перестановка. Если бы два разных входа приводили в один лист, алгоритм выдал бы для них одинаковый ответ, и для одного из них ответ был бы неверным.

Число сравнений в худшем случае — это глубина дерева. А двоичное дерево глубины dd имеет не больше 2d2^d листьев.

Складываем: 2dn!2^d \ge n!, то есть

d    log2n!d \;\ge\; \log_2 n!

Сколько это

Оценим log2n!\log_2 n! грубо, без формул. В произведении n!=12nn! = 1 \cdot 2 \cdot \ldots \cdot n последняя половина сомножителей — каждый не меньше n/2n/2. Значит n!(n/2)n/2n! \ge (n/2)^{n/2}, откуда

log2n!    n2(log2n1)\log_2 n! \;\ge\; \frac{n}{2}\left(\log_2 n - 1\right)

Это уже Θ(nlogn)\Theta(n \log n). Точная оценка даётся формулой Стирлинга: log2n!nlog2n1,44n\log_2 n! \approx n \log_2 n - 1{,}44\,n.

Подставим настоящие числа. При n=105n = 10^5 получается около 1,521061{,}52 \cdot 10^6 сравнений — любая сортировка сравнениями сделает не меньше. Это, кстати, и объясняет, почему nlognn \log n-сортировки на таких размерах отрабатывают мгновенно: полтора миллиона сравнений процессору не страшны.

Что доказано, а что нет

Доказано: сортировка сравнениями делает Ω(nlogn)\Omega(n \log n) сравнений в худшем случае. Заметьте — в худшем. Про конкретный вход это ничего не говорит: на уже отсортированном массиве вставки справляются за nn сравнений, и никакого противоречия нет.

Не доказано ничего про алгоритмы, которые сравнениями не ограничиваются. Отсюда все обходные пути:

  • сортировка подсчётом использует значение как адрес и работает за O(n+C)O(n + C);
  • поразрядная сортировка смотрит на разряды числа и работает за O(kn)O(k \cdot n);
  • если данные уже почти отсортированы, оценка «в худшем случае» просто не про них.

Мораль практическая: если вам нужен именно полный порядок, данные произвольны и вы умеете только сравнивать — nlognn \log n это потолок, и время лучше потратить на что-то другое. Если хоть одно из трёх условий нарушено, есть куда копать.

Полезное следствие

Из той же оценки следует, что поиск kk-й статистики — задача проще сортировки. Мы разобрали алгоритм за O(n)O(n); если бы можно было отсортировать за линию, отдельный алгоритм был бы не нужен. Нижняя оценка объясняет, почему он нужен: сортировка отвечает на более трудный вопрос и за это платит логарифмом.