Quicksort: scelta del perno


109

Quando si implementa Quicksort, una delle cose che devi fare è scegliere un pivot. Ma quando guardo uno pseudocodice come quello qui sotto, non è chiaro come dovrei scegliere il perno. Primo elemento della lista? Qualcos'altro?

 function quicksort(array)
     var list less, greater
     if length(array) ≤ 1  
         return array  
     select and remove a pivot value pivot from array
     for each x in array
         if x ≤ pivot then append x to less
         else append x to greater
     return concatenate(quicksort(less), pivot, quicksort(greater))

Qualcuno può aiutarmi a cogliere il concetto di scegliere un perno e se scenari diversi richiedono strategie diverse o meno.


Risposte:


87

La scelta di un pivot casuale riduce al minimo la possibilità che si verifichino prestazioni O (n 2 ) nel caso peggiore (scegliere sempre il primo o l'ultimo causerebbe prestazioni nel caso peggiore per dati quasi ordinati o quasi inversi). Anche la scelta dell'elemento intermedio sarebbe accettabile nella maggior parte dei casi.

Inoltre, se lo stai implementando da solo, ci sono versioni dell'algoritmo che funzionano sul posto (cioè senza creare due nuovi elenchi e quindi concatenarli).


10
Secondo l'idea che implementare una ricerca da soli potrebbe non valere la pena. Inoltre, fai attenzione a come scegli numeri casuali, poiché i generatori di numeri casuali a volte sono un po 'lenti.
PeterAllenWebb

La risposta di @Jonathan Leffler è migliore
Nathan

60

Dipende dalle tue esigenze. La scelta di un pivot a caso rende più difficile creare un set di dati che generi prestazioni O (N ^ 2). Anche la "mediana di tre" (primo, ultimo, medio) è un modo per evitare problemi. Attenzione però alle prestazioni relative dei confronti; se i tuoi confronti sono costosi, Mo3 fa più confronti che scegliere (un singolo valore pivot) a caso. I record del database possono essere costosi da confrontare.


Aggiornamento: inserire commenti in risposta.

mdkess ha affermato:

"Mediana di 3" NON è il primo ultimo medio. Scegli tre indici casuali e prendi il valore medio di questo. Il punto è assicurarsi che la scelta dei pivot non sia deterministica: se lo è, i dati del caso peggiore possono essere generati abbastanza facilmente.

A cui ho risposto:

  • Analysis Of Hoare's Find Algorithm With Median-Of-Three Partition (1997) di P Kirschenhofer, H Prodinger, C Martínez sostiene la tua tesi (che la "mediana di tre" è tre elementi casuali).

  • C'è un articolo descritto su portal.acm.org che parla di "The Worst Case Permutation for Median-of-Three Quicksort" di Hannu Erkiö, pubblicato su The Computer Journal, Vol 27, No 3, 1984. [Aggiornamento 2012-02- 26: Ho ricevuto il testo dell'articolo . La sezione 2 "L'algoritmo" inizia: " Utilizzando la mediana del primo, medio e ultimo elemento di A [L: R], è possibile ottenere partizioni efficienti in parti di dimensioni abbastanza uguali nella maggior parte delle situazioni pratiche. "Quindi, sta discutendo l'approccio primo-medio-ultimo Mo3.]

  • Un altro breve articolo interessante è di MD McIlroy, "A Killer Adversary for Quicksort" , pubblicato in Software-Practice and Experience, Vol. 29 (0), 1–4 (0 1999). Spiega come fare in modo che quasi tutti i Quicksort si comportino in modo quadratico.

  • AT&T Bell Labs Tech Journal, ottobre 1984 "Theory and Practice in the Construction of a Working Sort Routine" afferma "Hoare suggerì di suddividere la mediana attorno alla mediana di diverse linee selezionate casualmente. Sedgewick [...] raccomandò di scegliere la mediana della prima [. ..] ultimo [...] e mezzo ". Ciò indica che entrambe le tecniche per la "mediana di tre" sono note in letteratura. (Aggiornamento 23-11-2014: l'articolo sembra essere disponibile su IEEE Xplore o da Wiley , se sei abbonato o sei disposto a pagare una quota.)

  • 'Engineering a Sort Function' di JL Bentley e MD McIlroy, pubblicato in Software Practice and Experience, Vol 23 (11), novembre 1993, entra in un'ampia discussione dei problemi e hanno scelto un algoritmo di partizionamento adattivo basato in parte sul dimensione del set di dati. Si discute molto sui compromessi per i vari approcci.

  • Una ricerca su Google per "mediana di tre" funziona abbastanza bene per un ulteriore monitoraggio.

Grazie per l'informazione; Prima avevo incontrato solo la "mediana di tre" deterministica.


4
La mediana di 3 NON è il primo ultimo medio. Scegli tre indici casuali e prendi il valore medio di questo. Il punto è assicurarsi che la scelta dei pivot non sia deterministica: se lo è, i dati del caso peggiore possono essere generati abbastanza facilmente.
mindvirus

Stavo leggendo abt introsort che combina buone caratteristiche sia di quicksort che di heapsort. L'approccio per selezionare il pivot utilizzando la mediana di tre potrebbe non essere sempre favorevole.
Sumit Kumar Saha

4
Il problema con la scelta degli indici casuali è che i generatori di numeri casuali sono piuttosto costosi. Sebbene non aumenti il ​​costo elevato dell'ordinamento, probabilmente renderà le cose più lente rispetto a se avessi appena selezionato il primo, l'ultimo e il mezzo elementi. (Nel mondo reale, scommetto che nessuno sta creando situazioni artificiose per rallentare il tuo ordinamento veloce.)
Kevin Chen,

20

Eh, ho appena insegnato in questo corso.

Ci sono diverse opzioni.
Semplice: scegli il primo o l'ultimo elemento dell'intervallo. (pessimo su input parzialmente ordinato) Migliore: scegli l'elemento al centro dell'intervallo. (meglio su input parzialmente ordinato)

Tuttavia, la scelta di qualsiasi elemento arbitrario rischia di partizionare in modo insufficiente l'array di dimensione n in due array di dimensione 1 e n-1. Se lo fai abbastanza spesso, il tuo quicksort corre il rischio di diventare O (n ^ 2).

Un miglioramento che ho visto è scegliere la mediana (prima, ultima, metà); Nel peggiore dei casi, può ancora andare a O (n ^ 2), ma probabilisticamente, questo è un caso raro.

Per la maggior parte dei dati, è sufficiente selezionare il primo o l'ultimo. Ma, se ti accorgi che ti trovi spesso negli scenari peggiori (input parzialmente ordinato), la prima opzione sarebbe quella di scegliere il valore centrale (che è un perno statisticamente buono per i dati parzialmente ordinati).

Se continui a riscontrare problemi, segui il percorso mediano.


1
Abbiamo fatto un esperimento nella nostra classe, ottenendo i k elementi più piccoli da un array in ordine ordinato. Abbiamo generato array casuali, quindi abbiamo utilizzato un min-heap o un quicksort pivot selezionato e fisso randomizzato e abbiamo contato il numero di confronti. In base a questi dati "casuali", la seconda soluzione si è comportata in media peggio della prima. Il passaggio a un pivot randomizzato risolve il problema delle prestazioni. Quindi, anche per dati apparentemente casuali, il pivot fisso ha prestazioni significativamente peggiori del pivot randomizzato.
Robert S. Barnes

Perché partizionare l'array di dimensione n in due array di dimensione 1 en-1 correrebbe il rischio di diventare O (n ^ 2)?
Aaron Franke

Assumi un array di dimensione N. Partiziona in dimensioni [1, N-1]. Il passaggio successivo è il partizionamento della metà destra in [1, N-2]. e così via, fino a quando non avremo N partizioni di dimensione 1. Ma, se dovessimo partizionare a metà, faremmo 2 partizioni di N / 2 ogni passaggio, portando al termine Log (n) della complessità;
Chris Cudmore

11

Non scegliere mai un pivot fisso: questo può essere attaccato per sfruttare il runtime O (n ^ 2) del caso peggiore del tuo algoritmo, che sta solo chiedendo problemi. Il runtime del caso peggiore di Quicksort si verifica quando il partizionamento produce un array di 1 elemento e un array di n-1 elementi. Supponi di scegliere il primo elemento come partizione. Se qualcuno alimenta un array al tuo algoritmo che è in ordine decrescente, il tuo primo pivot sarà il più grande, quindi tutto il resto dell'array si sposterà a sinistra di esso. Quindi, quando ricorri, il primo elemento sarà di nuovo il più grande, quindi ancora una volta metti tutto a sinistra di esso, e così via.

Una tecnica migliore è il metodo della mediana di 3, in cui scegli tre elementi a caso e scegli il centro. Sai che l'elemento che scegli non sarà il primo o l'ultimo, ma anche, per il teorema del limite centrale, la distribuzione dell'elemento medio sarà normale, il che significa che tenderai verso il centro (e quindi , n lg n time).

Se vuoi assolutamente garantire il runtime O (nlgn) per l'algoritmo, il metodo delle colonne di 5 per trovare la mediana di un array viene eseguito in tempo O (n), il che significa che l'equazione di ricorrenza per quicksort nel caso peggiore lo farà essere T (n) = O (n) (trova la mediana) + O (n) (partizione) + 2T (n / 2) (ricorre a sinistra ea destra.) Secondo il Teorema del Maestro, questo è O (n lg n) . Tuttavia, il fattore costante sarà enorme, e se le prestazioni nel caso peggiore sono la tua preoccupazione principale, usa invece un merge sort, che è solo un po 'più lento di quicksort in media e garantisce tempo O (nlgn) (e sarà molto più veloce rispetto a questo mediocre quicksort).

Spiegazione dell'algoritmo della mediana delle mediane


6

Non cercare di diventare troppo intelligente e combinare strategie di rotazione. Se hai combinato la mediana di 3 con il pivot casuale scegliendo la mediana del primo, dell'ultimo e un indice casuale nel mezzo, sarai comunque vulnerabile a molte delle distribuzioni che inviano la mediana di 3 quadratica (quindi è effettivamente peggio di pivot casuale semplice)

Ad esempio, una distribuzione di organo a canne (1,2,3 ... N / 2..3,2,1) prima e l'ultima saranno entrambe 1 e l'indice casuale sarà un numero maggiore di 1, considerando che la mediana dà 1 ( primo o ultimo) e si ottiene un partizionamento estremamente sbilanciato.


2

È più facile suddividere il quicksort in tre sezioni in questo modo

  1. Funzione di scambio o scambio di dati
  2. La funzione di partizione
  3. Elaborazione delle partizioni

È solo leggermente più inefficace di una funzione lunga ma è molto più facile da capire.

Il codice segue:

/* This selects what the data type in the array to be sorted is */

#define DATATYPE long

/* This is the swap function .. your job is to swap data in x & y .. how depends on
data type .. the example works for normal numerical data types .. like long I chose
above */

void swap (DATATYPE *x, DATATYPE *y){  
  DATATYPE Temp;

  Temp = *x;        // Hold current x value
  *x = *y;          // Transfer y to x
  *y = Temp;        // Set y to the held old x value
};


/* This is the partition code */

int partition (DATATYPE list[], int l, int h){

  int i;
  int p;          // pivot element index
  int firsthigh;  // divider position for pivot element

  // Random pivot example shown for median   p = (l+h)/2 would be used
  p = l + (short)(rand() % (int)(h - l + 1)); // Random partition point

  swap(&list[p], &list[h]);                   // Swap the values
  firsthigh = l;                                  // Hold first high value
  for (i = l; i < h; i++)
    if(list[i] < list[h]) {                 // Value at i is less than h
      swap(&list[i], &list[firsthigh]);   // So swap the value
      firsthigh++;                        // Incement first high
    }
  swap(&list[h], &list[firsthigh]);           // Swap h and first high values
  return(firsthigh);                          // Return first high
};



/* Finally the body sort */

void quicksort(DATATYPE list[], int l, int h){

  int p;                                      // index of partition 
  if ((h - l) > 0) {
    p = partition(list, l, h);              // Partition list 
    quicksort(list, l, p - 1);        // Sort lower partion
    quicksort(list, p + 1, h);              // Sort upper partition
  };
};

1

Dipende interamente da come vengono ordinati i dati all'inizio. Se pensi che sarà pseudo-casuale, la soluzione migliore è scegliere una selezione casuale o scegliere il centro.


1

Se stai ordinando una raccolta accessibile in modo casuale (come un array), è generalmente meglio scegliere l'elemento centrale fisico. Con questo, se l'array è tutto pronto (o quasi ordinato), le due partizioni saranno quasi pari e otterrai la migliore velocità.

Se stai ordinando qualcosa con solo accesso lineare (come un elenco collegato), è meglio scegliere il primo elemento, perché è l'elemento più veloce a cui accedere. Qui, tuttavia, se l'elenco è già ordinato, sei fregato: una partizione sarà sempre nulla e l'altra avrà tutto, producendo il momento peggiore.

Tuttavia, per un elenco collegato, scegliere qualcosa oltre al primo, peggiorerà le cose. Scegli l'elemento centrale in un elenco elencato, dovresti eseguirlo in ogni passaggio della partizione - aggiungendo un'operazione O (N / 2) che viene eseguita logN volte rendendo il tempo totale O (1,5 N * log N) e questo se sappiamo quanto è lungo l'elenco prima di iniziare - di solito non lo facciamo, quindi dovremmo fare un passo completo per contarli, quindi fare un passo a metà per trovare il centro, quindi passare attraverso un terza volta per eseguire la partizione effettiva: O (2.5N * log N)


0

Idealmente, il pivot dovrebbe essere il valore medio dell'intero array. Ciò ridurrà le possibilità di ottenere le prestazioni peggiori.


1
carro davanti a cavallo qui.
ncmathsadist il

0

La complessità dell'ordinamento rapido varia notevolmente con la selezione del valore pivot. ad esempio, se scegli sempre il primo elemento come pivot, la complessità dell'algoritmo diventa peggiore di O (n ^ 2). ecco un metodo intelligente per scegliere l'elemento pivot: 1. scegli il primo, medio e ultimo elemento dell'array. 2. confronta questi tre numeri e trova il numero che è maggiore di uno e minore dell'altro, cioè mediano. 3. rendere questo elemento come elemento perno.

scegliendo il pivot con questo metodo si divide l'array in quasi due metà e quindi la complessità si riduce a O (nlog (n)).


0

In media, Mediana di 3 è buona per piccoli n. Una mediana di 5 è un po 'meglio per un n. Il ninther, che è la "mediana di tre mediane di tre" è ancora meglio per n molto grandi.

Più in alto vai con il campionamento, meglio ottieni all'aumentare di n, ma il miglioramento rallenta drasticamente all'aumentare dei campioni. E incorrere nel sovraccarico di campionamento e smistamento dei campioni.


0

Consiglio di utilizzare l'indice medio, poiché può essere calcolato facilmente.

Puoi calcolarlo arrotondando (array.length / 2).


-1

In un'implementazione veramente ottimizzata, il metodo per scegliere il pivot dovrebbe dipendere dalla dimensione dell'array: per un array di grandi dimensioni, vale la pena dedicare più tempo alla scelta di un buon pivot. Senza fare un'analisi completa, immagino che "middle of O (log (n)) elements" sia un buon inizio, e questo ha il vantaggio aggiuntivo di non richiedere memoria extra: usare tail-call sulla partizione più grande e posto il partizionamento, usiamo la stessa memoria extra O (log (n)) in quasi ogni fase dell'algoritmo.


1
Trovare la metà di 3 elementi può essere fatto in tempo costante. Non di più e dobbiamo essenzialmente ordinare il sotto-array. Man mano che n diventa grande, torniamo di nuovo al problema dell'ordinamento.
Chris Cudmore
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.