При идеальном разбиении на каждом шаге подзадача вдвое меньше задачи, то есть
b=2.
Коэффициент
d=1, так как операция разделения массива на два подмассива имеет сложность
O(n).
Получаем, что
logba=log21=0<1. Это соответствует первому случаю в
основной теореме о рекурсии. То есть
T(n)=O(n).
Гоша: Хорошо, я понял. Худший случай в алгоритме поиска k-й порядковой статистики —
O(n2), и он маловероятен. В лучшем случае алгоритм работает за линейное время. А что насчёт среднего случая?
Тимофей: Сейчас узнаешь. Назовём подходящим такой элемент
k, индекс
L которого в отсортированном массиве принадлежит отрезку
[4S1;4S3], где
S — длина массива.
Вероятность
p, что элемент
k окажется подходящим равна
21. Ведь отрезок
[4S1;4S3] занимает половину длины массива.
То есть в среднем каждая вторая попытка приведёт к удачному разбиению.
При удачном разбиении размеры получившихся подмассивов составят не менее
41 и не более
43 от исходного. Значит, хорошее разбиение уменьшает размер задачи как минимум в
34.
Так как удачной в среднем будет каждое второе разбиение, можно сделать вывод, что в среднем за каждые две попытки размер задачи станет меньше хотя бы в
34 раза.
Посчитаем число
Y, равное общему количеству сравнений:
2n+43×2n+(43)2×2n+...=2n×(1+43+(43)2+...) Каждое слагаемое в сумме определяет количество сравнений до того, как размер задачи уменьшится в
34 раза по сравнению с текущим. Умножаем на 2, так как в среднем это будет происходить за 2 попытки.
Второй множитель — сумма бесконечно убывающей геометрической прогрессии.
Для подсчёта суммы воспользуемся формулой:
Sn=1−qb1.
b1 — первый член геометрической прогрессии. В нашем примере он равен
1.
q — знаменатель геометрической прогрессии. Он показывает, как от члена на позиции
n перейти к члену на позиции
n+1.
q=bnbn+1.
В нашем примере
q=43.
Подставив
b1 и
q в формулу, получим сумму равную 4. Тогда число
Y=2n×4=8n, что равно
O(n).
Мы считали сумму бесконечно убывающей прогрессии. На самом деле число слагаемых будет конечным. То есть в результате получится, что
Y меньше, чем
8n. Но даже в случае равенства можно сделать вывод, что сложность будет линейной.
Получим:
T(n)≤Y=8n=O(n)→T(n)=O(n).
Для доказательства можно также воспользоваться основной теоремой о рекурсии.
T(n)⩽T(43n)+O(n)→O(n).
Этот случай в основной теореме о рекурсии соответствует сложности
O(n).
Гоша: Спасибо! Теперь ясно, почему алгоритм поиска k-й порядковой статистики на практике работает быстро. Кажется, эти рассуждения применимы и для быстрой сортировки, да?
Тимофей: Да, рассуждения для алгоритма быстрой сортировки очень похожи.
Можно также воспользоваться идеей с хорошим разбиением, при котором опорный элемент попадает в центральные 50 % элементов разделяемого массива. Удачной, также как для алгоритма поиска k-ой порядковой статистики, в среднем будет каждая вторая попытка.
Таким образом, всего для получения
k удачных разбиений - то есть таких, чтобы опорный элемент
k раз оказался среди подходящих элементов, в среднем потребуется
2k рекурсивных вызовов. То есть в среднем глубина рекурсии не превышает
2×log43(n), что равно
O(logn).
А так как при каждом рекурсивном вызове выполняется не более
O(n) операций, общая сложность алгоритма получится
O(nlogn).
Гоша: Теперь я знаю, почему быстрая сортировка на практике работает эффективно и почему её используют в стандартных библиотеках языков программирования.
Тимофей: Да, на практике этот алгоритм достаточно эффективен.
Однако есть такие последовательности, на которых алгоритм будет работать квадратичное время. Они называются killer - последовательностями (англ. killer-sequence).
Например, при выборе первого или последнего элемента массива в качестве опорного отсортированная последовательность будет killer - последовательностью.
Но вероятность наступления таких событий достаточно маленькая.
Гоша: Но ведь быстрая сортировка используется в стандартных библиотеках многих языков программирования. Получается, что алгоритм иногда может работать квадратичное время?
Тимофей: Нет, такие случаи предусмотрели. Например, в С++ есть такая оптимизация. Если в процессе сортировки превышен некоторый коэффициент при
nlogn, то работа алгоритма завершается. И к работе подключается пирамидальная сортировка. Она гарантированно отсортирует данные за
O(nlogn). Про эту сортировку я расскажу в другой раз.