Я хотів би додати до існуючих чудових відповідей деяку математику про те, як працює QuickSort, коли відходить від кращого випадку, і наскільки це можливо, що, сподіваюся, допоможе людям трохи краще зрозуміти, чому випадок O (n ^ 2) не є реальним стурбованість більш складними реалізаціями QuickSort.
Поза проблемами випадкового доступу є два основні фактори, які можуть впливати на продуктивність QuickSort, і вони пов'язані з тим, як стрижневий порівняння з даними, відсортованими.
1) Невелика кількість ключів у даних. Набір даних того самого значення буде сортувати за n ^ 2 разів у ванільному 2-роздільному розділі QuickSort, оскільки всі значення, за винятком місця розташування, розміщуються по одній стороні кожного разу. Сучасні реалізації вирішують це за допомогою таких методів, як використання сортування з 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 такі випадки, як відсортований масив (для першого елемента повороту) або реверсивно відсортований масив (для останнього зведення елемента) надійно дадуть час виконання 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 разів більший час виконання ідеального випадку. Наскільки ймовірно, що після 88 рекурсій ми не досягли б потрібних 22 хороших стрибків?
Біноміальні розподіли ймовірностей можуть відповісти на це, а відповідь приблизно 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) час.
qsort
, Pythonlist.sort
таArray.prototype.sort
JavaScript у Firefox - це всі супутні види злиття. (GNU STLsort
використовує замість Introsort, але це може бути тому, що в C ++, обмін потенційно виграє велику кількість копіювання.)