Як швидка, так і куча сортування виконують сортування на місці. Який краще? Які програми та випадки є переважними?
Відповіді:
Ця стаття має певний аналіз.
Також з Вікіпедії:
Найбільш безпосереднім конкурентом швидкого сорту є кучевий сорт. Хіпсорт зазвичай трохи повільніший, ніж швидкий, але найгірший час роботи завжди Θ (nlogn). Швидке сортування, як правило, швидше, хоча залишається ймовірність найгіршого результату, за винятком варіанту інтросортування, який перемикається на купірування, коли виявляється поганий випадок. Якщо заздалегідь відомо, що буде необхідний куп сорту, його безпосереднє використання буде швидшим, ніж очікування переходу на нього вбудованого сортування.
Гарячий сорт гарантований O (N log N), що набагато краще, ніж найгірший випадок у Quicksort. Heapsort не потребує більше пам'яті для іншого масиву для розміщення впорядкованих даних, як це потрібно Mergesort. То чому комерційні додатки дотримуються Quicksort? Що в Quicksort є таким особливим у порівнянні з іншими реалізаціями?
Я сам протестував алгоритми і переконався, що Quicksort насправді має щось особливе. Він працює швидко, набагато швидше, ніж алгоритми Heap та Merge.
Секрет Quicksort у тому, що він майже не робить непотрібних обмінів елементів. Обмін займає багато часу.
За допомогою Heapsort, навіть якщо всі ваші дані вже впорядковані, ви збираєтеся поміняти місцями 100% елементів для упорядкування масиву.
З Mergesort це ще гірше. Ви збираєтеся записати 100% елементів в інший масив і записати його назад у вихідний, навіть якщо дані вже впорядковані.
За допомогою Quicksort ви не міняєте місцями вже замовлене. Якщо ваші дані повністю упорядковані, ви майже нічого не поміняєте! Незважаючи на те, що є багато суєти щодо найгіршого випадку, невелике вдосконалення у виборі опори, будь-яке інше, крім отримання першого або останнього елемента масиву, може уникнути цього. Якщо ви отримуєте опору з проміжного елемента між першим, останнім та середнім елементом, достатньо уникнути найгіршого випадку.
Що перевершує Quicksort - це не найгірший випадок, а найкращий випадок! У кращому випадку ви робите однакову кількість порівнянь, добре, але ви майже нічого не поміняєте. У середньому ви поміняєте місцями частину елементів, але не всі елементи, як у Heapsort та Mergesort. Це те, що надає Quicksort найкращий час. Менше обміну, більша швидкість.
Реалізація нижче в C # на моєму комп'ютері, що працює в режимі звільнення, перевершує Array.Sort на 3 секунди із середнім опором та на 2 секунди з покращеним стрижнем (так, для отримання хорошого стрижня є накладні витрати).
static void Main(string[] args)
{
int[] arrToSort = new int[100000000];
var r = new Random();
for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
Console.WriteLine("Press q to quick sort, s to Array.Sort");
while (true)
{
var k = Console.ReadKey(true);
if (k.KeyChar == 'q')
{
// quick sort
Console.WriteLine("Beg quick sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
QuickSort(arrToSort, 0, arrToSort.Length - 1);
Console.WriteLine("End quick sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
}
else if (k.KeyChar == 's')
{
Console.WriteLine("Beg Array.Sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
Array.Sort(arrToSort);
Console.WriteLine("End Array.Sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
}
}
}
static public void QuickSort(int[] arr, int left, int right)
{
int begin = left
, end = right
, pivot
// get middle element pivot
//= arr[(left + right) / 2]
;
//improved pivot
int middle = (left + right) / 2;
int
LM = arr[left].CompareTo(arr[middle])
, MR = arr[middle].CompareTo(arr[right])
, LR = arr[left].CompareTo(arr[right])
;
if (-1 * LM == LR)
pivot = arr[left];
else
if (MR == -1 * LR)
pivot = arr[right];
else
pivot = arr[middle];
do
{
while (arr[left] < pivot) left++;
while (arr[right] > pivot) right--;
if(left <= right)
{
int temp = arr[right];
arr[right] = arr[left];
arr[left] = temp;
left++;
right--;
}
} while (left <= right);
if (left < end) QuickSort(arr, left, end);
if (begin < right) QuickSort(arr, begin, right);
}
У більшості ситуацій швидке та трохи швидше не має значення ... Ви просто ніколи не хочете, щоб воно іноді ставало повільно. Хоча ви можете налаштувати QuickSort, щоб уникнути повільних ситуацій, ви втрачаєте елегантність базового QuickSort. Отже, для більшості речей я насправді віддаю перевагу HeapSort ... ви можете реалізувати його в повній простій елегантності і ніколи не отримувати повільного сортування.
У ситуаціях, коли в більшості випадків ВИ бажаєте максимальної швидкості, QuickSort може бути кращим над HeapSort, але жоден з них не може бути правильною відповіддю. У ситуаціях, що мають критичну швидкість, варто уважно вивчити деталі ситуації. Наприклад, у деяких моїх критично важливих для швидкості кодах дуже часто дані сортуються або майже сортуються (це індексація декількох пов’язаних полів, які часто або переміщуються вгору і вниз разом АБО рухаються вгору і вниз навпроти один одного, отже, як тільки ви сортуєте за одним, інші сортуються або зворотно сортуються, або закриваються ... кожен з яких може вбити QuickSort). У цьому випадку я не застосував ні ... натомість, я застосував SmoothSort Дейкстри ... варіант HeapSort, який O (N), коли вже відсортований або майже відсортований ... це не так елегантно, не надто легко зрозуміти, але швидко ... читатиhttp://www.cs.utexas.edu/users/EWD/ewd07xx/EWD796a.PDF, якщо ви хочете щось більш складне для кодування.
Гібриди Quicksort-Heapsort на місці теж справді цікаві, оскільки більшості з них потрібні лише порівняння n * log n у гіршому випадку (вони є оптимальними щодо першого терміну асимптотики, тому вони уникають гірших сценаріїв розвитку подій). Quicksort), O (log n) додатковий простір, і вони зберігають принаймні "половину" належної поведінки Quicksort щодо вже впорядкованого набору даних. Надзвичайно цікавий алгоритм представлений Дікертом та Вайсом у http://arxiv.org/pdf/1209.4214v1.pdf :
Комп. між quick sort
і merge sort
оскільки обидва є типом сортування за місцем, існує різниця між часом запуску випадку гніву, час роботи гнівного випадку, для швидкого сортування становить, O(n^2)
а для сортування купи все ще є, O(n*log(n))
і для середньої кількості даних швидке сортування буде більш корисним. Оскільки це рандомізований алгоритм, то ймовірність отримання правильних відповідей. менше часу залежатиме від вибраного вами положення опорного елемента.
Отже, a
Хороший дзвінок: розміри L і G кожен менше 3s / 4
Поганий дзвінок: один із L та G має розмір більше 3s / 4
для невеликої кількості ми можемо піти на сортування вставки, а для дуже великої кількості даних - сортування купи.
Хепсорт має перевагу в тому, що у найгіршому випадку працює O (n * log (n)), тому в тих випадках, коли швидка сортування, ймовірно, буде неефективною (в основному сортуються набори даних), набагато кращою є сортування.
Ну, якщо ви перейдете на рівень архітектури ... ми використовуємо структуру даних черги в кеш-пам'яті. Тому все, що доступно в черзі, буде сортуватися. Як і при швидкому сортуванні, у нас немає проблем з розділенням масиву на будь-яку довжину ... але в купі сортування (за допомогою масиву) може статися так, що батько може не бути в підмасиві, доступному в кеш-пам’яті, а потім йому доведеться внести його в кеш-пам’ять ... що займає багато часу. Це швидкий сорт найкращий !! 😀
Теплий сорт створює купу, а потім неодноразово витягує максимум предмета. Найгірший її випадок - O (n log n).
Але якщо ви побачите найгірший випадок швидкого сортування , яким є O (n2), ви зрозумієте, що швидке сортування буде не дуже вдалим вибором для великих даних.
Отже, це робить сортування цікавою річчю; Я вважаю, що причина, через яку сьогодні живе так багато алгоритмів сортування, полягає в тому, що всі вони найкращі у своїх кращих місцях. Наприклад, сортування за допомогою міхура може виконувати швидке сортування, якщо дані сортуються. Або якщо ми знаємо щось про предмети, що підлягають сортуванню, то, мабуть, ми можемо зробити краще.
Це може не відповісти прямо на ваше запитання, я подумав би додати свої два центи.
Сортування купи - це безпечна ставка при роботі з дуже великими входами. Асимптотичний аналіз виявляє порядок зростання Хепсорту в гіршому випадку Big-O(n logn)
, що є кращим, ніж у Швидкого сорту , Big-O(n^2)
як найгірший випадок. Однак Heapsort на практиці на більшості машин дещо повільніший, ніж добре реалізований швидкий сорт. Хепсорт також не є стабільним алгоритмом сортування.
Причина того, що купірування на практиці повільніше, ніж швидке сортування, пояснюється кращою локальністю посилань (" https://en.wikipedia.org/wiki/Locality_of_reference ") у швидкій сортуванні, де елементи даних знаходяться у відносно близьких місцях зберігання. Системи, які демонструють сильну локальність посилань, є чудовими кандидатами для оптимізації продуктивності. Сортування купи, однак, має справу з більшими стрибками. Це робить швидкі сорти більш сприятливими для менших витрат.
Для мене існує дуже принципова різниця між сортовим та швидким сортами: останній використовує рекурсію. У рекурсивних алгоритмах купа зростає із збільшенням кількості рекурсій. Це не має значення, якщо n мало, але зараз я сортую дві матриці з n = 10 ^ 9 !!. Програма займає майже 10 ГБ оперативної пам'яті, і будь-яка зайва пам'ять змусить мій комп'ютер почати обмін на віртуальну пам'ять диска. Мій диск - це диск з оперативною пам’яттю, але все одно його обмін значно впливає на швидкість . Отже, у пакеті статистичних даних, кодованому на C ++, який включає матриці розмірів із регульованими розмірами, розмір яких заздалегідь невідомий програмісту, та непараметричне статистичне сортування, я віддаю перевагу купі сорту, щоб уникнути затримок використання з дуже великими матрицями даних.
Щоб відповісти на оригінальне запитання та звернутися до деяких інших коментарів тут:
Я щойно порівняв реалізації виділення, швидкого, об’єднання та сортування купи, щоб побачити, як вони складаються один проти одного. Відповідь полягає в тому, що всі вони мають свої мінуси.
TL; DR: Швидкий - найкращий сорт загального призначення (досить швидкий, стабільний і в основному на місці). Особисто я віддаю перевагу сортуванню купи, хоча, якщо мені не потрібне стабільне сортування.
Вибір - N ^ 2 - Це дійсно добре лише для 20 елементів або близько того, тоді він перевершує ефективність. Якщо ваші дані вже не відсортовані, або дуже, майже так. N ^ 2 дуже швидко стає дуже повільним.
З мого досвіду, швидкий, насправді не такий швидкий весь час. Бонуси за використання швидкого сортування як загального сортування полягають у тому, що він досить швидкий і стабільний. Це також алгоритм на місці, але оскільки він, як правило, реалізується рекурсивно, він займе додатковий простір у стеку. Він також знаходиться десь між O (n log n) та O (n ^ 2). Визначення часу для деяких сортів, схоже, підтверджує це, особливо коли значення потрапляють у вузький діапазон. Це набагато швидше, ніж виділення на 10000000 елементів, але повільніше, ніж злиття або купа.
Сортування злиття гарантоване O (n log n), оскільки його сортування не залежить від даних. Він просто робить те, що робить, незалежно від того, які цінності ви йому надали. Він також стабільний, але дуже великі сорти можуть підірвати ваш стек, якщо ви не будете обережні щодо реалізації. Є кілька складних реалізацій сортування на місці, але зазвичай вам потрібен інший масив на кожному рівні, в який слід об’єднати свої значення. Якщо ці масиви живуть у стеку, ви можете зіткнутися з проблемами.
Сортування купи становить максимум O (n журнал n), але в багатьох випадках це швидше, залежно від того, наскільки далеко вам доведеться переміщати свої значення вгору по купі журналу n. Купу можна легко реалізувати на місці в оригінальному масиві, тому їй не потрібна додаткова пам'ять, вона є ітераційною, тому не потрібно турбуватися про переповнення стека під час повторення. Величезний недолік купи сортування є те , що вона не є стабільною роду, а це значить , що це правильно, якщо вам потрібно.