← Startseite
🃏

Vorlesung 6

Sortieren II — Teile und herrsche
Rekursion, Merge Sort und warum O(n log n) ein Quantensprung ist

Fortgeschrittene Algorithmen und Programmierung
Prof. Dr. Alexandra Mikityuk
HTW Berlin

Rekursion Merge Sort Teile & herrsche

Was wir schon wissen

VL 4 — Sortieren I

Drei einfache Sortier-Algorithmen:

  • Bubble Sort — Nachbarn vergleichen
  • Selection Sort — Minimum suchen
  • Insertion Sort — Karten einsortieren

VL 5 — Big-O

Alle drei sind O(n²) im schlechtesten Fall.

Bei 1 Mio. Elementen: ca. 10¹² Operationen → Stunden Laufzeit.

Frage heute: geht das schneller? Können wir Sortier-Algorithmen bauen, die deutlich besser als O(n²) sind?

Lernziele

  • Das Prinzip Teile und herrsche (Divide & Conquer) verstehen
  • Rekursion als Werkzeug kennen — Funktion ruft sich selbst auf, Basis-Fall, Call-Stack
  • Merge Sort Schritt für Schritt nachvollziehen — auf Papier und im rekursiven C-Code
  • Begründen können, warum Merge Sort nur O(n log n) braucht — ein gewaltiger Sprung gegenüber den O(n²)-Sortierern aus VL 4
  • Konkret abschätzen: wie viel schneller ist O(n log n) als O(n²) bei n = 1 000 000?

Quicksort und der Vergleich aller Sortier-Verfahren folgen im nächsten Lab — heute geht es um Rekursion + Merge Sort gründlich.

Warum reicht O(n²) nicht?

Konkrete Laufzeit bei 1 Milliarde Operationen pro Sekunde (eine moderne CPU):

Eingabegröße nO(n²) — Bubble/SelectionO(n log n) — Merge/QuickFaktor
1000,00001 s0,0000007 s15×
10 0000,1 s0,0001 s1 000×
1 000 000~ 17 Minuten0,02 s50 000×
100 000 000~ 115 Tage2,7 s~ 3 Mio.×
Punkt: bei großen Daten ist O(n log n) der Unterschied zwischen „läuft instantan" und „läuft Wochen". Spotify, Google, jede Datenbank — alle nutzen O(n log n)-Sortierer.

Die Idee: Teile und herrsche

Ein uralter Trick — schon die Römer haben so regiert: divide et impera. In der Informatik heißt das:

Teile

Zerschneide das große Problem in zwei (oder mehr) kleinere Probleme — am besten gleich groß.

Herrsche

Löse jedes kleine Problem (oft rekursiv — also wieder mit derselben Methode).

Vereine

Setze die Teil-Lösungen zur Gesamt-Lösung zusammen.

Schlüssel-Trick: wenn man n Elemente halbiert, braucht man nur log₂(n) Halbierungen, bis nur noch einzelne Elemente übrig sind. Bei n = 1 Mio. sind das nur ca. 20 Halbierungen. Daher das log n.

Warum log n? — der Halbierungs-Baum

Wenn wir n=16 immer wieder halbieren, sind wir in 4 Schritten bei 1. Das ist log₂(16) = 4.

Start (n=16):
16
↓ halbieren ↓
1. Halbierung:
8
·
8
↓ halbieren ↓
2. Halbierung:
4
·
4
·
4
·
4
↓ halbieren ↓
3. Halbierung:
2
·
2
·
2
·
2
·
2
·
2
·
2
·
2
↓ halbieren ↓
4. Halbierung:16 × [1] ← Einzel-Elemente
Skalierung: n = 1.024 → 10 Halbierungen. n = 1 Mio. → 20 Halbierungen. n = 1 Mrd. → 30 Halbierungen. Logarithmus wächst extrem langsam — daher ist O(n log n) fast wie O(n).

Teile & herrsche kennt ihr schon

📚 Buch im Wörterbuch suchen

Du schlägst nicht Seite 1, 2, 3… auf. Du öffnest in der Mitte und schaust: zu früh oder zu spät? Dann halbierst du wieder.

Das ist Divide & Conquer. Aus Tausenden Seiten → nur ca. 10 Schritte.

🏆 K.O.-Turnier

WM mit 32 Mannschaften → erst 16 Spiele, dann 8, dann 4, dann 2, dann 1.

Nicht 32×31/2 = 496 Spiele (jeder gegen jeden), sondern nur 31. log-Verhalten in der Praxis.

Merke: wann immer ein Problem sich halbieren lässt, riecht es nach O(log n) oder O(n log n). Genau das nutzen wir jetzt zum Sortieren.

Kurzer Einschub: Rekursion

Bevor wir Merge Sort und Quicksort schreiben — ein Konzept, das wir brauchen.

🪆 Was ist Rekursion?

Eine Funktion ruft sich selbst auf — mit einem etwas kleineren Problem.

Das Bild dazu: russische Matrjoschka-Puppen. Jede enthält eine kleinere Version von sich selbst. Irgendwann ist die kleinste erreicht — und es geht zurück nach außen.

⚠️ Zwei Pflicht-Teile

  • Basis-Fall: wann hört die Funktion auf, sich selbst zu rufen? (Sonst Endlos-Schleife → Programm-Absturz.)
  • Rekursiver Schritt: die Funktion muss sich mit einem echt kleineren Problem aufrufen — Richtung Basis-Fall.
Warum jetzt? „Teile und herrsche" heißt im Kern: löse das gleiche Problem für kleinere Hälften. Das ist Rekursion in einem Satz. Wir nutzen sie heute zweimal — für Merge Sort und Quicksort. Eine richtige Vertiefung kommt in einer späteren Vorlesung.

Rekursion-Beispiel: Fakultät

Mathematisch: 5! = 5 · 4 · 3 · 2 · 1 = 120. Aber auch: 5! = 5 · 4! — und dasselbe Muster wiederholt sich nach innen.

🧮 Die Idee

fakt(5) = 5 · fakt(4)
fakt(4) = 4 · fakt(3)
fakt(3) = 3 · fakt(2)
fakt(2) = 2 · fakt(1)
fakt(1) = 1 ← Basis-Fall!

Jeder Aufruf ist einfacher als der vorherige (n wird um 1 kleiner). Bei n=1 hören wir auf.

💻 In C

int fakt(int n) {
    if (n <= 1) {
        return 1;         // Basis
    }
    return n * fakt(n - 1);  // ruft sich selbst
}
Was passiert beim Aufruf fakt(5)? Die Funktion ruft sich selbst mit 4 auf → die ruft sich mit 3 auf → mit 2 → mit 1. Bei 1 ist Basis: gib 1 zurück. Dann „klappen" die Aufrufe von innen nach außen wieder zu: 2·1=2, 3·2=6, 4·6=24, 5·24=120.

Call-Stack — Phase 1: Stack wächst (Push)

Jeder Aufruf von fakt bekommt ein eigenes Stockwerk (Stack-Frame). Solange ein Aufruf noch wartet, bleibt sein Frame liegen — gestapelt wie Teller, der neueste oben drauf (push).

push fakt(5):
n=5 · wartet auf fakt(4)
↓ ruft fakt(4) auf → push ↓
push fakt(4):
n=4 · wartet auf fakt(3)
↓ ruft fakt(3) auf → push ↓
push fakt(3):
n=3 · wartet auf fakt(2)
↓ ruft fakt(2) auf → push ↓
push fakt(2):
n=2 · wartet auf fakt(1)
↓ ruft fakt(1) auf → push ↓
push fakt(1):
Basis-Fall erreicht · liefert 1
Warum stapelt der Computer das? Wenn fakt(5) den Aufruf fakt(4) startet, kennt es das Ergebnis noch nicht. Es muss aber zwei Dinge merken: „mein n war 5" und „ich warte auf einen Wert zum Multiplizieren". Beides bleibt im Frame eingefroren liegen, bis der Aufruf darüber fertig ist.

Call-Stack — Phase 2: Stack schrumpft (Pop)

Sobald die Basis erreicht ist, „klappen" die Frames von oben nach unten wieder zu. Jeder Frame nutzt seine gepufferten Werte und das Ergebnis von oben, um seine eigene Rechnung fertigzustellen.

fakt(1) gibt zurück:
liefert 1 → pop
↓ Ergebnis 1 geht an fakt(2) ↓
fakt(2) rechnet:
2 · 1 = 2 → pop
↓ Ergebnis 2 geht an fakt(3) ↓
fakt(3) rechnet:
3 · 2 = 6 → pop
↓ Ergebnis 6 geht an fakt(4) ↓
fakt(4) rechnet:
4 · 6 = 24 → pop
↓ Ergebnis 24 geht an fakt(5) ↓
fakt(5) rechnet:
5 · 24 = 120 → fertig!
Stack-Limit: jeder Frame braucht etwas Speicher. Bei Merge Sort sind es nur log₂(n) Frames tief — bei n = 1 Mio. also ~ 20 Frames, völlig unkritisch. Erst bei fehlendem Basis-Fall (Endlos-Rekursion) läuft der Stack über → Stack Overflow.

Merge Sort — die Grundidee

Drei Schritte — Schritt 2 ist rekursiv (genau das Konzept von eben):

  1. Teile: teile die Liste in zwei gleich große Hälften.
  2. Sortiere jede Hälfte — mit Merge Sort, also rufe Merge Sort rekursiv auf jede Hälfte auf.
  3. Verschmelze: füge die zwei nun sortierten Hälften zu einer sortierten Gesamtliste zusammen.
Basis-Fall: wenn eine Liste nur noch 1 Element hat, ist sie automatisch sortiert — wir hören mit der Rekursion auf. Das ist genau wie der n <= 1 → return 1-Fall bei der Fakultät.
Der trickreiche Teil ist das Verschmelzen. Wenn zwei kleine Listen schon sortiert sind, kann man sie sehr schnell zu einer großen sortierten Liste zusammenfügen — wir schauen nur jeweils die vordersten Karten an. Dazu gleich der Trick.
Drei Slides ergeben den ganzen Algorithmus:
  • Phase 1 (nächste Slide) — Aufteilen bis zu Einzel-Karten
  • Werkzeug — der Verschmelz-Trick (zwei sortierte Listen → eine)
  • Phase 2 — den Trick auf jeder Ebene anwenden, bis alles eine sortierte Liste ist

Merge Sort — Phase 1: Aufteilen

Beispiel mit 8 Karten: [ 5, 2, 8, 1, 9, 3, 7, 4 ]

Start:
5
2
8
1
9
3
7
4
↓ teilen ↓
5
2
8
1
9
3
7
4
↓ wieder teilen ↓
5
2
8
1
9
3
7
4
↓ bis nur noch Einzel-Karten ↓
5
2
8
1
9
3
7
4

Bei 8 Karten: 3 Halbierungen (log₂ 8 = 3).

Das Werkzeug: der Verschmelz-Trick

„Warum erst alles bis auf 1 zerlegen und dann wieder zusammenbauen?" Weil unser Verschmelz-Trick nur funktioniert, wenn beide Eingabe-Listen sortiert sind. Und eine Liste mit nur 1 Element ist garantiert sortiert — das ist unser Startpunkt. Von dort bauen wir hoch: aus zwei sortierten 1-er-Listen wird eine sortierte 2-er-Liste. Aus zwei sortierten 2-er-Listen eine 4-er. Usw.
Der Trick (universell — gilt auf jeder Ebene): schau immer nur das vorderste Element der beiden Listen an. Nimm das kleinere, leg es ins Ergebnis. Wiederhole, bis eine Liste leer ist — den Rest der anderen übernimm ohne Vergleichen.

Allererster Merge — zwei Einzel-Karten

A = [5] · B = [2]

Vergleich 5 vs 2 → 2 ist kleiner → nimm 2 (aus B). B ist jetzt leer → Rest aus A übernehmen (die 5).

Ergebnis: [2, 5]

Genauso für die anderen Paare: [8]+[1] → [1,8], [9]+[3] → [3,9], [7]+[4] → [4,7]. Wir haben jetzt 4 sortierte 2-er-Listen.

Nächste Ebene — zwei 2-er-Listen

A und B sind die Ergebnisse aus ① — sie wurden gerade selbst durch Merge erzeugt:

5
2
↓ merge ①
A = [2, 5]
8
1
↓ merge ①
B = [1, 8]

Jetzt verschmelzen wir A und B mit demselben Trick:

A vorneB vornenimmErgebnis
211 (aus B)[1]
282 (aus A)[1, 2]
585 (aus A)[1, 2, 5]
leer8Rest aus B[1, 2, 5, 8]
Punkt: derselbe Algorithmus läuft auf jeder Ebene — nur die Listen werden länger. Bei zwei Listen mit zusammen 2n Elementen brauchen wir maximal 2n Vergleichelinear in n.

Merge Sort — Phase 2: den Trick auf allen Ebenen anwenden

Jetzt wenden wir das Werkzeug von Slide 9 nicht nur einmal an, sondern immer wieder — Ebene für Ebene, bis das ganze Array eine sortierte Liste ist. Aufteilen + Trick allein reichen nicht; erst die Wiederholung des Tricks macht das Sortieren komplett.

Start: 8 Einzel-Karten (jede für sich „sortiert")
5
2
8
1
9
3
7
4
↓ Stufe 1: je 2 Einzel-Karten zu einem Paar verschmelzen ↓
4 sortierte Paare
2
5
1
8
3
9
4
7
↓ Stufe 2: je 2 Paare zu einer 4-er-Gruppe verschmelzen ↓
2 sortierte 4-er-Gruppen
1
2
5
8
3
4
7
9
↓ Stufe 3: die zwei 4-er-Gruppen zur 8-er-Gruppe verschmelzen ↓
Fertig — komplett sortiert
1
2
3
4
5
7
8
9

✓ 3 Stufen · jede Stufe verschmilzt alle 8 Karten · zusammen ca. n × log n Vergleiche

Merge Sort — Stufen-Übersicht

Schauen wir das Aufteilen einmal von oben: aus einer großen Liste werden in jeder Stufe doppelt so viele, halb so große Häufchen.

Stufe 0 (Start):
[5, 2, 8, 1, 9, 3, 7, 4]
1 Liste · 8 Karten
↓ halbieren ↓
Stufe 1:
[5,2,8,1]
·
[9,3,7,4]
2 Listen · je 4 Karten
↓ halbieren ↓
Stufe 2:
[5,2]
·
[8,1]
·
[9,3]
·
[7,4]
4 Listen · je 2 Karten
↓ halbieren ↓
Stufe 3 (Basis):
5
2
8
1
9
3
7
4
8 Listen · je 1 Karte

Anzahl der Stufen

3 Stufen bei n=8 — weil 8 = 2³.

Allgemein: log₂(n) Stufen. Bei n = 1 Mio. nur 20 Stufen.

Arbeit pro Stufe

Beim Verschmelzen werden auf jeder Stufe insgesamt alle n Karten angefasst — egal in wie viele Häufchen sie aufgeteilt sind.

Daraus folgt: Stufen × Arbeit pro Stufe = log₂(n) × n = n · log n.

Merge Sort — Pseudocode (rekursiv)

Drei Zeilen sind das Herz: halbieren · sich selbst auf jede Hälfte aufrufen · verschmelzen.

// Sortiert den Abschnitt a[links..rechts]
function mergeSort(a, links, rechts):
    if links >= rechts:
        return                            // Basis-Fall: 0 oder 1 Element ist sortiert

    mitte = (links + rechts) / 2
    mergeSort(a, links, mitte)             // linke Hälfte weiter teilen (sich selbst rufen)
    mergeSort(a, mitte + 1, rechts)        // rechte Hälfte weiter teilen
    merge(a, links, mitte, rechts)         // HIER wird sortiert: vergleichen + verschmelzen

// Hilfs-Funktion: verschmilzt a[links..mitte] und a[mitte+1..rechts]
// (genau der Trick aus Slide 9 — vorderste Karten vergleichen)
function merge(a, links, mitte, rechts):
    // → siehe nächste Slide (C-Code)
Wie liest man das? Die Funktion mergeSort ruft sich zweimal selbst auf — mit der linken und der rechten Hälfte. Jede dieser Hälften wird wieder halbiert, und so weiter. Beim Basis-Fall (1 Element) macht die Funktion nichts — sie kehrt einfach zurück. Erst der Eltern-Aufruf verschmilzt: sobald seine zwei Kind-Aufrufe zurück sind, ruft er merge auf. So wird auf jeder Ebene nach den Rückkehrern verschmolzen.

Merge Sort in C — die merge-Funktion

Erst die Hilfs-Funktion: verschmilzt a[l..m] und a[m+1..r] — beide sind bereits sortiert.

void merge(int a[], int l, int m, int r) {
    int hilfs[1024];          // Hilfs-Array für das Ergebnis
    int i = l;                  // Zeiger auf vorderstes Element links
    int j = m + 1;              // Zeiger auf vorderstes Element rechts
    int k = 0;                  // Schreib-Position in hilfs[]

    // Solange beide Hälften noch Elemente haben: nimm das kleinere
    while (i <= m && j <= r) {
        if (a[i] <= a[j]) {
            hilfs[k] = a[i];
            i = i + 1;
        } else {
            hilfs[k] = a[j];
            j = j + 1;
        }
        k = k + 1;
    }
    // Rest übernehmen (immer nur eine der beiden Schleifen läuft)
    while (i <= m) { hilfs[k] = a[i]; i = i + 1; k = k + 1; }
    while (j <= r) { hilfs[k] = a[j]; j = j + 1; k = k + 1; }

    // Hilfs-Array zurück in a[l..r] kopieren
    for (int x = 0; x < k; x = x + 1) {
        a[l + x] = hilfs[x];
    }
}
Zentrale Stelle: die zwei Variablen i und j sind die „Zeiger" auf die vordersten Karten der zwei Hälften — genau wie in der Tabelle auf Slide 9. Bei jedem Schritt rückt einer um 1 weiter.

Merge Sort in C — die rekursive Hauptfunktion

Drei Zeilen Logik — exakt das Pseudocode-Muster von Slide 11. Die Funktion ruft sich für jede Hälfte selbst auf.

// Sortiert a[l..r] rekursiv
void merge_sort(int a[], int l, int r) {
    if (l >= r) {
        return;                       // Basis-Fall
    }
    int m = (l + r) / 2;            // Mitte

    merge_sort(a, l, m);              // links weiter teilen
    merge_sort(a, m + 1, r);          // rechts weiter teilen
    merge(a, l, m, r);                // HIER wird sortiert!
}

// Aufruf für ein Array a der Länge n:
//   merge_sort(a, 0, n - 1);

📋 Die drei Parameter

  • a — das Array (z.B. {5,2,8,1,9,3,7,4})
  • llinker Index (inklusiv)
  • rrechter Index (inklusiv)

Bei den rekursiven Aufrufen bleibt a immer dasselbe Array — nur die Indizes verschieben sich.

⚠️ eckige Klammern nur in Deklaration

merge_sort(a, 0, 7);    // ✓ richtig
merge_sort(a[], 0, 7);  // ✗ Fehler
merge_sort(a[0], 0, 7); // ✗ a[0] ist 5!

a alleine = ganzes Array · a[i] = ein Element.

Konkret bei merge_sort(a, 0, 7) (n=8): wird zu merge_sort(a, 0, 3) + merge_sort(a, 4, 7) + merge(a, 0, 3, 7). Wir schneiden das Array nicht zerteilt, sondern verschieben nur die Indizes. → wo das eigentliche Sortieren passiert, klären wir auf der nächsten Slide.

„Aber wo wird denn jetzt sortiert?"

Beim Lesen der drei Zeilen merge_sort(...) · merge_sort(...) · merge(...) denkt jeder: „die ersten zwei machen doch nichts — die rufen sich nur selbst auf!" Genau richtig:

1️⃣ merge_sort

Vergleicht keine einzige Karte. Es zerlegt nur das Array immer weiter:

Hälfte → Hälfte → … → Einzel-Karte

2️⃣ Basis-Fall (l ≥ r)

Auch hier passiert nichts. Die Funktion sieht ein einzelnes Element, sagt „ist schon sortiert" und kehrt sofort per return zurück.

3️⃣ merge — HIER!

Das ist der einzige Ort, wo Elemente verglichen und in die richtige Reihenfolge gebracht werden — vergleichend ins Hilfs-Array geschrieben.

Auf jeder Ebene des Aufruf-Baums wird einmal merge aufgerufen — von ganz unten (1+1 Karten) bis ganz oben (n/2 + n/2 Karten). Da entsteht die Ordnung. merge_sort liefert nur die Struktur (Aufteilung + Reihenfolge der merge-Aufrufe), merge erledigt die echte Arbeit.

Merge Sort in C — das komplette Programm

Alles zusammen in einer Datei merge_sort.c — kopier-und-kompilier-fertig. Erinnerung: merge_sort teilt auf · merge sortiert beim Zusammenfügen.

#include <stdio.h>

// WERKZEUG: nimmt 2 sortierte Teile, macht daraus 1 sortierten Teil
void merge(int a[], int l, int m, int r) {
    int hilfs[1024];
    int i = l, j = m + 1, k = 0;       // Zeiger links/rechts/hilfs
    while (i <= m && j <= r) {           // kleinere von beiden nehmen
        if (a[i] <= a[j]) { hilfs[k] = a[i]; i++; }
        else              { hilfs[k] = a[j]; j++; }
        k++;
    }
    while (i <= m) { hilfs[k] = a[i]; i++; k++; }   // Rest links
    while (j <= r) { hilfs[k] = a[j]; j++; k++; }   // Rest rechts
    for (int x = 0; x < k; x++) a[l + x] = hilfs[x];
}

// SORTIERER: ruft sich rekursiv für jede Hälfte auf, dann merge
void merge_sort(int a[], int l, int r) {
    if (l >= r) return;                // Basis: 0/1 Element ist sortiert
    int m = (l + r) / 2;
    merge_sort(a, l, m);                // linke Hälfte weiter teilen
    merge_sort(a, m + 1, r);            // rechte Hälfte weiter teilen
    merge(a, l, m, r);                  // HIER wird sortiert!
}

int main(void) {
    int a[] = {5, 2, 8, 1, 9, 3, 7, 4};
    int n = 8;
    merge_sort(a, 0, n - 1);            // ganzes Array sortieren
    for (int i = 0; i < n; i++) printf("%d ", a[i]);
    printf("\n");                          // → 1 2 3 4 5 7 8 9
    return 0;
}

🔨 Kompilieren & ausführen

$ gcc merge_sort.c -o ms
$ ./ms
1 2 3 4 5 7 8 9

⚠️ Zwei Stolperfallen

  • merge muss vor merge_sort im Code stehen — sonst kennt der Compiler die Funktion noch nicht.
  • hilfs[1024] ist hart auf 1024 Elemente begrenzt — größere Arrays brauchen malloc (kommt später).

📂 Aufbau der Datei

  1. #include
  2. merge (Werkzeug)
  3. merge_sort (Sortierer)
  4. main (Eingabe + Aufruf + Ausgabe)

Warum braucht Merge Sort O(n log n)?

Tiefe des Rekursionsbaums

Jeder Aufruf halbiert die Liste.

Aus n → n/2 → n/4 → … → 1

Das geht genau log₂(n)-mal.

n=8 → 3 Ebenen
n=1024 → 10 Ebenen
n=1 000 000 → 20 Ebenen

Arbeit pro Ebene

Auf jeder Ebene werden insgesamt n Elemente verschmolzen — egal wie viele Teilstücke es sind.

Beispiel n=8:

  • Ebene 1: 4 Merges à 2 = 8
  • Ebene 2: 2 Merges à 4 = 8
  • Ebene 3: 1 Merge à 8 = 8
Gesamt: n · log₂(n) Operationen. Für n = 1 000 000 sind das ~ 20 Mio. Operationen — eine moderne CPU schafft das in 0,02 Sekunden.

O(n²) vs. O(n log n) — visuell

Operationen für verschiedene Eingabegrößen (Balkenlänge proportional zur Operations-Anzahl, log-skaliert):

n = 100
O(n²)
10 000 Ops
O(n log n)
~ 700 Ops · 15× besser
n = 10 000
O(n²)
100 Mio. Ops
O(n log n)
~ 130 000 Ops · 770× besser
n = 1 000 000
O(n²)
… × 7 000
O(n log n)
~ 20 Mio. Ops · 50 000× besser
Punkt: bei kleinen n ist der Unterschied klein. Bei großen n wird er existenziell. Genau deshalb existiert die Big-O-Welt: damit man frühzeitig die richtige Klasse wählt.

Die vier Sortier-Algorithmen im Vergleich

AlgorithmusBestAverageWorstSpeicherStabil?Wann?
Bubble SortO(n)O(n²)O(n²)O(1)janur Lehre
Selection SortO(n²)O(n²)O(n²)O(1)neinkleine n, wenig Swaps wichtig
Insertion SortO(n)O(n²)O(n²)O(1)jafast-sortierte Daten
Merge SortO(n log n)O(n log n)O(n log n)O(n)jawenn O(n log n) garantiert
„Stabil" heißt: gleiche Elemente behalten ihre ursprüngliche Reihenfolge. Wichtig, wenn man nach mehreren Kriterien sortiert (z.B. erst Name, dann Alter).
Im Lab kommt noch Quicksort dazu — ein weiterer O(n log n)-Sortierer, der ohne Extra-Speicher auskommt, im Worst Case aber auf O(n²) fallen kann.

Was nutzen echte Sprachen intern?

C — qsort()

Standard-Library hat qsort — eine optimierte O(n log n)-Sortierung. Sehr schnell, aber instabil.

Python & Java — TimSort

Eine Mischung aus Merge Sort + Insertion Sort, optimiert für realistische Daten. Stabil, O(n log n).

C++ — Introsort

std::sort — eine hybride O(n log n)-Sortierung mit Garantie auch im Worst Case.

Lektion: in der Praxis baut niemand mehr Merge Sort „nackt" — es gibt fertige hybride Sortierer. Aber das Verständnis ist Pflicht: ohne die Grundlagen versteht man weder die Doku noch die Performance.

🤔 Quiz

Eine Liste mit 1 000 000 Elementen soll sortiert werden. Welcher Algorithmus garantiert unter 1 Sekunde Laufzeit?

A) Bubble Sort
B) Selection Sort
C) Merge Sort
D) Insertion Sort
Warum: Bubble, Selection und Insertion sind alle O(n²) — bei n=1 Mio. dauert das viele Minuten. Nur Merge Sort garantiert O(n log n) und schafft 1 Mio. Elemente in unter einer Sekunde.

Zusammenfassung

  • Rekursion = Funktion ruft sich selbst auf, mit kleinerem Problem · Basis-Fall · Call-Stack puffert die Zwischen-Zustände.
  • Teile und herrsche ist eines der mächtigsten Algorithmus-Schemata — überall dort, wo „halbieren" hilft.
  • Merge Sort teilt rekursiv, verschmilzt sortierte Hälften — garantiert O(n log n), braucht O(n) Extra-Speicher, ist stabil.
  • Beim Sortieren passiert die echte Arbeit in mergemerge_sort teilt nur die Arbeit auf.
  • Bei großen Daten ist O(n log n) der Unterschied zwischen praktikabel und unbenutzbar.

📚 Vertiefung bis nächste Woche

Hausaufgabe

  • Zeichnet auf Papier die Stufen-Übersicht von Merge Sort für [6, 3, 8, 1, 5, 2, 7, 4] — Aufteilen und Verschmelzen, Stufe für Stufe.
  • Schätzt ab: wie viele Vergleiche braucht Insertion Sort vs. Merge Sort bei n = 100? bei n = 10 000?
  • Spielt mit der Animation auf toptal.com/developers/sorting-algorithms — dort sieht man alle Sortierer parallel laufen.
Im nächsten Lab: wir bauen Quicksort — den zweiten klassischen O(n log n)-Sortierer (Pivot + Partitionieren statt verschmelzen) — und schauen uns die qsort-Standardfunktion in C an.

Vielen Dank!

Fragen?

Prof. Dr. Alexandra Mikityuk

HTW Berlin · Büro Raum 308

Tel +49 30 5019-2664

Nächste Woche: Suchen & Hashing

1 / …