Der Merge-Sort-Algorithmus

Der Merge-Sort-Algorithmus

🌐 Read in English
📅 2026-09-11 ✍ Andreas Wittmann đŸ‘ïž ... algorithm sorting computer-graphics

Quicksort erreicht O(n log n), indem es um ein aus den Daten selbst gewĂ€hltes Pivot herum partitioniert — genau das lĂ€sst seinen Worst Case von der Eingabereihenfolge abhĂ€ngen. Merge Sort erreicht dieselbe KomplexitĂ€t auf anderem Weg, mit einer Garantie, die Quicksort nicht geben kann: Es ist jedes Mal O(n log n), egal wie die Eingabe angeordnet ist — auf Kosten eines zweiten Arrays, in dem gearbeitet wird.

Die Idee

Eine Liste mit einem Element ist bereits sortiert. Zwei sortierte Listen lassen sich zu einer grĂ¶ĂŸeren sortierten Liste verschmelzen, indem man wiederholt das jeweils kleinere der beiden vorderen Elemente nimmt — dieser Kombinationsschritt heißt Merge und braucht nur einen Durchlauf durch beide Listen, sodass das Verschmelzen zweier LĂ€ufe der LĂ€nge m insgesamt O(m) Arbeit kostet, nicht O(m log m).

Top-down-Merge-Sort wendet das rekursiv an: die Liste halbieren, jede HĂ€lfte (rekursiv) sortieren, dann die beiden sortierten HĂ€lften verschmelzen. Das Sample unten baut dasselbe Ergebnis stattdessen bottom-up auf, was die Rekursion komplett ĂŒberflĂŒssig macht: ZunĂ€chst wird jedes einzelne Element als bereits "sortiert" behandelt (ein Lauf der LĂ€nge 1), dann werden wiederholt benachbarte LĂ€ufe zu doppelt so großen LĂ€ufen verschmolzen — LĂ€nge 1 zu LĂ€nge 2, LĂ€nge 2 zu LĂ€nge 4 und so weiter — bis ein Lauf das gesamte Array abdeckt. Dass sich die LauflĂ€nge bei jedem Durchgang verdoppelt, ist genau der Grund, warum es nur log₂ n DurchgĂ€nge gibt.

Implementierung

#include "plot.hpp"
#include <stdlib.h>

const int N = 20;
int values[N];
int temp[N];

int width = 1; // size of each run being merged this pass
int lo, mid, hi;
int p, q, k;    // left run pointer, right run pointer, write pointer
bool merging = false;
bool done = false;
int tick = 0;

void setup() {
    PlotCanvas(640, 480);

    for (int n = 0; n < N; n++) {
        values[n] = n + 1;
    }

    srand(1);
    for (int n = N - 1; n > 0; n--) {
        int r = rand() % (n + 1);
        int tmp = values[n];
        values[n] = values[r];
        values[r] = tmp;
    }

    lo = 0;
}

void frame() {
    // Slow the animation down: one step every few calls to frame() instead
    // of one per call, so individual comparisons stay visible.
    if (!done && ++tick >= 4) {
        tick = 0;

        if (!merging) {
            if (width >= N) {
                done = true;
            } else if (lo >= N) {
                width *= 2;
                lo = 0;
                if (width >= N) done = true;
            } else {
                mid = lo + width;
                if (mid > N) mid = N;
                hi = lo + 2 * width;
                if (hi > N) hi = N;

                if (mid >= hi) {
                    lo += 2 * width; // right run empty, nothing to merge here
                } else {
                    p = lo;
                    q = mid;
                    k = lo;
                    merging = true;
                }
            }
        } else {
            if (p < mid && (q >= hi || values[p] <= values[q])) {
                temp[k] = values[p];
                p++;
            } else {
                temp[k] = values[q];
                q++;
            }
            k++;

            if (k >= hi) {
                for (int n = lo; n < hi; n++) {
                    values[n] = temp[n];
                }
                lo += 2 * width;
                merging = false;
            }
        }
    }

    PlotBackground(250, 250, 250);

    int barWidth = plot_width / N;
    for (int idx = 0; idx < N; idx++) {
        int v;
        if (done) {
            PlotColor(70, 170, 90);   // fully sorted
            v = values[idx];
        } else if (merging && idx >= lo && idx < hi) {
            if (idx < k) {
                PlotColor(70, 170, 90);   // already merged this pass
                v = temp[idx];
            } else if (idx == p || idx == q) {
                PlotColor(220, 60, 60);   // heads currently being compared
                v = values[idx];
            } else {
                PlotColor(30, 100, 200);  // waiting in its run
                v = values[idx];
            }
        } else {
            PlotColor(30, 100, 200);      // not part of the active merge
            v = values[idx];
        }

        int height = v * (plot_height - 20) / N;
        int x0 = idx * barWidth + 1;
        int x1 = x0 + barWidth - 2;
        int y0 = plot_height - height;
        int y1 = plot_height;
        PlotFilledRectangle(x0, y0, x1, y1);
    }
}
Open in full editor →

width ist die aktuelle LauflĂ€nge; ein vollstĂ€ndiger Durchgang verschmilzt jedes benachbarte Paar von width-großen LĂ€ufen, danach verdoppelt sich width fĂŒr den nĂ€chsten Durchgang. Innerhalb eines Merges sind p und q das nĂ€chste noch nicht verschmolzene Element des linken bzw. rechten Laufs, k ist die Schreibposition im Hilfsarray temp, und jeder Schritt vergleicht einfach values[p] mit values[q] und hĂ€ngt den kleineren Wert an. Ist ein Laufpaar vollstĂ€ndig verschmolzen, wird es blockweise aus temp zurĂŒck in values kopiert, und lo rĂŒckt zum nĂ€chsten Paar vor.

Da ein Merge immer nur aus dem Originalarray liest und in temp schreibt, kann die Visualisierung zeigen, wie ein Lauf schon vor dem ZurĂŒckkopieren korrekt verschmolzen an Ort und Stelle wĂ€chst — das ist das wachsende grĂŒne Segment innerhalb des aktiven Fensters:

Schritt fĂŒr Schritt durchgerechnet

Das Verschmelzen der beiden bereits sortierten LĂ€ufe [1, 4] und [2, 8]:

p (links) q (rechts) Vergleich schreiben
1 2 1 ≀ 2 1
4 2 2 < 4 2
4 8 4 ≀ 8 4
— 8 linker Lauf erschöpft 8

Ergebnis: [1, 2, 4, 8]. Jedes der vier Elemente wird genau einmal betrachtet — kein erneutes Durchsuchen — was einen Merge bei O(m) fĂŒr zwei LĂ€ufe der GesamtlĂ€nge m hĂ€lt, und den gesamten Durchgang bei O(n) fĂŒr alle LĂ€ufe einer gegebenen Breite zusammen.

Warum sich das zusÀtzliche Array lohnt

Merge Sorts O(n log n)-Grenze gilt bedingungslos, anders als bei Quicksort, weil das Verschmelzen nie davon abhĂ€ngt, wie die Daten zufĂ€llig angeordnet sind — nur davon, wie viele Elemente es sind. Diese Vorhersagbarkeit ist wertvoll genug, dass Merge Sort (oder eine Hybridform davon) der Standard-Sort fĂŒr Daten ist, bei denen der Worst Case wichtig ist, oder bei denen StabilitĂ€t gefordert ist — gleiche Elemente behalten ihre relative Reihenfolge, da der Merge-Schritt bei Gleichstand immer den linken Lauf bevorzugt.

Der Preis ist das temp-Array: Merge Sort braucht O(n) zusĂ€tzlichen Speicher, wo Quicksort und die einfachen Sortierverfahren keinen brauchen. Es bewegt in der Praxis auch tendenziell mehr Daten als Quicksorts In-place-Partitionierung, was der Hauptgrund ist, warum Quicksort bei typischen Eingaben meist schneller ist, obwohl beide im Schnitt O(n log n) sind. Wo Merge Sort klar gewinnt, ist das externe Sortieren — das Sortieren von Daten, die nicht in den Speicher passen — da das Verschmelzen sequenziell von der Festplatte gelesener sortierter Blöcke seinen StĂ€rken weit besser entgegenkommt als Quicksorts Zugriffsmuster mit wahlfreiem Zugriff.

Fazit

Über diese Serie hinweg erzeugt dieselbe zugrunde liegende Idee — Sortieren auf Vergleiche und Umordnungen zurĂŒckfĂŒhren — sehr unterschiedliche Algorithmen, je danach, wie die Arbeit strukturiert ist: Nachbarn wiederholt durchsuchen, einen sortierten PrĂ€fix wachsen lassen, oder das Problem teilen und die Ergebnisse kombinieren. Merge Sorts Beitrag ist eine Garantie: Egal wie die Eingabe aussieht, das Verschmelzen sortierter LĂ€ufe kostet genau das, was es kostet, und nicht mehr.