Quicksort: Auswahl des Pivots

109

Bei der Implementierung von Quicksort müssen Sie unter anderem einen Pivot auswählen. Aber wenn ich mir Pseudocode wie den folgenden ansehe, ist nicht klar, wie ich den Pivot wählen soll. Erstes Element der Liste? Etwas anderes?

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

Kann mir jemand helfen, das Konzept der Auswahl eines Pivots zu verstehen und ob unterschiedliche Szenarien unterschiedliche Strategien erfordern oder nicht.

Jacob T. Nielsen
quelle

Antworten:

87

Wenn Sie einen zufälligen Pivot auswählen, wird die Wahrscheinlichkeit minimiert, dass Sie auf die O (n 2 ) -Leistung im ungünstigsten Fall stoßen (immer die erste oder letzte Auswahl führt zu einer Worst-Case-Leistung für nahezu sortierte oder nahezu umgekehrt sortierte Daten). Die Wahl des mittleren Elements wäre in den meisten Fällen ebenfalls akzeptabel.

Wenn Sie dies selbst implementieren, gibt es auch Versionen des Algorithmus, die direkt funktionieren (dh ohne zwei neue Listen zu erstellen und diese dann zu verketten).

Pennen
quelle
10
Ich würde der Vorstellung zustimmen, dass die Implementierung einer Suche selbst möglicherweise nicht die Mühe wert ist. Seien Sie auch vorsichtig, wie Sie Zufallszahlen auswählen, da Zufallszahlengeneratoren manchmal etwas langsam sind.
PeterAllenWebb
@ Jonathan Lefflers Antwort ist besser
Nathan
60

Das hängt von Ihren Anforderungen ab. Durch die zufällige Auswahl eines Pivots wird es schwieriger, einen Datensatz zu erstellen, der eine O (N ^ 2) -Leistung generiert. 'Median-of-Three' (erster, letzter, mittlerer) ist auch ein Weg, um Probleme zu vermeiden. Achten Sie jedoch auf die relative Leistung von Vergleichen. Wenn Ihre Vergleiche kostspielig sind, führt Mo3 mehr Vergleiche durch als die zufällige Auswahl (eines einzelnen Pivot-Werts). Der Vergleich von Datenbankeinträgen kann kostspielig sein.


Update: Kommentare in die Antwort ziehen.

mdkess behauptete:

'Median von 3' ist NICHT die erste letzte Mitte. Wählen Sie drei zufällige Indizes und nehmen Sie den Mittelwert davon. Der springende Punkt ist, sicherzustellen, dass Ihre Auswahl der Drehpunkte nicht deterministisch ist. Wenn dies der Fall ist, können Worst-Case-Daten ganz einfach generiert werden.

Worauf ich geantwortet habe:

  • Die Analyse von Hoares Suchalgorithmus mit Median-of-Three-Partition (1997) von P. Kirschenhofer, H. Prodinger, C. Martínez unterstützt Ihre Behauptung (der „Median-of-Three“ besteht aus drei zufälligen Elementen).

  • Auf portal.acm.org ist ein Artikel beschrieben , der sich mit "Die Worst-Case-Permutation für den Median-of-Three-Quicksort" von Hannu Erkiö befasst und im Computer Journal, Band 27, Nr. 3, 1984, veröffentlicht wurde. [Update 2012-02- 26: Habe den Text für den Artikel . Abschnitt 2 'Der Algorithmus' beginnt: ' Mit dem Median des ersten, mittleren und letzten Elements von A [L: R] können in den meisten praktischen Situationen effiziente Partitionen in Teile von ziemlich gleicher Größe erzielt werden. 'Daher wird der erste-mittlere-letzte Mo3-Ansatz diskutiert.]

  • Ein weiterer interessanter interessanter Artikel ist von MD McIlroy, "A Killer Adversary for Quicksort" , veröffentlicht in Software-Practice and Experience, Vol. 3, No. 29 (0), 1–4 (0 1999). Es wird erklärt, wie sich fast jeder Quicksort quadratisch verhält.

  • AT & T Bell Labs Tech Journal, Oktober 1984 "Theorie und Praxis bei der Konstruktion einer funktionierenden Sortierroutine" besagt "Hoare schlug vor, um den Median mehrerer zufällig ausgewählter Linien zu partitionieren. Sedgewick empfahl [...], den Median der ersten [. ..] letzte [...] und mittlere ". Dies weist darauf hin, dass beide Techniken für den "Median-of-Three" in der Literatur bekannt sind. (Update 23.11.2014: Der Artikel scheint bei IEEE Xplore oder bei Wiley erhältlich zu sein - wenn Sie Mitglied sind oder bereit sind, eine Gebühr zu zahlen.)

  • 'Engineering a Sort Function' von JL Bentley und MD McIlroy, veröffentlicht in Software Practice and Experience, Band 23 (11), November 1993, geht auf eine ausführliche Diskussion der Probleme ein und wählte einen adaptiven Partitionierungsalgorithmus, der teilweise auf dem basiert Größe des Datensatzes. Es gibt viele Diskussionen über Kompromisse für verschiedene Ansätze.

  • Eine Google-Suche nach "Median-of-Three" funktioniert ziemlich gut für die weitere Verfolgung.

Danke für die Auskunft; Ich war zuvor nur auf den deterministischen 'Median-of-Three' gestoßen.

Jonathan Leffler
quelle
4
Der Median von 3 ist NICHT die erste letzte Mitte. Wählen Sie drei zufällige Indizes und nehmen Sie den Mittelwert davon. Der springende Punkt ist, sicherzustellen, dass Ihre Auswahl der Drehpunkte nicht deterministisch ist. Wenn dies der Fall ist, können Worst-Case-Daten ganz einfach generiert werden.
Mindvirus
Ich habe abt introsort gelesen, das gute Eigenschaften von Quicksort und Heapsort kombiniert. Der Ansatz zur Auswahl des Pivots unter Verwendung des Medians von drei ist möglicherweise nicht immer günstig.
Sumit Kumar Saha
4
Das Problem bei der Auswahl von Zufallsindizes ist, dass Zufallszahlengeneratoren ziemlich teuer sind. Dies erhöht zwar nicht die hohen O-Kosten für das Sortieren, macht die Dinge jedoch wahrscheinlich langsamer, als wenn Sie gerade das erste, letzte und mittlere Element ausgewählt hätten. (Ich wette, in der realen Welt macht niemand erfundene Situationen, um Ihre schnelle Sortierung zu verlangsamen.)
Kevin Chen
20

Heh, ich habe gerade diese Klasse unterrichtet.

Es gibt mehrere Möglichkeiten.
Einfach: Wählen Sie das erste oder letzte Element des Bereichs aus. (schlecht bei teilweise sortierter Eingabe) Besser: Wählen Sie den Artikel in der Mitte des Bereichs aus. (besser bei teilweise sortierter Eingabe)

Bei Auswahl eines beliebigen Elements besteht jedoch die Gefahr, dass das Array der Größe n schlecht in zwei Arrays der Größe 1 und n-1 aufgeteilt wird. Wenn Sie dies oft genug tun, läuft Ihr Quicksort Gefahr, O (n ^ 2) zu werden.

Eine Verbesserung, die ich gesehen habe, ist der Auswahlmedian (erster, letzter, mittlerer); Im schlimmsten Fall kann es immer noch zu O (n ^ 2) gehen, aber wahrscheinlich ist dies ein seltener Fall.

Für die meisten Daten ist es ausreichend, die erste oder letzte auszuwählen. Wenn Sie jedoch feststellen, dass Sie häufig auf Worst-Case-Szenarien stoßen (teilweise sortierte Eingabe), besteht die erste Option darin, den zentralen Wert auszuwählen (was ein statistisch guter Dreh- und Angelpunkt für teilweise sortierte Daten ist).

Wenn Sie immer noch auf Probleme stoßen, gehen Sie den Medianweg.

Chris Cudmore
quelle
1
Wir haben in unserer Klasse ein Experiment durchgeführt, bei dem die k kleinsten Elemente aus einem Array in sortierter Reihenfolge ermittelt wurden. Wir haben zufällige Arrays generiert, dann entweder einen Min-Heap oder eine zufällige Auswahl und einen festen Pivot-Quicksort verwendet und die Anzahl der Vergleiche gezählt. Bei diesen "zufälligen" Daten schnitt die zweite Lösung im Durchschnitt schlechter ab als die erste. Das Wechseln zu einem zufälligen Pivot löst das Leistungsproblem. Selbst bei vermeintlich zufälligen Daten ist der feste Drehpunkt deutlich schlechter als der zufällige Drehpunkt.
Robert S. Barnes
Warum besteht bei der Aufteilung des Arrays der Größe n in zwei Arrays der Größe 1 und n-1 die Gefahr, dass O (n ^ 2) wird?
Aaron Franke
Nehmen Sie ein Array der Größe N an. Partitionierung in die Größen [1, N-1]. Der nächste Schritt ist die Aufteilung der rechten Hälfte in [1, N-2]. und so weiter, bis wir N Partitionen der Größe 1 haben. Wenn wir jedoch in zwei Hälften partitionieren würden, würden wir jeden Schritt 2 Partitionen von N / 2 durchführen, was zum Log (n) -Term der Komplexität führt;
Chris Cudmore
11

Wählen Sie niemals einen festen Drehpunkt - dieser kann angegriffen werden, um die O (n ^ 2) -Laufzeit Ihres Algorithmus im ungünstigsten Fall auszunutzen, die nur nach Problemen fragt. Die Worst-Case-Laufzeit von Quicksort tritt auf, wenn die Partitionierung zu einem Array mit 1 Element und einem Array mit n-1 Elementen führt. Angenommen, Sie wählen das erste Element als Partition. Wenn jemand Ihrem Algorithmus ein Array in absteigender Reihenfolge zuführt, ist Ihr erster Pivot der größte, sodass alles andere im Array links davon verschoben wird. Wenn Sie dann wiederkehren, ist das erste Element wieder das größte, also setzen Sie noch einmal alles links davon und so weiter.

Eine bessere Technik ist die Median-of-3-Methode, bei der Sie drei Elemente zufällig auswählen und die Mitte auswählen. Sie wissen, dass das Element, das Sie auswählen, nicht das erste oder das letzte ist, aber nach dem zentralen Grenzwertsatz ist die Verteilung des mittleren Elements normal, was bedeutet, dass Sie zur Mitte (und damit) tendieren , n lg n Zeit).

Wenn Sie unbedingt die O (nlgn) -Laufzeit für den Algorithmus garantieren möchten, wird die Spalten-zu-5-Methode zum Ermitteln des Medians eines Arrays in O (n) -Zeit ausgeführt, was bedeutet, dass die Wiederholungsgleichung für Quicksort im schlimmsten Fall verwendet wird sei T (n) = O (n) (finde den Median) + O (n) (Partition) + 2T (n / 2) (rekursiere links und rechts). Nach dem Hauptsatz ist dies O (n lg n) . Der konstante Faktor wird jedoch sehr groß sein. Wenn die Leistung im ungünstigsten Fall Ihr Hauptanliegen ist, verwenden Sie stattdessen eine Zusammenführungssortierung, die im Durchschnitt nur ein wenig langsamer als Quicksort ist und die O (nlgn) -Zeit garantiert (und viel schneller ist) als dieser lahme Median Quicksort).

Erklärung des Median-of-Medians-Algorithmus

Mindvirus
quelle
6

Versuchen Sie nicht, zu schlau zu werden und Pivot-Strategien zu kombinieren. Wenn Sie den Median von 3 mit dem zufälligen Pivot kombiniert haben, indem Sie den Median des ersten, letzten und eines zufälligen Index in der Mitte ausgewählt haben, sind Sie immer noch anfällig für viele der Verteilungen, die einen Median von 3 quadratisch senden (also ist er tatsächlich schlechter als einfacher zufälliger Drehpunkt)

ZB ist eine Pfeifenorgelverteilung (1,2,3 ... N / 2..3,2,1) zuerst und zuletzt beide 1 und der Zufallsindex ist eine Zahl größer als 1, wobei der Median 1 ergibt ( entweder zuerst oder zuletzt) ​​und Sie erhalten eine extrem unausgeglichene Partitionierung.

Papierpferd
quelle
2

Dabei ist es einfacher, den Quicksort in drei Abschnitte zu unterteilen

  1. Datenelementfunktion austauschen oder austauschen
  2. Die Partitionsfunktion
  3. Verarbeiten der Partitionen

Es ist nur geringfügig ineffizienter als eine lange Funktion, aber viel einfacher zu verstehen.

Code folgt:

/* 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
  };
};
Uglybb
quelle
1

Es hängt ganz davon ab, wie Ihre Daten zunächst sortiert sind. Wenn Sie glauben, dass es pseudozufällig ist, wählen Sie am besten entweder eine zufällige Auswahl oder die Mitte.

Joe Phillips
quelle
1

Wenn Sie eine zufällig zugängliche Sammlung (wie ein Array) sortieren, ist es im Allgemeinen am besten, das physische mittlere Element auszuwählen. Wenn das Array fertig sortiert (oder fast sortiert) ist, sind die beiden Partitionen nahezu gleich und Sie erhalten die beste Geschwindigkeit.

Wenn Sie etwas mit nur linearem Zugriff sortieren (z. B. eine verknüpfte Liste), wählen Sie am besten das erste Element aus, da es das schnellste Element ist, auf das zugegriffen werden kann. Wenn die Liste hier jedoch bereits sortiert ist, sind Sie fertig - eine Partition ist immer null und die andere hat alles, was die schlechteste Zeit ergibt.

Wenn Sie jedoch für eine verknüpfte Liste etwas anderes als die erste auswählen, wird die Sache nur noch schlimmer. Wenn Sie das mittlere Element in einer Liste auswählen, müssen Sie es bei jedem Partitionsschritt schrittweise durchlaufen. Fügen Sie eine O (N / 2) -Operation hinzu, die logN-mal ausgeführt wird, wodurch die Gesamtzeit O (1,5 N * log N) ergibt. und das ist, wenn wir wissen, wie lang die Liste ist, bevor wir anfangen - normalerweise tun wir das nicht, also müssten wir den ganzen Weg durchlaufen, um sie zu zählen, dann auf halbem Weg, um die Mitte zu finden, dann durch a drittes Mal, um die eigentliche Partition durchzuführen: O (2.5N * log N)

James Curran
quelle
0

Idealerweise sollte der Pivot der mittlere Wert im gesamten Array sein. Dies verringert die Wahrscheinlichkeit einer Worst-Case-Leistung.

Faizan
quelle
1
Karren vor Pferd hier.
ncmathsadist
0

Die Komplexität der schnellen Sortierung hängt stark von der Auswahl des Pivot-Werts ab. Wenn Sie beispielsweise immer das erste Element als Drehpunkt auswählen, wird die Komplexität des Algorithmus so schlecht wie O (n ^ 2). Hier ist eine intelligente Methode zur Auswahl des Pivot-Elements: 1. Wählen Sie das erste, mittlere und letzte Element des Arrays. 2. Vergleichen Sie diese drei Zahlen und finden Sie die Zahl, die größer als eine und kleiner als die andere ist, dh der Median. 3. Machen Sie dieses Element als Pivot-Element.

Durch Auswahl des Pivots nach dieser Methode wird das Array in fast zwei Hälften geteilt, und daher reduziert sich die Komplexität auf O (nlog (n)).

vivek
quelle
0

Im Durchschnitt ist der Median von 3 gut für kleine n. Der Median von 5 ist etwas besser für größere n. Der neunte, der "Median von drei Medianen von drei" ist, ist für sehr große n sogar noch besser.

Je höher Sie mit dem Sampling sind, desto besser werden Sie, wenn n zunimmt, aber die Verbesserung verlangsamt sich dramatisch, wenn Sie das Sampling erhöhen. Und Sie müssen die Proben entnehmen und sortieren.

S0lo
quelle
0

Ich empfehle die Verwendung des mittleren Index, da dieser leicht berechnet werden kann.

Sie können es durch Runden berechnen (array.length / 2).

Milesman34
quelle
-1

In einer wirklich optimierten Implementierung sollte die Methode zur Auswahl des Pivots von der Arraygröße abhängen. Bei einem großen Array lohnt es sich, mehr Zeit für die Auswahl eines guten Pivots aufzuwenden. Ohne eine vollständige Analyse würde ich vermuten, dass "Mitte von O (log (n)) Elementen" ein guter Anfang ist, und dies hat den zusätzlichen Vorteil, dass kein zusätzlicher Speicher benötigt wird: Verwenden von Tail-Call auf der größeren Partition und In Bei der Partitionierung verwenden wir in fast jeder Phase des Algorithmus denselben zusätzlichen O-Speicher (log (n)).

Morten Kloster
quelle
1
Das Finden der Mitte von 3 Elementen kann in konstanter Zeit erfolgen. Noch mehr, und wir müssen im Wesentlichen das Sub-Array sortieren. Wenn n groß wird, stoßen wir wieder auf das Sortierproblem.
Chris Cudmore