Quicksort vs heapsort


Risposte:


60

Questo documento contiene alcune analisi.

Inoltre, da Wikipedia:

Il concorrente più diretto di quicksort è heapsort. Heapsort è tipicamente un po 'più lento di quicksort, ma il tempo di esecuzione nel caso peggiore è sempre Θ (nlogn). Quicksort è solitamente più veloce, sebbene permanga la possibilità di prestazioni nel caso peggiore tranne nella variante introsort, che passa a heapsort quando viene rilevato un caso negativo. Se è noto in anticipo che heapsort sarà necessario, utilizzarlo direttamente sarà più veloce che aspettare che introsort passi ad esso.


12
Potrebbe essere importante notare che nelle implementazioni tipiche, né quicksort né heapsort sono ordinamenti stabili.
MjrKusanagi

@DVK, secondo il tuo link cs.auckland.ac.nz/~jmor159/PLDS210/qsort3.html , l'ordinamento dell'heap richiede 2.842 confronti per n = 100, ma richiede 53.113 confronti per n = 500. E ciò implica che il rapporto tra n = 500 en = 100 è 18 volte e NON corrisponde all'algoritmo di ordinamento dell'heap con complessità O (N logN). Immagino sia abbastanza probabile che la loro implementazione dell'ordinamento di heap contenga qualche tipo di bug all'interno.
DU Jiaen

@DUJiaen - ricorda che O () riguarda il comportamento asintotico in generale N e ha un possibile moltiplicatore
DVK

Questo NON è correlato al moltiplicatore. Se un algoritmo ha una complessità di O (N log N), dovrebbe seguire un trend di Tempo (N) = C1 * N * log (N). E se prendi Time (500) / Time (100), è ovvio che C1 scomparirà e il risultato dovrebbe essere chiuso a (500 log500) / (100 log100) = 6.7 Ma dal tuo link, è 18, che è troppo fuori scala.
DU Jiaen

2
Il collegamento è morto
PlsWork

123

Heapsort è O (N log N) garantito, ciò che è molto meglio del caso peggiore in Quicksort. Heapsort non ha bisogno di più memoria per un altro array per inserire i dati ordinati come richiesto da Mergesort. Allora perché le applicazioni commerciali si attaccano a Quicksort? Cosa ha Quicksort di così speciale rispetto ad altre implementazioni?

Ho testato io stesso gli algoritmi e ho visto che Quicksort ha davvero qualcosa di speciale. Funziona velocemente, molto più velocemente degli algoritmi Heap e Merge.

Il segreto di Quicksort è: quasi non esegue scambi di elementi non necessari. Lo scambio richiede tempo.

Con Heapsort, anche se tutti i tuoi dati sono già ordinati, cambierai il 100% degli elementi per ordinare l'array.

Con Mergesort è anche peggio. Scriverai il 100% degli elementi in un altro array e lo riscriverai in quello originale, anche se i dati sono già ordinati.

Con Quicksort non scambi ciò che è già ordinato. Se i tuoi dati sono completamente ordinati, non scambi quasi nulla! Sebbene ci siano molte preoccupazioni sul caso peggiore, un piccolo miglioramento nella scelta del pivot, oltre a ottenere il primo o l'ultimo elemento dell'array, può evitarlo. Se ottieni un perno dall'elemento intermedio tra il primo, l'ultimo e il mezzo, è sufficiente evitare il caso peggiore.

Ciò che è superiore in Quicksort non è il caso peggiore, ma il caso migliore! Nel migliore dei casi fai lo stesso numero di confronti, ok, ma non cambi quasi nulla. In media, si scambiano parte degli elementi, ma non tutti, come in Heapsort e Mergesort. Questo è ciò che dà a Quicksort il miglior tempo. Meno scambi, più velocità.

L'implementazione di seguito in C # sul mio computer, in esecuzione in modalità di rilascio, batte Array.Sort di 3 secondi con il pivot centrale e di 2 secondi con il pivot migliorato (sì, c'è un sovraccarico per ottenere un buon pivot).

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);
}

10
+1 per considerazioni sul n. di operazioni di scambio, lettura / scrittura richieste per diversi algoritmi di ordinamento
ycy

2
Per qualsiasi strategia di selezione deterministica e costante del pivot temporale, è possibile trovare un array che produce il caso peggiore O (n ^ 2). Non basta eliminare solo il minimo. Devi scegliere in modo affidabile i perni che si trovano all'interno di una certa banda pecrentile.
Antimonio

1
Sono curioso di sapere se questo è il codice esatto che hai eseguito per le tue simulazioni tra il tuo ordinamento rapido codificato a mano e Array.sort integrato in C #? Ho testato questo codice e in tutti i miei test, nella migliore delle ipotesi, l'ordinamento rapido codificato a mano era lo stesso di Array.sort. Una cosa che ho controllato durante i miei test è stata quella di creare due copie identiche dell'array casuale. Dopotutto, una data randomizzazione potrebbe essere potenzialmente più favorevole (inclinazione verso il caso migliore) di un'altra randomizzazione. Quindi ho eseguito i set identici in ognuno di essi. Array.sort ha pareggiato o battuto ogni volta (rilascio build btw).
Chris

1
L'ordinamento di fusione non deve copiare il 100% degli elementi, a meno che non sia un'implementazione molto ingenua da un libro di testo. È semplice da implementare in modo che sia necessario copiarne solo il 50% (il lato sinistro dei due array uniti). È anche banale posticipare la copia fino a quando non devi effettivamente "scambiare" due elementi, quindi con dati già ordinati non avrai alcun sovraccarico di memoria. Quindi anche il 50% è in realtà il caso peggiore e puoi avere qualsiasi cosa tra quello e lo 0%.
ddekany

1
@MarquinhoPeli Volevo dire che è necessario solo il 50% in più di memoria disponibile rispetto alle dimensioni dell'elenco ordinato, non il 100%, il che sembra essere un malinteso comune. Quindi stavo parlando del picco di utilizzo della memoria. Non posso fornire un collegamento, ma è facile vedere se provi a unire le due metà già ordinate di un array in posizione (solo la metà sinistra ha il problema di sovrascrivere elementi che non hai ancora consumato). La quantità di memoria da copiare durante l'intero processo di ordinamento è un'altra domanda, ma ovviamente il caso peggiore non può essere inferiore al 100% per nessun algoritmo di ordinamento.
ddekany

15

Per la maggior parte delle situazioni, avere veloce o un po 'più veloce è irrilevante ... semplicemente non vuoi mai che occasionalmente diventi mooolto lento. Sebbene tu possa modificare QuickSort per evitare situazioni lente, perdi l'eleganza del QuickSort di base. Quindi, per la maggior parte delle cose, in realtà preferisco HeapSort ... puoi implementarlo nella sua piena semplice eleganza e non ottenere mai un ordinamento lento.

Per le situazioni in cui nella maggior parte dei casi si desidera la massima velocità, QuickSort potrebbe essere preferito a HeapSort, ma nessuno dei due potrebbe essere la risposta giusta. Per le situazioni critiche per la velocità, vale la pena esaminare da vicino i dettagli della situazione. Ad esempio, in alcuni dei miei codici critici per la velocità, è molto comune che i dati siano già ordinati o quasi ordinati (si tratta di indicizzare più campi correlati che spesso si muovono su e giù insieme O si muovono su e giù l'uno di fronte all'altro, quindi una volta ordinato per uno, gli altri vengono ordinati o invertiti o chiusi ... entrambi possono uccidere QuickSort). In quel caso, non ho implementato né ... invece, ho implementato SmoothSort di Dijkstra ... una variante HeapSort che è O (N) quando è già ordinata o quasi ordinata ... non è così elegante, non troppo facile da capire, ma veloce ... leggihttp://www.cs.utexas.edu/users/EWD/ewd07xx/EWD796a.PDF se vuoi qualcosa di un po 'più impegnativo da codificare.


6

Anche gli ibridi sul posto Quicksort-Heapsort sono davvero interessanti, poiché la maggior parte di essi necessita solo di n * log n confronti nel caso peggiore (sono ottimali rispetto al primo termine degli asintotici, quindi evitano gli scenari peggiori di Quicksort), O (log n) extra-spazio e conservano almeno "la metà" del buon comportamento di Quicksort rispetto al set di dati già ordinato. Un algoritmo estremamente interessante è presentato da Dikert e Weiss in http://arxiv.org/pdf/1209.4214v1.pdf :

  • Seleziona un pivot p come mediana di un campione casuale di elementi sqrt (n) (questo può essere fatto in un massimo di 24 confronti sqrt (n) attraverso l'algoritmo di Tarjan & co, o 5 confronti sqrt (n) attraverso il ragno molto più contorto -algoritmo di fabbrica di Schonhage);
  • Partiziona il tuo array in due parti come nel primo passaggio di Quicksort;
  • Heapify la parte più piccola e usa O (log n) bit extra per codificare un heap in cui ogni figlio sinistro ha un valore maggiore del suo fratello;
  • Estrarre ricorsivamente la radice del mucchio, setacciare la lacuna lasciata dalla radice fino a raggiungere una foglia del mucchio, quindi riempire la lacuna con un elemento appropriato preso dall'altra parte della matrice;
  • Ricorre sulla parte rimanente non ordinata dell'array (se p viene scelto come mediana esatta, non c'è affatto ricorsione).

2

Comp. tra quick sorte merge sortpoiché entrambi sono tipi di ordinamento sul posto, c'è una differenza tra il tempo di esecuzione del caso di wrost del tempo di esecuzione del caso di wrost per l'ordinamento rapido è O(n^2)e per l'ordinamento di heap è ancora O(n*log(n))e per una quantità media di dati l'ordinamento rapido sarà più utile. Poiché è un algoritmo randomizzato, la probabilità di ottenere risposte corrette. in meno tempo dipenderà dalla posizione dell'elemento pivot che scegli.

Quindi a

Buona scelta: le taglie di L e G sono ciascuna inferiore a 3s / 4

Cattiva chiamata: uno tra L e G ha una dimensione maggiore di 3s / 4

per piccole quantità possiamo usare l'ordinamento per inserzione e per grandi quantità di dati andare per l'ordinamento dell'heap.


Sebbene l'ordinamento di tipo merge possa essere implementato con l'ordinamento sul posto, l'implementazione è complessa. Per quanto ne so, la maggior parte delle implementazioni di merge sort non sono sul posto, ma sono stabili.
MjrKusanagi

2

Heapsort ha il vantaggio di avere un caso di esecuzione peggiore di O (n * log (n)), quindi nei casi in cui è probabile che quicksort abbia prestazioni scadenti (per lo più set di dati generalmente ordinati) heapsort è di gran lunga preferito.


4
Quicksort funziona male solo su un set di dati per lo più ordinato se viene scelto un metodo di scelta pivot scadente. Vale a dire, il metodo di scelta del pivot sbagliato sarebbe quello di scegliere sempre il primo o l'ultimo elemento come pivot. Se ogni volta viene scelto un pivot casuale e viene utilizzato un buon metodo per gestire gli elementi ripetuti, la possibilità di un quicksort nel caso peggiore è molto piccola.
Justin Peel

1
@ Justin - Questo è molto vero, stavo parlando di un'implementazione ingenua.
zellio

1
@ Justin: Vero, ma la possibilità di un forte rallentamento è sempre presente, anche se minima. Per alcune applicazioni, potrei voler garantire il comportamento O (n log n), anche se è più lento.
David Thornley

2

Bene, se vai a livello di architettura ... usiamo la struttura dei dati della coda nella memoria cache, quindi ciò che è disponibile in coda verrà ordinato Come nell'ordinamento rapido non abbiamo problemi a dividere l'array in qualsiasi lunghezza ... ma in heap ordina (usando array) può succedere che il genitore non sia presente nel sotto-array disponibile nella cache e quindi deve portarlo nella memoria cache ... il che richiede tempo. Questo è il quicksort è il migliore !! 😀


1

Heapsort crea un heap e quindi estrae ripetutamente l'elemento massimo. Il suo caso peggiore è O (n log n).

Ma se vedessi il caso peggiore di ordinamento rapido , che è O (n2), ti renderesti conto che l'ordinamento rapido sarebbe una scelta non così buona per dati di grandi dimensioni.

Quindi questo rende l'ordinamento una cosa interessante; Credo che il motivo per cui così tanti algoritmi di ordinamento vivono oggi sia perché tutti sono "migliori" nei loro posti migliori. Ad esempio, l'ordinamento a bolle può eseguire l'ordinamento rapido se i dati vengono ordinati. Oppure, se sappiamo qualcosa sugli articoli da smistare, probabilmente possiamo fare di meglio.

Questo potrebbe non rispondere direttamente alla tua domanda, ho pensato di aggiungere i miei due centesimi.


1
Non usare mai il Bubble sort. Se ritieni ragionevolmente che i tuoi dati verranno ordinati, puoi utilizzare l'ordinamento per inserzione o persino testare i dati per vedere se sono ordinati. Non usare bubblesort.
vy32

se hai un set di dati RANDOM molto grande, la soluzione migliore è quicksort. Se ordinato parzialmente, allora no, ma se inizi a lavorare con enormi set di dati dovresti sapere almeno questo molto su di loro.
Kobor42

1

Heap Sort è una scommessa sicura quando si tratta di input molto grandi. L'analisi asintotica rivela l'ordine di crescita di Heapsort nel peggiore dei casi Big-O(n logn), che è migliore di quello di Quicksort Big-O(n^2)nel peggiore dei casi. Tuttavia, Heapsort è un po 'più lento nella pratica sulla maggior parte delle macchine rispetto a un ordinamento rapido ben implementato. Anche Heapsort non è un algoritmo di ordinamento stabile.

Il motivo per cui heapsort è più lento in pratica di quicksort è dovuto alla migliore località di riferimento (" https://en.wikipedia.org/wiki/Locality_of_reference ") in quicksort, dove gli elementi di dati si trovano all'interno di posizioni di archiviazione relativamente vicine. I sistemi che presentano una forte località di riferimento sono ottimi candidati per l'ottimizzazione delle prestazioni. L'ordinamento degli heap, tuttavia, si occupa di salti più grandi. Ciò rende Quicksort più favorevole per input più piccoli.


2
Anche l'ordinamento rapido non è stabile.
Antimonio

1

Per me c'è una differenza fondamentale tra heapsort e quicksort: quest'ultimo utilizza una ricorsione. Negli algoritmi ricorsivi l'heap cresce con il numero di ricorsioni. Non importa se n è piccolo, ma in questo momento sto ordinando due matrici con n = 10 ^ 9 !!. Il programma richiede quasi 10 GB di RAM e l'eventuale memoria aggiuntiva farà sì che il mio computer inizi a passare alla memoria del disco virtuale. Il mio disco è un disco RAM, ma continuare a cambiarlo fa un'enorme differenza di velocità . Quindi in uno statpack codificato in C ++ che include matrici di dimensioni regolabili, con dimensioni sconosciute in anticipo al programmatore, e tipo di ordinamento statistico non parametrico, preferisco l'heapsort per evitare ritardi nell'utilizzo con matrici di dati molto grandi.


1
In media è necessaria solo la memoria O (logn). L'overhead di ricorsione è banale, supponendo che tu non sia sfortunato con i pivot, nel qual caso hai problemi più grandi di cui preoccuparti.
Antimonio

-1

Per rispondere alla domanda originale e indirizzare alcuni degli altri commenti qui:

Ho appena confrontato le implementazioni di selezione, veloce, unione e ordinamento heap per vedere come si sovrappongono l'una contro l'altra. La risposta è che hanno tutti i loro lati negativi.

TL; DR: Quick è il miglior ordinamento generico (ragionevolmente veloce, stabile e per lo più sul posto) Personalmente preferisco l'ordinamento heap, a meno che non mi serva un ordinamento stabile.

Selezione - N ^ 2 - È davvero buono solo per meno di 20 elementi o giù di lì, quindi ha prestazioni migliori. A meno che i tuoi dati non siano già ordinati, o molto, molto quasi. N ^ 2 diventa molto lento molto velocemente.

Veloce, nella mia esperienza, non è in realtà così veloce tutto il tempo. I bonus per l'utilizzo dell'ordinamento rapido come ordinamento generale sono però che è ragionevolmente veloce ed è stabile. È anche un algoritmo sul posto, ma poiché è generalmente implementato in modo ricorsivo, occuperà spazio aggiuntivo nello stack. Inoltre cade da qualche parte tra O (n log n) e O (n ^ 2). Il tempismo su alcuni tipi sembra confermare questo, soprattutto quando i valori rientrano in un intervallo ristretto. È molto più veloce dell'ordinamento della selezione su 10.000.000 di elementi, ma più lento della fusione o dell'heap.

L'ordinamento di unione è garantito O (n log n) poiché il suo ordinamento non dipende dai dati. Fa semplicemente quello che fa, indipendentemente dai valori che gli hai dato. È anche stabile, ma i tipi molto grandi possono far saltare il tuo stack se non stai attento all'implementazione. Esistono alcune complesse implementazioni di merge sort sul posto, ma in genere è necessario un altro array in ogni livello per unire i valori. Se questi array risiedono nello stack, puoi incorrere in problemi.

L'ordinamento dell'heap è max O (n log n), ma in molti casi è più veloce, a seconda di quanto lontano devi spostare i tuoi valori nel log n heap profondo. L'heap può essere facilmente implementato sul posto nell'array originale, quindi non necessita di memoria aggiuntiva ed è iterativo, quindi nessuna preoccupazione per l'overflow dello stack durante la ricorrenza. L' enorme svantaggio dell'ordinamento heap è che non è un ordinamento stabile, il che significa che è giusto se ne hai bisogno.


L'ordinamento rapido non è un ordinamento stabile. Oltre a ciò, domande di questa natura incoraggiano risposte basate sull'opinione e potrebbero portare a modificare guerre e argomenti. Le domande che richiedono risposte basate sull'opinione sono esplicitamente scoraggiate dalle linee guida SO. Coloro che rispondono dovrebbero evitare la tentazione di rispondere anche se hanno esperienza e saggezza significative nello sono. Segnalali per la chiusura o attendi che qualcuno con sufficiente reputazione per segnalarli e chiuderli. Questo commento non è una riflessione sulla tua conoscenza o sulla validità della tua risposta.
MikeC
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.