Я хотел бы добавить к существующим отличным ответам некоторую математику о том, как QuickSort работает при отклонении от лучшего случая, и насколько это вероятно, что, я надеюсь, поможет людям немного лучше понять, почему случай O (n ^ 2) не является реальным озабоченность в отношении более сложных реализаций QuickSort.
Помимо проблем произвольного доступа, есть два основных фактора, которые могут повлиять на производительность быстрой сортировки, и оба они связаны с тем, как сводка сравнивается с сортируемыми данными.
1) Небольшое количество ключей в данных. Набор данных с одним и тем же значением будет отсортирован за n ^ 2 раз на ванильной 2-секционной быстрой сортировке, потому что все значения, кроме местоположения центра, каждый раз располагаются на одной стороне. Современные реализации решают эту проблему такими методами, как использование 3-секционной сортировки. Эти методы выполняются в наборе данных с одинаковым значением за O (n) раз. Таким образом, использование такой реализации означает, что ввод с небольшим количеством клавиш фактически увеличивает время производительности и больше не является проблемой.
2) Чрезвычайно неудачный выбор точки поворота может привести к ухудшению производительности. В идеальном случае опорная точка всегда будет такой, что 50% данных будут меньше, а 50% - больше, так что вход будет разбит пополам во время каждой итерации. Это дает нам n сравнений и меняет время log-2 (n) рекурсий на O (n * logn).
Насколько неидеальный выбор сводки влияет на время выполнения?
Давайте рассмотрим случай, когда стержень последовательно выбирается таким образом, что 75% данных находятся на одной стороне стержня. Это все еще O (n * logn), но теперь база журнала изменилась на 1 / 0,75 или 1,33. Отношение в производительности при изменении базы всегда является константой, представленной log (2) / log (newBase). В этом случае эта константа равна 2,4. Так что это качество выбора разворота занимает в 2,4 раза больше времени, чем идеальное.
Как быстро это становится хуже?
Не очень быстро, пока выбор центра не станет (последовательно) очень плохим:
- 50% с одной стороны: (идеальный случай)
- 75% с одной стороны: в 2,4 раза больше
- 90% с одной стороны: в 6,6 раза больше
- 95% с одной стороны: в 13,5 раза больше
- 99% с одной стороны: в 69 раз больше
Когда мы приближаемся к 100% с одной стороны, лог-часть выполнения приближается к n, и все выполнение асимптотически приближается к O (n ^ 2).
В простой реализации QuickSort такие случаи, как отсортированный массив (для сводки 1-го элемента) или массив с обратной сортировкой (для сводки последнего элемента), будут надежно создавать время выполнения O (n ^ 2) в худшем случае. Кроме того, реализации с предсказуемым выбором поворота могут подвергаться DoS-атаке с помощью данных, предназначенных для выполнения в худшем случае. Современные реализации избегают этого с помощью различных методов, таких как рандомизация данных перед сортировкой, выбор медианы из 3 случайно выбранных индексов и т. Д. С этой рандомизацией в миксе мы имеем 2 случая:
- Небольшой набор данных. Наихудший случай вполне возможен, но O (n ^ 2) не является катастрофическим, потому что n достаточно мало, поэтому n ^ 2 также мало.
- Большой набор данных. Худший случай возможен в теории, но не на практике.
Насколько вероятно, что мы увидим ужасную производительность?
Шансы исчезающе малы . Давайте рассмотрим своего рода 5000 значений:
Наша гипотетическая реализация выберет опорную точку, используя медиану из 3 случайно выбранных индексов. Мы будем рассматривать «точки», которые находятся в диапазоне 25% -75%, как «хорошие», а точки, которые находятся в диапазоне 0% -25% или 75% -100%, являются «плохими». Если вы посмотрите на распределение вероятностей, используя медиану из 3 случайных индексов, у каждой рекурсии есть шанс 11/16 закончиться хорошим разворотом. Давайте сделаем 2 консервативных (и ложных) предположения для упрощения математики:
Хорошие точки разворота всегда точно на 25% / 75% и работают в 2,4 * идеальном случае. Мы никогда не получим идеальное разделение или любое разделение лучше, чем 25/75.
Плохие точки всегда являются наихудшим случаем и, по сути, не способствуют решению проблемы.
Наша реализация QuickSort остановится на n = 10 и переключится на сортировку вставкой, поэтому нам потребуется 22 25% / 75% pivot-разделов, чтобы разбить входное значение 5000 на такую глубину. (10 * 1.333333 ^ 22> 5000) Или нам нужно 4990 наихудших опорных точек. Имейте в виду, что если в какой-то момент мы накопим 22 хороших пивота, то сортировка будет завершена, поэтому наихудший случай или что-то близкое к нему требует крайне неудачи. Если бы нам потребовалось 88 рекурсий для фактического достижения 22 хороших опорных точек, необходимых для сортировки до n = 10, это было бы в 4 * 2,4 * идеальном случае или примерно в 10 раз больше времени выполнения идеального случая. Насколько вероятно, что мы не достигнем требуемых 22 хороших точек после 88 рекурсий?
Биномиальное распределение вероятностей может ответить на это, и ответ составляет около 10 ^ -18. (n равно 88, k равно 21, p равно 0,6875) Вероятность удара молнии за 1 секунду, которую требуется от молнии [SORT], у пользователя примерно в тысячу раз выше, чем у 5 000 элементов, которые работают хуже чем 10 * идеальный случай. Этот шанс уменьшается по мере увеличения набора данных. Вот некоторые размеры массивов и их соответствующие шансы работать дольше 10 * идеально:
- Массив из 640 предметов: 10 ^ -13 (требуется 15 хороших точек разворота из 60 попыток)
- Массив из 5000 элементов: 10 ^ -18 (требуется 22 хороших пивота из 88 попыток)
- Массив из 40000 элементов: 10 ^ -23 (требуется 29 хороших опорных точек из 116)
Помните, что это с двумя консервативными предположениями, которые хуже, чем реальность. Таким образом, фактическая производительность еще лучше, а баланс оставшейся вероятности ближе к идеалу, чем нет.
Наконец, как уже упоминали другие, даже эти абсурдно маловероятные случаи можно устранить, переключившись на сортировку кучи, если стек рекурсии заходит слишком глубоко. Таким образом, TLDR заключается в том, что для хороших реализаций QuickSort наихудшего случая на самом деле не существует, потому что он был разработан и выполнение завершается за O (n * logn) времени.
qsort
, Pythonlist.sort
иArray.prototype.sort
JavaScript в Firefox - все это упорядоченные слияния. (GNU STLsort
использует вместо этого Introsort, но это может быть потому, что в C ++ подкачка потенциально выигрывает у копирования.)