Countingsort und Radixsort
Kurz gesagt: Countingsort ordnet ganze Zahlen aus einem kleinen Bereich ohne einen einzigen Vergleich, indem er zählt, wie oft jeder Wert vorkommt: O(n + k); Radixsort wendet ihn Ziffer für Ziffer an.
Problem
Eine Million Bestellungen, hundert Zonen
Der Kurierdienst bekommt für den Tag eine Million Bestellungen. Jede hat eine Lieferzone: eine Zahl von 0 bis 99. Bevor die Transporter losfahren, werden die Bestellungen nach Zone sortiert, damit jeder Transporter seinen eigenen Stapel nehmen kann.
Mergesort macht bei einer Million Bestellungen etwa 18,7 Millionen Vergleiche, und Heapsort ist nicht besser (siehe Heapsort). Sie vergleichen Bestellung mit Bestellung, weil sie nichts über den Schlüssel wissen.
Aber hier weißt du viel: Es gibt nur hundert verschiedene Zonen. Genauso ist es bei den Bewertungen eines Produkts: von 1 bis 5. Eine Million Bestellungen zu vergleichen, um herauszufinden, dass 37 vor 38 kommt, ist Arbeit, die man gar nicht machen muss.
Die Idee
Zähle statt zu vergleichen
Der Schlüssel ist eine kleine ganze Zahl. Also kann er ein Index in ein Array sein. Das Sortieren wird zu drei Durchläufen:
- Zählen. Ein Zähler für jede Zone. Jede Bestellung erhöht den Zähler ihrer Zone um eins.
- Wo jede Zone beginnt. Zone 0 beginnt bei Platz 0. Zone 1 beginnt nach allen Bestellungen der Zone 0. Jeder Anfang ist der vorige plus die Anzahl der vorigen Zone. Das nennt man Präfixsumme.
- Einsortieren. Du gehst die Bestellungen von links nach rechts durch. Jede kommt an den Anfang ihrer Zone, und der Anfang rückt um eins weiter.
Das ist Countingsort. Keine einzige Bestellung wird mit einer anderen verglichen.
Das Durchgehen von links nach rechts hat eine wichtige Folge: Die Bestellungen einer Zone bleiben in der Reihenfolge, in der sie kamen. Waren sie nach Uhrzeit sortiert, sind sie es in ihrer Zone weiterhin. Eine solche Sortierung heißt stabil. In der Lektion zur Stabilität siehst du, warum das so wichtig ist.
Sieh es dir an
Acht Bestellungen, vier Zonen
Die Zahl der Zonen ist ein Parameter. Bei der Million Bestellungen ist sie 100, hier ist sie 4, damit alles zu sehen ist:
function byZone(orders, zones) { const count = new Array(zones).fill(0); for (const o of orders) { count[o.zone]++; } const start = new Array(zones).fill(0); for (let z = 1; z < zones; z++) { start[z] = start[z - 1] + count[z - 1]; } const out = new Array(orders.length); for (const o of orders) { out[start[o.zone]++] = o; } return out;}const orders = [ { id: 101, zone: 2 }, { id: 102, zone: 0 }, { id: 103, zone: 3 }, { id: 104, zone: 2 }, { id: 105, zone: 1 }, { id: 106, zone: 0 }, { id: 107, zone: 2 }, { id: 108, zone: 3 },];for (const o of byZone(orders, 4)) { console.log(`Zone ${o.zone}, Bestellung ${o.id}`);}Zone 0, Bestellung 102Zone 0, Bestellung 106Zone 1, Bestellung 105Zone 2, Bestellung 101Zone 2, Bestellung 104Zone 2, Bestellung 107Zone 3, Bestellung 103Zone 3, Bestellung 108function byZone(orders, zones) { const count = new Array(zones).fill(0); for (const o of orders) { count[o.zone]++; } const start = new Array(zones).fill(0); for (let z = 1; z < zones; z++) { start[z] = start[z - 1] + count[z - 1]; } const out = new Array(orders.length); for (const o of orders) { out[start[o.zone]++] = o; } return out;}Schritt für Schritt, in Worten
- Acht Bestellungen, jede mit einer Lieferzone von 0 bis 3. Ein Zähler pro Zone, alle auf 0.
- Zählen: Jede Bestellung erhöht den Zähler ihrer Zone um eins. Keine Bestellung wird mit einer anderen verglichen.
- Wo jede Zone beginnt: Zone 0 bei Platz 0, Zone 1 nach den zwei Bestellungen der Zone 0, bei Platz 2; Zone 2 bei 3, Zone 3 bei 6. Jeder Anfang ist der vorige Anfang plus die Anzahl der vorigen Zone.
- Einsortieren von links nach rechts: 101 kommt an den Anfang der Zone 2, Platz 3, und der Anfang der Zone 2 rückt um eins weiter.
- Nach Zone sortiert in 2n + k Schritten. In Zone 2 stehen die Bestellungen 101, 104, 107: in der Reihenfolge, in der sie kamen. Das ist eine stabile Sortierung.
Die Bestellungen 101, 104 und 107 liegen in Zone 2. Sie kamen in dieser Reihenfolge und gingen in dieser Reihenfolge hinaus.
start[...]++ sortiert am aktuellen Anfang ein und rückt ihn dann um eins weiter: ++ nach dem Ausdruck liefert den alten Wert.Probier es aus
Ergänze
Der Anfang der Zone z ist der Anfang der vorigen Zone plus:
start[z] = start[z - 1] + ;Zone z beginnt dort, wo Zone z - 1 endet: ihr Anfang plus die Zahl ihrer Bestellungen. Deshalb ist es count[z - 1], nicht count[z].
Die Idee
Was es kostet: O(n + k)
Jede Bestellung wird einmal gezählt und einmal einsortiert: 2n Schritte. Die Anfänge sind einer pro Zone: k Schritte. Insgesamt O(n + k), wobei k die Zahl der möglichen Schlüssel ist. Speicher: das Array der Zähler (k) und das Array mit dem Ergebnis (n).
Hier gibt es keine Vergleiche, also zählt Bench jeden Schritt des Countingsort: jedes Zählen, jeden Zähler und jedes Einsortieren. Mergesort zählt seine Vergleiche:
- Counting Sort (Zonen 0–99)
O(n) - Sortieren (Merge Sort)
O(n log n)
| Algorithmus | n = 10 | n = 1.000 | n = 100.000 | n = 1.000.000 |
|---|---|---|---|---|
| Counting Sort (Zonen 0–99) | 120 | 2.100 | 200.100 | 2.000.100 |
| Sortieren (Merge Sort) | 21 | 8.709 | 1.536.371 | 18.674.993 |
Bei einer Million Bestellungen macht Countingsort 2.000.100 Schritte und Mergesort 18.674.993 Vergleiche. Die gerade Linie des Countingsort ist O(n): doppelt so viele Bestellungen, doppelt so viel Arbeit.
Genauer, für Neugierige
Kann eine Sortierung durch Vergleichen schneller sein als n log n? Nein. Bei n Elementen gibt es n! mögliche Reihenfolgen, und jeder Vergleich hat zwei Antworten. Um n! Fälle zu unterscheiden, braucht man mindestens log₂(n!) Vergleiche, und das sind etwa n log₂ n. Countingsort umgeht diese Grenze, weil er nicht vergleicht: Er benutzt den Schlüssel als Adresse.
Sieh es dir an
Radixsort: Ziffer für Ziffer
Und wenn der Schlüssel größer ist? Bulgarische Postleitzahlen haben vier Ziffern, von 1000 bis 9999: 9000 Zähler jeden Tag. Bestellnummern haben sechs Ziffern: 900.000 Zähler.
Radixsort sortiert Ziffer für Ziffer mit demselben Zählen, aber mit nur 10 Zählern. Zuerst nach der letzten Ziffer, dann nach der davor und so weiter bis zur ersten (LSD, von der niedrigstwertigen Ziffer). Jeder Durchlauf ist ein Countingsort nach einer Ziffer:
function byDigit(codes, exp) { const count = new Array(10).fill(0); for (const c of codes) { count[Math.floor(c / exp) % 10]++; } const start = new Array(10).fill(0); for (let d = 1; d < 10; d++) { start[d] = start[d - 1] + count[d - 1]; } const out = new Array(codes.length); for (const c of codes) { out[start[Math.floor(c / exp) % 10]++] = c; } return out;}let codes = [4003, 1504, 9000, 1000, 8001, 4000, 1404];for (let exp = 1; exp <= 1000; exp *= 10) { codes = byDigit(codes, exp); console.log(`[${codes.join(", ")}]`);}[9000, 1000, 4000, 8001, 4003, 1504, 1404][9000, 1000, 4000, 8001, 4003, 1504, 1404][9000, 1000, 4000, 8001, 4003, 1404, 1504][1000, 1404, 1504, 4000, 4003, 8001, 9000]Nach dem ersten Durchlauf sind die Zahlen nach der letzten Ziffer sortiert. Nach dem zweiten nach den letzten zwei. Nach dem vierten nach allen vier.
Das funktioniert nur, weil jeder Durchlauf stabil ist. Der Durchlauf nach den Hundertern bringt die Zahlen mit gleichen Hundertern nicht durcheinander, und sie sind schon nach den letzten zwei Ziffern sortiert. 1000 und 1404 und 1504 haben dieselbe erste Ziffer: Der letzte Durchlauf lässt sie in der Reihenfolge, die sie von den früheren bekommen haben.
Math.floor(c / exp) % 10 ist die Ziffer: / in JavaScript liefert einen Bruch, daher Math.floor.Kosten: d Durchläufe mit n + 10 Schritten, wobei d die Zahl der Ziffern ist. Bei sechsstelligen Zahlen sind das 6 · (2n + 10):
- Radix Sort (6 Ziffern)
O(n) - Counting Sort (Zonen 0–99)
O(n) - Sortieren (Merge Sort)
O(n log n)
| Algorithmus | n = 10 | n = 1.000 | n = 100.000 | n = 1.000.000 |
|---|---|---|---|---|
| Radix Sort (6 Ziffern) | 180 | 12.060 | 1.200.060 | 12.000.060 |
| Counting Sort (Zonen 0–99) | 120 | 2.100 | 200.100 | 2.000.100 |
| Sortieren (Merge Sort) | 21 | 8.709 | 1.536.371 | 18.674.993 |
Probier es aus
Sage die Ausgabe voraus
Ein Durchlauf nach der letzten Ziffer (exp ist 1). Was gibt das Programm aus?
const codes = [31, 12, 22, 11];console.log(`[${byDigit(codes, 1).join(", ")}]`);Probier es aus
Finde den Fehler
Jemand sortiert die Bestellungen von hinten nach vorn ein. Die Zonen kommen sortiert heraus, aber innerhalb der Zone 2 stimmt etwas nicht:
function byZone(orders, zones) { const count = new Array(zones).fill(0); for (const o of orders) { count[o.zone]++; } const start = new Array(zones).fill(0); for (let z = 1; z < zones; z++) { start[z] = start[z - 1] + count[z - 1]; } const out = new Array(orders.length); for (let i = orders.length - 1; i >= 0; i--) { out[start[orders[i].zone]++] = orders[i]; } return out;}const orders = [ { id: 101, zone: 2 }, { id: 102, zone: 0 }, { id: 103, zone: 3 }, { id: 104, zone: 2 }, { id: 105, zone: 1 }, { id: 106, zone: 0 }, { id: 107, zone: 2 }, { id: 108, zone: 3 },];for (const o of byZone(orders, 4)) { console.log(`Zone ${o.zone}, Bestellung ${o.id}`);}Zone 0, Bestellung 106Zone 0, Bestellung 102Zone 1, Bestellung 105Zone 2, Bestellung 107Zone 2, Bestellung 104Zone 2, Bestellung 101Zone 3, Bestellung 108Zone 3, Bestellung 103In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Das Einsortieren beginnt am Ende, während die Anfänge vorwärts wachsen. Die letzte Bestellung der Zone 2 nimmt den ersten Platz der Zone, und die Reihenfolge innerhalb der Zone ist umgekehrt: 107, 104, 101. Die Sortierung ist nicht mehr stabil, und Radixsort mit einem solchen Durchlauf liefert ein falsches Ergebnis. Geh von links nach rechts. (Es gibt auch eine Variante von hinten nach vorn, aber dann benutzt du die Enden der Zonen, und die sinken.)
Die Idee
Wann es nicht funktioniert
- k ist riesig. Ein Preis in Euro-Cent bis 100.000 €: 10 Millionen Zähler, um tausend Bestellungen zu sortieren. Mergesort ist nach etwa 10.000 Vergleichen fertig. Countingsort lohnt sich, wenn k etwa n oder kleiner ist.
- Der Schlüssel ist keine ganze Zahl. Namen, Adressen, Preise mit Nachkommastellen: Sie sind keine Indizes in ein Array. Radixsort kann Strings Zeichen für Zeichen sortieren, aber nur, wenn alle gleich lang oder aufgefüllt sind.
- Negative Schlüssel. Sie funktionieren, wenn du den kleinsten abziehst: Schlüssel − min beginnt bei 0.
Deshalb ist das eingebaute sort keiner der vier Sprachen ein Countingsort. Es muss alles sortieren, und das Einzige, was es über die Elemente weiß, ist, wie man sie vergleicht.
Genauer, für Neugierige
Wie es in den anderen Sprachen ist: In Java ist Arrays.sort für ein int-Array Dual-Pivot-Quicksort, in C# ist Array.Sort Introsort, in Python sort und in JavaScript sort sind Varianten des Mergesort (TimSort). Alle vergleichen. Countingsort und Radixsort schreibt man von Hand, wenn man weiß, dass der Schlüssel klein ist: Zonen, Bewertungen, Jahre, Codes. Wie die eingebauten sortieren, steht in der Lektion zur Stabilität.
Übung
Rezensionen nach Bewertung
Löse die Aufgabe in deinem Editor.
Bewertungen von 1 bis 5. Zwölf Rezensionen eines Produkts haben die Bewertungen 5, 3, 4, 5, 1, 2, 5, 4, 4, 3, 5, 2. Zähle mit einem Array von Zählern, wie viele es von jeder Bewertung gibt, gib die Zahlen aus und danach die sortierten Bewertungen, ohne einen einzigen Vergleich zweier Bewertungen.
1: 12: 23: 24: 35: 4Sortiert: [1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 5, 5]Weitere Aufgaben
- Die Rezensionen, nicht nur die Bewertungen. Jede Rezension hat einen Autor und eine Bewertung. Sortiere die Rezensionen mit
byZonenach Bewertung und prüfe, dass Rezensionen mit gleicher Bewertung in der Reihenfolge bleiben, in der sie geschrieben wurden. - Sechsstellige Zahlen. Sortiere die Bestellnummern
482113, 105990, 482100, 300001, 105911mit Radixsort. Wie viele Durchläufe brauchst du? - Negative Temperaturen. Sortiere die Temperaturen
-3, 5, 0, -8, 12, 5mit Countingsort. Was ziehst du von jedem Schlüssel ab?
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Zeitkomplexität: Schritte zählen · Big-O: wie die Arbeit wächst · Mergesort und „Teile und herrsche“ · Heap und Heapsort
- Geht weiter in: Stabiles Sortieren und die eingebaute Sortierung
- Aufgaben zum Thema: Sortieren