Ich möchte zu den vorhandenen großartigen Antworten einige Berechnungen hinzufügen, wie QuickSort funktioniert, wenn es vom besten Fall abweicht, und wie wahrscheinlich dies ist. Ich hoffe, dass dies den Menschen hilft, ein wenig besser zu verstehen, warum der O (n ^ 2) -Fall nicht real ist Bedenken bei den komplexeren Implementierungen von QuickSort.
Abgesehen von Problemen mit wahlfreiem Zugriff gibt es zwei Hauptfaktoren, die sich auf die Leistung von QuickSort auswirken können. Beide hängen davon ab, wie der Pivot mit den zu sortierenden Daten verglichen wird.
1) Eine kleine Anzahl von Schlüsseln in den Daten. Ein Datensatz mit demselben Wert wird auf einem Vanilla 2-Partitions-QuickSort in n ^ 2-mal sortiert, da alle Werte außer der Pivot-Position jedes Mal auf einer Seite platziert werden. Moderne Implementierungen adressieren dies durch Methoden wie die Verwendung einer 3-Partitions-Sortierung. Diese Methoden werden in O (n) Zeit für einen Datensatz mit demselben Wert ausgeführt. Die Verwendung einer solchen Implementierung bedeutet also, dass eine Eingabe mit einer kleinen Anzahl von Schlüsseln tatsächlich die Leistungszeit verbessert und kein Problem mehr darstellt.
2) Eine extrem schlechte Pivot-Auswahl kann zu einer Worst-Case-Leistung führen. Im Idealfall ist der Drehpunkt immer so, dass 50% der Daten kleiner und 50% der Daten größer sind, sodass die Eingabe bei jeder Iteration in zwei Hälften geteilt wird. Dies gibt uns n Vergleiche und tauscht Zeiten log-2 (n) Rekursionen gegen O (n * logn) Zeit aus.
Inwieweit wirkt sich eine nicht ideale Pivot-Auswahl auf die Ausführungszeit aus?
Betrachten wir einen Fall, in dem der Pivot konsistent so gewählt wird, dass sich 75% der Daten auf einer Seite des Pivots befinden. Es ist immer noch O (n * logn), aber jetzt hat sich die Basis des Protokolls auf 1 / 0,75 oder 1,33 geändert. Die Beziehung in der Leistung beim Ändern der Basis ist immer eine Konstante, die durch log (2) / log (newBase) dargestellt wird. In diesem Fall beträgt diese Konstante 2,4. Diese Qualität der Pivot-Auswahl dauert also 2,4-mal länger als das Ideal.
Wie schnell wird das schlimmer?
Nicht sehr schnell, bis die Auswahl des Pivots (durchweg) sehr schlecht wird:
- 50% auf einer Seite: (Idealfall)
- 75% auf einer Seite: 2,4-mal so lang
- 90% auf einer Seite: 6,6 mal so lang
- 95% auf einer Seite: 13,5 mal so lang
- 99% auf einer Seite: 69 mal so lang
Wenn wir uns 100% auf einer Seite nähern, nähert sich der logarithmische Teil der Ausführung n und die gesamte Ausführung nähert sich asymptotisch O (n ^ 2).
In einer naiven Implementierung von QuickSort erzeugen Fälle wie ein sortiertes Array (für den Pivot des ersten Elements) oder ein Array mit umgekehrter Sortierung (für den Pivot des letzten Elements) zuverlässig eine Ausführungszeit im ungünstigsten Fall O (n ^ 2). Darüber hinaus können Implementierungen mit einer vorhersagbaren Pivot-Auswahl einem DoS-Angriff durch Daten ausgesetzt werden, die für die Ausführung im ungünstigsten Fall ausgelegt sind. Moderne Implementierungen vermeiden dies durch eine Vielzahl von Methoden, z. B. durch Randomisieren der Daten vor dem Sortieren, Auswählen des Medians von 3 zufällig ausgewählten Indizes usw. Mit dieser Randomisierung im Mix haben wir zwei Fälle:
- Kleiner Datensatz. Der schlimmste Fall ist vernünftigerweise möglich, aber O (n ^ 2) ist nicht katastrophal, da n klein genug ist, dass n ^ 2 ebenfalls klein ist.
- Großer Datensatz. Der schlimmste Fall ist theoretisch möglich, aber nicht in der Praxis.
Wie wahrscheinlich ist es, dass wir eine schreckliche Leistung sehen?
Die Chancen sind verschwindend gering . Betrachten wir eine Art von 5.000 Werten:
Unsere hypothetische Implementierung wählt einen Pivot unter Verwendung eines Medians von 3 zufällig ausgewählten Indizes. Wir werden Pivots im Bereich von 25% bis 75% als "gut" und Pivots im Bereich von 0% bis 25% oder 75% bis 100% als "schlecht" betrachten. Wenn Sie die Wahrscheinlichkeitsverteilung anhand des Medians von 3 zufälligen Indizes betrachten, hat jede Rekursion eine Chance von 11/16, einen guten Pivot zu erhalten. Lassen Sie uns zwei konservative (und falsche) Annahmen treffen, um die Mathematik zu vereinfachen:
Gute Drehpunkte sind immer genau zu 25% / 75% aufgeteilt und arbeiten im Idealfall 2,4 *. Wir bekommen nie einen idealen Split oder einen Split, der besser als 25/75 ist.
Schlechte Drehpunkte sind immer der schlimmste Fall und tragen im Wesentlichen nichts zur Lösung bei.
Unsere QuickSort-Implementierung stoppt bei n = 10 und wechselt zu einer Einfügesortierung. Daher benötigen wir 22 Pivot-Partitionen mit 25% / 75%, um die Eingabe mit 5.000 Werten so weit aufzuschlüsseln. (10 * 1.333333 ^ 22> 5000) Oder wir benötigen 4990 Worst-Case-Pivots. Denken Sie daran, dass, wenn wir zu irgendeinem Zeitpunkt 22 gute Drehpunkte sammeln, die Sortierung abgeschlossen ist. Der schlimmste Fall oder etwas in der Nähe erfordert daher extrem viel Pech. Wenn wir 88 Rekursionen benötigen würden, um tatsächlich die 22 guten Drehpunkte zu erreichen, die erforderlich sind, um auf n = 10 zu sortieren, wäre dies ein 4 * 2,4 * Idealfall oder etwa das 10-fache der Ausführungszeit des Idealfalls. Wie wahrscheinlich ist es, dass wir nach 88 Rekursionen nicht die erforderlichen 22 guten Drehpunkte erreichen?
Binomiale Wahrscheinlichkeitsverteilungen können darauf antworten, und die Antwort ist ungefähr 10 ^ -18. (n ist 88, k ist 21, p ist 0,6875) Ihr Benutzer wird in der 1 Sekunde, die zum Klicken auf [SORTIEREN] benötigt wird, ungefähr tausendmal häufiger vom Blitz getroffen, als zu sehen, dass die Sortierung von 5.000 Elementen schlechter läuft als 10 * Idealfall. Diese Chance wird kleiner, wenn der Datensatz größer wird. Hier sind einige Array-Größen und ihre entsprechenden Chancen, länger als 10 * zu laufen, ideal:
- Array von 640 Elementen: 10 ^ -13 (erfordert 15 gute Drehpunkte aus 60 Versuchen)
- Array von 5.000 Elementen: 10 ^ -18 (erfordert 22 gute Pivots von 88 Versuchen)
- Array von 40.000 Elementen: 10 ^ -23 (erfordert 29 gute Drehpunkte von 116)
Denken Sie daran, dass dies mit zwei konservativen Annahmen geschieht, die schlechter als die Realität sind. Die tatsächliche Leistung ist also noch besser, und das Gleichgewicht der verbleibenden Wahrscheinlichkeit ist näher am Ideal als nicht.
Schließlich können, wie andere bereits erwähnt haben, selbst diese absurd unwahrscheinlichen Fälle durch Umschalten auf eine Heap-Sortierung beseitigt werden, wenn der Rekursionsstapel zu tief geht. Das TLDR ist also, dass für gute Implementierungen von QuickSort der schlimmste Fall nicht wirklich existiert, da er ausgearbeitet wurde und die Ausführung in O (n * logn) Zeit abgeschlossen ist.
qsort
, Python'slist.sort
und dasArray.prototype.sort
in Firefox's JavaScript sind allesamt aufgemotzte Zusammenführungssorten. (GNU STLsort
verwendet stattdessen Introsort, aber das könnte daran liegen, dass in C ++ das Austauschen möglicherweise