Quadsort-Sortieralgorithmus

Einführung



Dieser Artikel beschreibt einen stabilen nicht rekursiven adaptiven Zusammenführungssortieralgorithmus namens quadsort.



Vierfacher Austausch



Quadsort basiert auf einem Vierfachaustausch. Traditionell basieren die meisten Sortieralgorithmen auf einem binären Austausch, bei dem zwei Variablen unter Verwendung einer dritten temporären Variablen sortiert werden. Es sieht normalerweise so aus:



    if (val[0] > val[1])
    {
        tmp[0] = val[0];
        val[0] = val[1];
        val[1] = tmp[0];
    }


Bei einem vierfachen Austausch erfolgt die Sortierung unter Verwendung von vier Substitutionsvariablen (Swap). Im ersten Schritt werden die vier Variablen teilweise in vier Swap-Variablen sortiert, im zweiten Schritt werden sie vollständig in die vier ursprünglichen Variablen zurücksortiert.





Dieser Vorgang ist im obigen Diagramm dargestellt.



Nach der ersten Sortierrunde bestimmt eine if-Prüfung, ob die vier Swap-Variablen der Reihe nach sortiert sind. In diesem Fall endet der Austausch sofort. Anschließend wird geprüft, ob die Swap-Variablen in umgekehrter Reihenfolge sortiert sind. Wenn ja, endet die Sortierung sofort. Wenn beide Tests fehlschlagen, ist der endgültige Ort bekannt und es verbleiben zwei Tests, um die endgültige Reihenfolge zu bestimmen.



Dies eliminiert einen redundanten Vergleich für Sequenzen, die in Ordnung sind. Gleichzeitig wird ein zusätzlicher Vergleich für zufällige Sequenzen erstellt. In der realen Welt vergleichen wir jedoch selten wirklich zufällige Daten. Wenn die Daten also eher geordnet als unordentlich sind, ist diese Verschiebung der Wahrscheinlichkeit von Vorteil.



Es gibt auch eine allgemeine Leistungsverbesserung durch Eliminieren des verschwenderischen Austauschs. In der Sprache C sieht ein einfacher Vierfach-Swap folgendermaßen aus:



    if (val[0] > val[1])
    {
        tmp[0] = val[1];
        tmp[1] = val[0];
    }
    else
    {
        tmp[0] = val[0];
        tmp[1] = val[1];
    }

    if (val[2] > val[3])
    {
        tmp[2] = val[3];
        tmp[3] = val[2];
    }
    else
    {
        tmp[2] = val[2];
        tmp[3] = val[3];
    }

    if (tmp[1] <= tmp[2])
    {
        val[0] = tmp[0];
        val[1] = tmp[1];
        val[2] = tmp[2];
        val[3] = tmp[3];
    }
    else if (tmp[0] > tmp[3])
    {
        val[0] = tmp[2];
        val[1] = tmp[3];
        val[2] = tmp[0];
        val[3] = tmp[1];
    }
    else
    {
       if (tmp[0] <= tmp[2])
       {
           val[0] = tmp[0];
           val[1] = tmp[2];
       }
       else
       {
           val[0] = tmp[2];
           val[1] = tmp[0];
       }

       if (tmp[1] <= tmp[3])
       {
           val[2] = tmp[1];
           val[3] = tmp[3];
       }
       else
       {
           val[2] = tmp[3];
           val[3] = tmp[1];
       }
    }


Falls das Array nicht perfekt in vier Teile unterteilt werden kann, wird das Ende von 1-3 Elementen unter Verwendung des herkömmlichen binären Austauschs sortiert.



Der oben beschriebene Vierfachaustausch ist in Quadsort implementiert.



Vierfache Zusammenführung



In der ersten Stufe sortiert der Vierfachaustausch das Array wie oben beschrieben in Blöcke mit vier Elementen vor.



Die zweite Stufe verwendet einen ähnlichen Ansatz zum Erkennen geordneter und umgekehrter Ordnungen, aber in Blöcken mit 4, 16, 64 oder mehr Elementen wird diese Stufe wie eine herkömmliche Zusammenführungssortierung behandelt.



Dies kann wie folgt dargestellt werden:



main memory: AAAA BBBB CCCC DDDD

swap memory: ABABABAB  CDCDCDCD

main memory: ABCDABCDABCDABCD


In der ersten Zeile werden mit einem vierfachen Tausch vier Blöcke mit jeweils vier sortierten Elementen erstellt. In der zweiten Zeile wird eine Quad-Zusammenführung verwendet, um zwei Blöcke mit jeweils acht sortierten Elementen im Swap-Speicher zu kombinieren. In der letzten Zeile werden die Blöcke wieder in den Hauptspeicher zusammengeführt, und es bleibt ein Block mit 16 sortierten Elementen übrig. Unten ist die Visualisierung.







Diese Operationen erfordern tatsächlich eine Verdoppelung des Auslagerungsspeichers. Dazu später mehr.



Überspringen



Ein weiterer Unterschied besteht darin, dass es aufgrund der erhöhten Kosten für Zusammenführungsvorgänge vorteilhaft ist, zu überprüfen, ob vier Blöcke in Vorwärts- oder Rückwärtsreihenfolge sind.



Wenn die vier Blöcke in Ordnung sind, wird der Zusammenführungsvorgang übersprungen, da er bedeutungslos ist. Dies erfordert jedoch eine zusätzliche if-Prüfung, und für zufällig sortierte Daten wird diese if-Prüfung mit zunehmender Blockgröße zunehmend unwahrscheinlicher. Glücklicherweise vervierfacht sich die Häufigkeit dieser Prüfung in jedem Zyklus, während sich der potenzielle Nutzen in jedem Zyklus vervierfacht.



Falls die vier Blöcke in umgekehrter Reihenfolge sind, wird ein stabiler In-Place-Swap durchgeführt.



Für den Fall, dass nur zwei der vier Blöcke geordnet oder in umgekehrter Reihenfolge sind, sind Vergleiche in der Zusammenführung selbst nicht erforderlich und werden anschließend weggelassen. Die Daten müssen noch in den Auslagerungsspeicher kopiert werden, dies ist jedoch ein weniger rechnerischer Vorgang.



Dies ermöglicht es dem Quadsort-Algorithmus, Sequenzen in Vorwärts- und Rückwärtsreihenfolge unter Verwendung von nVergleichen anstelle von n * log nVergleichen zu sortieren .



Grenzkontrollen



Ein weiteres Problem bei der herkömmlichen Zusammenführungssortierung besteht darin, dass Ressourcen für unnötige Grenzprüfungen verschwendet werden. Es sieht aus wie das:



    while (a <= a_max && b <= b_max)
        if (a <= b)
            [insert a++]
        else
            [insert b++]


Zur Optimierung vergleicht unser Algorithmus das letzte Element der Sequenz A mit dem letzten Element der Sequenz B. Wenn das letzte Element der Sequenz A kleiner als das letzte Element der Sequenz B ist, wissen wir, dass der Test b < b_maximmer falsch ist, da die Sequenz A das erste ist, das vollständig zusammengeführt wird.



Wenn das letzte Element der Sequenz A größer als das letzte Element der Sequenz B ist, wissen wir ebenfalls, dass der Test a < a_maximmer falsch ist. Es sieht aus wie das:



    if (val[a_max] <= val[b_max])
        while (a <= a_max)
        {
            while (a > b)
                [insert b++]
            [insert a++]
        }
    else
        while (b <= b_max)
        {
            while (a <= b)
                [insert a++]
            [insert b++]
        }


Fusionsschwanz



Wenn Sie ein Array mit 65 Elementen sortieren, erhalten Sie am Ende ein Array mit 64 Elementen und am Ende ein Array mit einem Element. Dies führt nicht zu einem zusätzlichen Austauschvorgang, wenn die gesamte Sequenz in Ordnung ist. In jedem Fall muss das Programm dazu die optimale Arraygröße wählen (64, 256 oder 1024).



Ein weiteres Problem besteht darin, dass die suboptimale Größe des Arrays zu unnötigen Austauschoperationen führt. Um diese beiden Probleme zu umgehen, wird die vierfache Zusammenführungsprozedur abgebrochen, wenn die Blockgröße 1/8 der Arraygröße erreicht und der Rest des Arrays mithilfe der Endzusammenführung sortiert wird. Der Hauptvorteil der Schwanzfusion besteht darin, dass der Quadsort-Swap auf n / 2 reduziert werden kann, ohne die Leistung wesentlich zu beeinträchtigen.



Big O.



Name Beste Mitte Am schlimmsten Stabil Erinnerung
Quadsort n n log n n log n Ja n


Wenn die Daten bereits sortiert oder in umgekehrter Reihenfolge sortiert sind, führt quadsort n Vergleiche durch.



Zwischenspeicher



Da quadsort n / 2 Swaps verwendet, ist die Cache-Nutzung nicht so ideal wie die direkte Sortierung. Das Sortieren zufälliger Daten führt jedoch zu einem nicht optimalen Austausch. Basierend auf meinen Benchmarks ist quadsort immer schneller als die direkte Sortierung, solange das Array den L1-Cache nicht überläuft, der auf modernen Prozessoren bis zu 64 KB betragen kann.



Wolfsort



Wolfsort ist ein hybrider Radixsort / Quadsort-Sortieralgorithmus, der die Leistung beim Arbeiten mit zufälligen Daten verbessert. Dies ist im Grunde ein Proof of Concept, der nur mit vorzeichenlosen 32- und 64-Bit-Ganzzahlen funktioniert.



Visualisierung



Die folgende Visualisierung führt vier Tests aus. Der erste Test basiert auf einer zufälligen Verteilung, der zweite auf einer aufsteigenden Verteilung, der dritte auf einer absteigenden Verteilung und der vierte auf einer aufsteigenden Verteilung mit einem zufälligen Schwanz.



Die obere Hälfte zeigt Swap und die untere Hälfte zeigt den Hauptspeicher. Die Farben variieren beim Überspringen, Austauschen, Zusammenführen und Kopieren.







Benchmark: quadsort, std :: stabile_sort, timsort, pdqsort und wolfsort



Der folgende Benchmark wurde mit der WSL gcc-Konfigurationsversion 7.4.0 (Ubuntu 7.4.0-1ubuntu1 ~ 18.04.1) ausgeführt. Der Quellcode wird vom Team zusammengestellt g++ -O3 quadsort.cpp. Jeder Test wird hundertmal ausgeführt und nur der beste Lauf wird gemeldet.



Die Abszisse ist die Ausführungszeit.







Quadsort-Datenblatt, std :: stabile_sort, timsort, pdqsort und wolfsort
Name Elemente Eine Art Beste Mitte Vergleiche Bestellung
Quadsort 1.000.000 i32 0,070469 0,070635 zufällige Reihenfolge
stablesort 1000000 i32 0.073865 0.074078
timsort 1000000 i32 0.089192 0.089301
pdqsort 1000000 i32 0.029911 0.029948
wolfsort 1000000 i32 0.017359 0.017744
quadsort 1000000 i32 0.000485 0.000511
stablesort 1000000 i32 0.010188 0.010261
timsort 1000000 i32 0.000331 0.000342
pdqsort 1000000 i32 0.000863 0.000875
wolfsort 1000000 i32 0.000539 0.000579
quadsort 1000000 i32 0.008901 0.008934 ()
stablesort 1000000 i32 0.017093 0.017275 ()
timsort 1000000 i32 0.008615 0.008674 ()
pdqsort 1000000 i32 0.047785 0.047921 ()
wolfsort 1000000 i32 0.012490 0.012554 ()
quadsort 1000000 i32 0.038260 0.038343
stablesort 1000000 i32 0.042292 0.042388
timsort 1000000 i32 0.055855 0.055967
pdqsort 1000000 i32 0.008093 0.008168
wolfsort 1000000 i32 0.038320 0.038417
quadsort 1000000 i32 0.000559 0.000576
stablesort 1000000 i32 0.010343 0.010438
timsort 1000000 i32 0.000891 0.000900
pdqsort 1000000 i32 0.001882 0.001897
wolfsort 1000000 i32 0.000604 0.000625
quadsort 1000000 i32 0.006174 0.006245
stablesort 1000000 i32 0.014679 0.014767
timsort 1000000 i32 0.006419 0.006468
pdqsort 1000000 i32 0.016603 0.016629
wolfsort 1000000 i32 0.006264 0.006329
quadsort 1000000 i32 0.018675 0.018729
stablesort 1000000 i32 0.026384 0.026508
timsort 1000000 i32 0.023226 0.023395
pdqsort 1000000 i32 0.028599 0.028674
wolfsort 1000000 i32 0.009517 0.009631
quadsort 1000000 i32 0.037593 0.037679
stablesort 1000000 i32 0.043755 0.043899
timsort 1000000 i32 0.047008 0.047112
pdqsort 1000000 i32 0.029800 0.029847
wolfsort 1000000 i32 0.013238 0.013355
quadsort 1000000 i32 0.009605 0.009673
stablesort 1000000 i32 0.013667 0.013785
timsort 1000000 i32 0.007994 0.008138
pdqsort 1000000 i32 0.024683 0.024727
wolfsort 1000000 i32 0.009642 0.009709


Es ist zu beachten, dass pdqsort keine stabile Sortierung ist und daher schneller mit Daten in normaler Reihenfolge (allgemeine Reihenfolge) arbeitet.



Der folgende Benchmark wurde auf WSL gcc Version 7.4.0 (Ubuntu 7.4.0-1ubuntu1 ~ 18.04.1) ausgeführt. Der Quellcode wird vom Team zusammengestellt g++ -O3 quadsort.cpp. Jeder Test wird hundertmal ausgeführt und nur der beste Lauf wird gemeldet. Es misst die Leistung auf Arrays mit 675 bis 100.000 Elementen.



Die x-Achse des folgenden Diagramms ist die Kardinalität, die y-Achse ist die Ausführungszeit.







Quadsort-Datenblatt, std :: stabile_sort, timsort, pdqsort und wolfsort
quadsort 100000 i32 0.005800 0.005835
stablesort 100000 i32 0.006092 0.006122
timsort 100000 i32 0.007605 0.007656
pdqsort 100000 i32 0.002648 0.002670
wolfsort 100000 i32 0.001148 0.001168
quadsort 10000 i32 0.004568 0.004593
stablesort 10000 i32 0.004813 0.004923
timsort 10000 i32 0.006326 0.006376
pdqsort 10000 i32 0.002345 0.002373
wolfsort 10000 i32 0.001256 0.001274
quadsort 5000 i32 0.004076 0.004086
stablesort 5000 i32 0.004394 0.004420
timsort 5000 i32 0.005901 0.005938
pdqsort 5000 i32 0.002093 0.002107
wolfsort 5000 i32 0.000968 0.001086
quadsort 2500 i32 0.003196 0.003303
stablesort 2500 i32 0.003801 0.003942
timsort 2500 i32 0.005296 0.005322
pdqsort 2500 i32 0.001606 0.001661
wolfsort 2500 i32 0.000509 0.000520
quadsort 1250 i32 0.001767 0.001823
stablesort 1250 i32 0.002812 0.002887
timsort 1250 i32 0.003789 0.003865
pdqsort 1250 i32 0.001036 0.001325
wolfsort 1250 i32 0.000534 0.000655
quadsort 675 i32 0.000368 0.000592
stablesort 675 i32 0.000974 0.001160
timsort 675 i32 0.000896 0.000948
pdqsort 675 i32 0.000489 0.000531
wolfsort 675 i32 0.000283 0.000308


Benchmark: Quadsort und Qsort (Mergesort)



Der nächste Benchmark wurde auf WSL gcc Version 7.4.0 (Ubuntu 7.4.0-1ubuntu1 ~ 18.04.1) ausgeführt. Der Quellcode wird vom Team zusammengestellt g++ -O3 quadsort.cpp. Jeder Test wird hundertmal ausgeführt und nur der beste Lauf wird gemeldet.



         quadsort: sorted 1000000 i32s in 0.119437 seconds. MO:   19308657 ( )
            qsort: sorted 1000000 i32s in 0.133077 seconds. MO:   21083741 ( )

         quadsort: sorted 1000000 i32s in 0.002071 seconds. MO:     999999 ()
            qsort: sorted 1000000 i32s in 0.007265 seconds. MO:    3000004 ()

         quadsort: sorted 1000000 i32s in 0.019239 seconds. MO:    4007580 ( )
            qsort: sorted 1000000 i32s in 0.071322 seconds. MO:   20665677 ( )

         quadsort: sorted 1000000 i32s in 0.076605 seconds. MO:   19242642 ( )
            qsort: sorted 1000000 i32s in 0.038389 seconds. MO:    6221917 ( )

         quadsort: sorted 1000000 i32s in 0.002305 seconds. MO:     999999 ()
            qsort: sorted 1000000 i32s in 0.009659 seconds. MO:    4000015 ()

         quadsort: sorted 1000000 i32s in 0.022940 seconds. MO:    9519209 ( )
            qsort: sorted 1000000 i32s in 0.042135 seconds. MO:   13152042 ( )

         quadsort: sorted 1000000 i32s in 0.034456 seconds. MO:    6787656 ( )
            qsort: sorted 1000000 i32s in 0.098691 seconds. MO:   20584424 ( )

         quadsort: sorted 1000000 i32s in 0.066240 seconds. MO:   11383441 ( )
            qsort: sorted 1000000 i32s in 0.118086 seconds. MO:   20572142 ( )

         quadsort: sorted 1000000 i32s in 0.038116 seconds. MO:   15328606 ( )
            qsort: sorted 1000000 i32s in 4.471858 seconds. MO: 1974047339 ( )

         quadsort: sorted 1000000 i32s in 0.060989 seconds. MO:   15328606 ()
            qsort: sorted 1000000 i32s in 0.043175 seconds. MO:   10333679 ()

         quadsort: sorted    1023 i32s in 0.016126 seconds.       (random 1-1023)
            qsort: sorted    1023 i32s in 0.033132 seconds.       (random 1-1023)


MO gibt die Anzahl der Vergleiche an, die mit 1 Million Artikeln durchgeführt wurden.



Der obige Benchmark vergleicht quadsort mit glibc qsort () über dieselbe Allzweckschnittstelle und ohne bekannte unfaire Vorteile wie Inlining.



     random order: 635, 202,  47, 229, etc
  ascending order: 1, 2, 3, 4, etc
    uniform order: 1, 1, 1, 1, etc
 descending order: 999, 998, 997, 996, etc
       wave order: 100, 1, 102, 2, 103, 3, etc
  stable/unstable: 100, 1, 102, 1, 103, 1, etc
     random range: time to sort 1000 arrays ranging from size 0 to 999 containing random data


Benchmark: Quadsort und Qsort (Quicksort)



Dieser spezielle Test wird mit der qsort () - Implementierung von Cygwin durchgeführt, die Quicksort unter der Haube verwendet. Der Quellcode wird vom Team zusammengestellt gcc -O3 quadsort.c. Jeder Test wird hundertmal ausgeführt, es wird nur der beste Lauf gemeldet.



         quadsort: sorted 1000000 i32s in 0.119437 seconds. MO:   19308657 ( )
            qsort: sorted 1000000 i32s in 0.133077 seconds. MO:   21083741 ( )

         quadsort: sorted 1000000 i32s in 0.002071 seconds. MO:     999999 ()
            qsort: sorted 1000000 i32s in 0.007265 seconds. MO:    3000004 ()

         quadsort: sorted 1000000 i32s in 0.019239 seconds. MO:    4007580 ( )
            qsort: sorted 1000000 i32s in 0.071322 seconds. MO:   20665677 ( )

         quadsort: sorted 1000000 i32s in 0.076605 seconds. MO:   19242642 ( )
            qsort: sorted 1000000 i32s in 0.038389 seconds. MO:    6221917 ( )

         quadsort: sorted 1000000 i32s in 0.002305 seconds. MO:     999999 ()
            qsort: sorted 1000000 i32s in 0.009659 seconds. MO:    4000015 ()

         quadsort: sorted 1000000 i32s in 0.022940 seconds. MO:    9519209 ( )
            qsort: sorted 1000000 i32s in 0.042135 seconds. MO:   13152042 ( )

         quadsort: sorted 1000000 i32s in 0.034456 seconds. MO:    6787656 ( )
            qsort: sorted 1000000 i32s in 0.098691 seconds. MO:   20584424 ( )

         quadsort: sorted 1000000 i32s in 0.066240 seconds. MO:   11383441 ( )
            qsort: sorted 1000000 i32s in 0.118086 seconds. MO:   20572142 ( )

         quadsort: sorted 1000000 i32s in 0.038116 seconds. MO:   15328606 ( )
            qsort: sorted 1000000 i32s in 4.471858 seconds. MO: 1974047339 ( )

         quadsort: sorted 1000000 i32s in 0.060989 seconds. MO:   15328606 ()
            qsort: sorted 1000000 i32s in 0.043175 seconds. MO:   10333679 ()

         quadsort: sorted    1023 i32s in 0.016126 seconds.       (random 1-1023)
            qsort: sorted    1023 i32s in 0.033132 seconds.       (random 1-1023)


In diesem Benchmark wird deutlich, warum Quicksort häufig der herkömmlichen Zusammenführung vorzuziehen ist. Es werden weniger Vergleiche für Daten in aufsteigender Reihenfolge, gleichmäßig verteilt und für Daten in absteigender Reihenfolge durchgeführt. In den meisten Tests schnitt es jedoch schlechter ab als Quadsort. Quicksort weist auch eine extrem langsame Sortiergeschwindigkeit für wellengeordnete Daten auf. Der Zufallsdatentest zeigt, dass Quadsort beim Sortieren kleiner Arrays mehr als doppelt so schnell ist.



Quicksort ist in allgemeinen und stabilen Tests schneller, aber nur, weil es sich um einen instabilen Algorithmus handelt.



Siehe auch:






All Articles