Я хотел бы добавить к существующим отличным ответам некоторую математику о том, как 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% сводных раздела, чтобы разбить входное значение 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], у пользователя примерно в тысячу раз выше, чем при просмотре 5000 пунктов сортировки хуже чем 10 * идеальный случай. Этот шанс уменьшается по мере увеличения набора данных. Вот некоторые размеры массивов и соответствующие им шансы на запуск более 10 * идеально:
- Массив из 640 предметов: 10 ^ -13 (требуется 15 хороших точек разворота из 60 попыток)
- Массив из 5000 элементов: 10 ^ -18 (требуется 22 хороших пивота из 88 попыток)
- Массив из 40000 элементов: 10 ^ -23 (требуется 29 хороших опорных точек из 116)
Помните, что это с двумя консервативными предположениями, которые хуже, чем реальность. Таким образом, фактическая производительность еще лучше, а баланс оставшейся вероятности ближе к идеалу, чем нет.
Наконец, как уже упоминали другие, даже эти невероятно маловероятные случаи можно устранить, переключившись на сортировку кучи, если стек рекурсии заходит слишком глубоко. Таким образом, TLDR заключается в том, что для хороших реализаций QuickSort наихудший случай на самом деле не существует , потому что он был разработан и выполнение завершается за O (n * logn) времени.