Sortieren II
Tobias Pröger
20. Oktober (vorläufige Fassung 0 vom 19. Oktober 2016)
Erklärung: Diese Mitschrift ist als Ergänzung zur Vorlesung gedacht. Wir erheben keinen
Anspruch auf Vollständigkeit und Korrektheit. Wir sind froh über Hinweise zu Fehlern oder
Ungenauigkeiten. Bitte senden Sie diese an [email protected].
1
Heapsort
Wir erinnern uns kurz an Sortieren durch Auswahl. Die quadratische Laufzeit dieses
Verfahrens ergab sich aus der linearen Laufzeit zur Bestimmung des maximalen
Schlüssels. Wenn wir also das Maximum effizienter bestimmen könnten, dann ergäbe
sich ein Sortierverfahren, das mit weniger als quadratischer Zeit auskommt. Der im
Folgenden vorgestellte Heap erlaubt die Extraktion des maximalen Schlüssels in Zeit
O(log n), wenn n Schlüssel verwaltet werden.
Heaps Ein Array A[1..n] mit n Schlüsseln heisst Max-Heap, wenn alle Positionen
k ∈ {1, . . . , n} die Heap-Eigenschaft
(2k ≤ m) ⇒ (A[k] ≥ A[2k]) und (2k + 1 ≤ m) ⇒ (A[k] ≥ A[2k + 1])
(1)
erfüllen. Obwohl ein Max-Heap intern als Array gespeichert wird, kann er als binärer
Baum visualisiert werden, wobei jeder Schlüssel in genau einem Knoten gespeichert
wird (vgl. Abbildung 2.1). Wir beobachten, dass die Nachfolger(knoten) des in A an
Position k gespeicherten Knotens im Array an den Positionen 2k sowie 2k +1 gespeichert sind, sofern diese existieren. Die Heap-Eigenschaft besagt damit anschaulich,
dass für einen Knoten mit Schlüssel x die Schlüssel der entsprechenden Nachfolger
höchstens so gross wie x selbst sind. Folglich ist der maximale Schlüssel des Heaps
in A[1] bzw. im obersten Knoten gespeichert. Weiterhin sehen wir, dass jeder unter
einem Knoten gespeicherte Teilbaum ebenfalls als Heap interpretiert werden kann.
Max-Heap
HeapEigenschaft
Auslesen des
Maximums
Abb. 2.1: Darstellung des Max-Heaps A = h9, 6, 7, 3, 4, 5, 2, 1i als binärer
Baum. Der Index rechts neben einem Knoten mit Schlüssel k entspricht
der Position von k im Array A.
Während das reine Auslesen des maximalen Schlüssels also einfach ist, ist nicht offensichtlich, wie er effizient aus dem Heap extrahiert (d.h. entfernt) werden kann.
Das Problem liegt in der starren, durch das Array implizit vorgegebenen Struktur
1
Extraktion des
Maximums
Abb. 2.2: Extraktion des Maximums (Wert 9) in dem in Abbildung
2.1 dargestellten Heaps. Im ersten Schritt wird der in A[8] gespeicherte
Schlüssel nach A[1] kopiert.
Abb. 2.3: Wiederherstellung der Heap-Eigenschaft für die in Abbildung
2.2 dargestellte Struktur. Zunächst werden die Schlüssel 1 und 7 vertauscht, danach 1 und 5.
Abb. 2.4: Der rekonstruierte Heap A = h7, 6, 5, 3, 4, 1, 2i.
des Heaps. Wir lösen das Problem wie folgt. Sei m eine Variable, die die Anzahl der
Elemente eines Heaps A[1..m] speichert. Wir speichern zu extrahierenden Schlüssel
A[1], kopieren den in A[m] gespeicherten Schlüssel nach A[1] und verringern den
Wert von m um 1. Nun aber ist A[1..m] im Allgemeinen kein Heap mehr, denn
an der Wurzel ist die Heap-Eigenschaft verletzt (der dort gespeicherte Schlüssel ist
grösser als die Schlüssel der Nachfolger). Daher wird der in der Wurzel gespeicherte
Schlüssel mit dem grösseren der an den beiden Nachfolgern gespeicherten Schlüssel
vertauscht. Somit ist die Heap-Eigenschaft an der Wurzel erfüllt, kann aber am entsprechenden Nachfolger verletzt sein. Folglich wird genau dort die Heap-Eigenschaft
auf die gleiche Art wiederhergestellt, und die Wiederherstellung wird für die jeweiligen Nachfolger fortgesetzt, bis A[1..m] wieder ein Heap ist. Man beachte, dass die
Heap-Eigenschaft jeweils nur für den Nachfolger w1 wiederhergestellt werden muss,
dessen Schlüssel mit dem Schlüssel des jeweiligen Vorgängers v vertauscht wurde.
Für den anderen Nachfolger w2 (sofern existent) ist die Heap-Eigenschaft sowieso erfüllt, weil in dem unter w2 gespeicherten Teilbaum überhaupt keine Schlüssel
verändert wurden, und weil der in w1 vorher gespeicherte Schlüssel grösser als der
in w2 gespeicherte Schlüssel ist.
2
Restore-Heap-Condition(A, i, m)
1 while 2 ∗ i ≤ m do
. A[i] hat linken Nachfolger
2
j ←2∗i
. A[j] ist linker Nachfolger
3
if j + 1 ≤ m then
. A[i] hat rechten Nachfolger
4
if A[j] < A[j + 1] then j ← j + 1
. A[j] ist grösserer Nachfolger
5
if A[i] < A[j] then Vertausche A[i] und A[j]; i ← j
6
else return
. Heap-Bed. erfüllt: Abbruch
Theorem 1. Sei A[i..m] mit 1 ≤ i < m ein Array, sodass die Heap-Eigenschaft für
alle Positionen k ∈ {i + 1, . . . , m} gilt (d.h., sie ist höchstens bei i verletzt). Die
Laufzeit von Restore-Heap-Condition(A, i, m) ist O(log(m/i)), und nach der
Terminierung gilt die Heap-Eigenschaft für alle Positionen k ∈ {i, . . . , m}.
Wiederherstellung des
Heaps
Laufzeit und
Korrektheit
Beweis. Wir haben bereits im Vorfeld argumentiert, dass der Algorithmus tatsächlich nicht nur die Heap-Eigenschaft an der Position i, sondern für alle Positionen i, i + 1, . . . , m wiederherstellt. Die Laufzeit ist
proportional zur Anzahl der Schleifendurchläufe, denn in den Schritten
2–6 werden nur konstant viele Operationen ausgeführt. Sei f (k) der Wert
der Variablen i vor dem k-ten Durchlauf der Schleife. In den Schritten
2–4 wird der Wert von i mindestens verdopppelt, also sind
f (0) = i und f (k) ≥ 2f (k − 1).
(2)
Man kann leicht induktiv zeigen, dass f (k) ≥ i2k−1 . Die Schleife terminiert spätestens, wenn die Variable i den Wert m erreicht. Wir erhalten
i2k−1 ≤ f (k) ≤ m ⇒ 2k−1 ≤
m
⇒ k − 1 ≤ log(m/i),
i
(3)
also gibt es maximal k ∈ O(log(m/i)) Schleifendurchläufe.
Erzeugung eines Heaps Im vorigen Abschnitt wurde gezeigt, wie das maximale Element effizient aus dem Heap extrahiert werden kann. Es verbleibt zu zeigen,
wie aus einer Menge von n Schlüsseln A[1], . . . , A[n] effizient ein Heap zu dieser
Schlüsselmenge erstellt werden kann. Wir beobachten zunächst, dass für die Positionen bn/2c + 1, . . . , n die Heap-Eigenschaft sofort gilt (anschaulich gesprochen
besitzen diese keine Nachfolgerknoten und können als Heaps mit genau einem Element interpretiert werden). Für die Position bn/2c muss aber die Heap-Eigenschaft
nicht notwendigerweise erfüllt sein. Wir rufen nun für i ∈ {bn/2c, . . . , 1} sukzessive den Algorithmus Restore-Heap-Condition(A, i, n) auf. Vor dem Aufruf der
Methode mit Parameter i galt die Heap-Eigenschaft für alle k ∈ {i + 1, . . . , n}, und
nach Theorem (1) gilt nach Terminierung des Aufrufs die Heap-Eigenschaft für alle
k ∈ {i, . . . , n}. Folglich ist nach dem letzten Aufruf mit dem Parameter i = 1 das
Array A[1..n] ein Max-Heap.
Theorem 2. Sei A[1..n] ein Array. Wird für i = bn/2c, . . . , 1 sukzessive der Algorithmus Restore-Heap-Condition(A, i, n) aufgerufen, dann ist A[1..n] ein MaxHeap. Dabei werden insgesamt nur Θ(n) Operationen ausgeführt.
3
Laufzeit und
Korrektheit
Beweis. Die Korrektheit des Vorgehens wurde bereits im Vorfeld gezeigt. Die Aussage der Laufzeit ist ein wenig trickreicher. Der Aufruf von
Restore-Heap-Condition(A, i, n) hat eine Laufzeit von C log(n/i) für
eine geeignete Konstante C > 0. Wir müssen also
bn/2c
X
bn/2c
C log(n/i) = C
X
i=1
log(n/i) ∈ Θ(n)
(4)
i=1
zeigen. Dazu nutzen wir zunächst aus, dass für k positive Zahlen a1 , . . . , ak
k
X
k
Y
log(ai ) = log
i=1
!
(5)
ai
i=1
gilt. Nun erhalten wir
bn/2c
dn/2e
X
X
log(n/i) ≤
i=1
i=1

Y n
 = log
log(n/i) = log 
i

dn/2e
i=1
ndn/2e
Qdn/2e
i=1
!
i
(∗)
≤ log (2e)dn/2e = dn/2e log(2e) ∈ Θ(n).
(6)
Dabei ist e ≈ 2.71828 die Eulersche Zahl. Die Abschätzung (∗) gilt wegen
dn/2e
Y
i=1
StirlingAbschätzung
i≥
dn/2e
e
dn/2e
≥
n dn/2e
2e
,
und diese wiederum gilt aufgrund der Stirling-Abschätzung
n n
n n √
n! ≥
2πn ≥
.
e
e
(7)
(8)
Heapsort Sei A[1..n] das zu sortierende Array. Zur Sortierung von A benutzen wir
nun zunächst das im vorigen Abschnitt vorgestellte Verfahren zur Erzeugung eines
Heaps aus A, und danach extrahieren wir n − 1 Mal das Maximum aus dem Heap.
Heapsort
Heapsort(A)
1 for i ← bn/2c downto 1 do
2
Restore-Heap-Condition(A, i, n)
3 for m ← n downto 2 do
4
Vertausche A[1] und A[m]
5
Restore-Heap-Condition(A, 1, m − 1)
Laufzeit und
Korrektheit
Theorem 3. Sei A[1..n] ein Array. Der Algorithmus Heapsort sortiert A; dabei
werden maximal O(n log n) Schlüsselvergleiche und -vertauschungen durchgeführt.
Beweis. Nach der Terminierung der Schleife in Schritt 1 ist A[1..n] wegen Theorem (2) ein Max-Heap. Für die Schleife in Schritt 3 zeigen wir
4
nun die folgende Invariante: Nach genau i ∈ {0, . . . , n − 1} Schleifendurchläufen ist A[1..n − i] ein Max-Heap, und für jedes j ∈ {1, . . . , i}
enthält A[n − j + 1] das j-grösste Element.
Induktionsverankerung (i = 0): Die Invariante gilt vor dem ersten
Schleifendurchlauf, da A[1..n] wie gesehen ein Max-Heap ist.
Induktionsannahme: Angenommen, die Invariante ist gültig, nachdem
die Schleife genau i Mal durchlaufen wurde.
Induktionsschluss (i → i + 1): Im (i + 1)-ten Schleifendurchlauf gilt
offenbar m = n−i. Nach Induktionsannahme ist zu Beginn dieses Durchlaufs A[1..n − i] ein Max-Heap, und für jedes j ∈ {1, . . . , i} enthält
A[n − j + 1] das j-grösste Element. Das (n − i + 1)-grösste Element
ist also in derzeit A[1] gespeichert, und durch die Vertauschung mit
A[m] = A[n − i] in Schritt 4 ist am Ende des Durchlaufs für jedes
j ∈ {1, . . . , i + 1} das j-grösste Element in A[n − j + 1] gespeichert.
Der Heap erstreckt sich nun über die Positionen A[1..m − 1], und nur
für die Position 1 kann die Heap-Bedingung verletzt sein. Nach Theorem
(1) wird diese in Schritt 5 aber wiederhergestellt, also gilt die Invariante
auch am Ende des (i + 1)-ten Schleifendurchlaufs.
Nach der Terminierung des Algorithmus ist A[1..n] also sortiert, und
wegen den Theoremen (1) und (2) ist die Gesamtlaufzeit durch O(log n)
nach oben beschränkt.
2
Mergesort
Idee Schon beim Maximum-Subarray-Problem haben wir gesehen, dass Divideand-Conquer helfen kann, die Laufzeit eines Algorithmus zu verbessern. Dieses Prinzip des rekursiven Aufteilens führt zu folgender Idee für ein Sortierverfahren: Die Folge A[1], . . . , A[n] wird in die zwei annähernd gleich grossen Teilfolgen A[1], . . . , A[bn/2c]
sowie A[bn/2c + 1], . . . , A[n] aufgeteilt, die rekursiv sortiert werden. Nach der Sortierung der Teilfolgen werden diese wieder zu einer sortierten Gesamtfolge der Länge
n zusammengefügt. Wir zeigen zunächst, wie diese Verschmelzung sortierter Teilfolgen realisiert werden kann. Danach beschreiben wir Sortierverfahren, die bereits
sortierte Teilfolgen verschmelzen und so eine insgesamt sortierte Folge erzeugen.
Verschmelzen sortierter Teilfolgen Seien F1 und F2 zwei sortierte Teilfolgen. Die
sortierte Gesamtfolge F wird erzeugt, indem F1 und F2 mit zwei Zeigern von vorn
nach hinten durchlaufen werden. Dies geschieht mithilfe von zwei Variablen i und
j, die wir mit 1 initialisieren. Ist F1 [i] < F2 [j], dann wird F1 [i] das nächste Element
von F , und der Zeiger i wird um 1 erhöht. Ansonsten wird F2 [j] das nächste Element
von F , und j wird um 1 erhöht. Dieses Vorgehen wird wiederholt, bis entweder F1
oder F2 vollständig durchlaufen sind. Danach wird die noch nicht erschöpfte Folge
komplett an das Ende von F gehängt.
Der im Folgenden vorgestellte Algorithmus Merge realisiert die Verschmelzung
zweier bereits sortierter Teilfolgen. Diesem Algorithmus werden die vier Parameter
A, left, middle und right übergeben. Dabei findet sich die bereits sortierte Teilfolge
F1 in A[left..middle] und F2 in A[middle + 1..right]. Die verschmolzene Folge F soll
schliesslich in A[left..right] gespeichert werden. Da dies genau die Positionen in A
sind, an denen F1 und F2 gespeichert sind, muss die sortierte Folge zunächst in einem
5
Hilfsarray B der Länge right−left+1 gespeichert werden. Die Inhalte von B werden
dann im letzten Schritt auf die entsprechenden Positionen im Array A kopiert.
Verschmelzen
der Teilfolgen
Merge(A, left, middle, right)
1 B ← new Array[right-left+1]
. Hilfsarray zum Verschmelzen
2 i ← left; j ← middle+1; k ← 1
. Zeiger zum Durchlaufen
3 . Wiederhole, solange beide Teilfolgen noch nicht erschöpft sind
while (i ≤ middle) and (j ≤ right) do
4
if A[i] ≤ A[j] then B[k] ← A[i]; i ← i + 1
5
else B[k] ← A[j]; j ← j + 1
6
k ←k+1
7 . Falls die erste Teilfolge noch nicht erschöpft ist, hänge sie hinten an
while i ≤ middle do B[k] ← A[i]; i ← i + 1; k ← k + 1
8 . Falls die zweite Teilfolge noch nicht erschöpft ist, hänge sie hinten an
while j ≤ right do B[k] ← A[j]; j ← j + 1; k ← k + 1
9 . Kopiere Inhalte von B nach A zurück
for k ← left to right do A[k] ← B[k − left + 1]
Korrektheit
Lemma 4. Seien A[1..n] ein Array, 1 ≤ left ≤ middle ≤ right ≤ n und sowohl
A[left..middle] als auch A[middle + 1..right] bereits sortiert. Nach dem Aufruf von
Merge(A, left, middle, right) ist A[left..right] komplett sortiert.
Beweis. Wir zeigen zunächst induktiv die folgende Aussage: Nach k
Durchläufen der Schleife in Schritt 3 ist B[1..k] sortiert, und es sind
B[k] ≤ A[i], falls i ≤ middle, sowie B[k] ≤ A[j], falls j ≤ right.
Induktionsverankerung (k = 0): Vor dem ersten Schleifendurchlauf
ist B[1..0] ein Array der Länge 0 und die Aussage gilt trivialerweise. (!)
Induktionsannahme: Sei nach k Durchläufen der Schleife B[1..k] sortiert, und B[k] ≤ A[i], falls i ≤ middle, sowie B[k] ≤ A[j], falls j ≤ right.
Induktionsschluss (k → k + 1): Betrachte den (k + 1)-ten Durchlauf
der Schleife in Schritt 3. Es seien o.B.d.A. A[i] ≤ A[j], i ≤ middle und
j ≤ right. Nach Induktionsannahme ist B[1..k] sortiert, und B[k] ≤ A[i].
Wird also B[k+1] ← A[i] gesetzt, dann ist B[1..k+1] noch immer sortiert,
und B[k+1] = A[i] ≤ A[i+1], falls (i+1) ≤ middle, sowie B[k+1] ≤ A[j],
falls j ≤ right. Anschliessend werden sowohl i als auch k um 1 erhöht,
also gilt die Aussage auch nach dem (k + 1)-ten Schleifendurchlauf. Der
Fall A[j] < A[i] wird analog bewiesen.
Nach der Terminierung der Schleife in Schritt 3 gilt entweder i >
middle oder j > right. Also wird genau eine der Schleifen in den Schritten
7 und 8 ausgeführt. Sei o.B.d.A. i ≤ middle. Dann ist nach k Durchläufen
der Schleife im dritten Schritt B[k] ≤ A[i], und da A[i..middle] sortiert
ist, können die verbleibenden Elemente von A[i..middle] an das Ende von
B gehängt werden, und nach Schritt 8 ist B[1..(right − left + 1)] komplett
sortiert.
6
Lemma 5. Seien A[1..n] ein Array, 1 ≤ left < right ≤ n, middle = b(left + right)/2c
und sowohl A[left..middle] als auch A[middle + 1..right] bereits sortiert. Im Aufruf
von Merge(A, left, middle, right) werden Θ(right − left) Schlüsselbewegungen und
Vergleiche ausgeführt.
Laufzeit
Beweis. Der Aufwand zur Initialisierung des Arrays der Grösse (right −
left + 1) im ersten Schritt beträgt Θ(right − left). Die Schleifen in den
Schritten 3 und 9 werden Θ(right − left) Mal durchlaufen, die Schleifen
in den Schritten 7 und 8 nur höchstens O(right − left) Mal. Da für alle
anderen Schritte nur Zeit O(1) anfällt, ergibt sich eine Gesamtlaufzeit
von Θ(right − left). In jedem Durchlauf der Schleife in Schritt 3 wird
genau ein Schlüsselvergleich ausgeführt. Ausserdem wird jedes Element
genau einmal nach B und genau einmal zurück nach A kopiert. Damit
beträgt die Anzahl der Schlüsselbewegungen und Vergleiche ebenfalls
Θ(right − left).
Rekursives 2-Wege-Mergesort
Der im letzten Abschnitt vorgestellte Algorithmus zur Verschmelzung zweier sortierter Teilfolgen kann nun benutzt werden, um ein Array rekursiv zu sortieren. Dem
im Folgenden vorgestellten Algorithmus Mergesort werden die drei Parameter A,
left und right übergeben, und die Aufgabe besteht in der Sortierung des Teilarrays
A[left..right]. Zur Sortierung des gesamten Arrays A[1..n] wird dann Mergesort(A,
1, n) aufgerufen.
Mergesort(A, left, right)
Rekursives
Mergesort
1 if left < right then
2
middle ← b(left + right)/2c
3
Mergesort(A, left, middle)
4
Mergesort(A, middle+1, right)
5
Merge(A, left, middle, right)
.
.
.
.
Mittlere Position
Sortiere vordere Hälfte von A
Sortiere hintere Hälfte von A
Verschmilz Teilfolgen
Theorem 6. Sei A[1..n] ein Array. Der Aufruf von Mergesort(A, 1, n) sortiert
A und benötigt in jedem Fall Θ(n log n) viele Schlüsselbewegungen und Vergleiche.
Beweis. Für den Nachweis der Korrektheit von Mergesort benutzen
wir verallgemeinerte Induktion über die Arraygrösse n.
Induktionsverankerung (n = 1): Arrays der Länge 1 sind bereits sortiert. Mergesort terminiert direkt, da die Parameter left und right den
gleichen Wert haben.
Induktionsannahme: Wir nehmen an, dass alle Arrays mit höchstens
n Elementen korrekt sortiert werden.
Induktionsschluss (n → n + 1): Betrachte ein Array mit n + 1 Elementen. Es ist left < right, damit sind A[left..middle] sowie A[middle +
1..right] Arrays mit höchstens n Elementen. Diese werden nach Induktionsvoraussetzung korrekt sortiert. Nach Lemma 4 werden diese sortierten Teilarrays so verschmolzen, dass A[1..n + 1] sortiert ist.
7
Laufzeit und
Korrektheit
Zum Verschmelzen zweier annähernd gleich langer Teilfolgen mit Gesamtlänge n werden nach Lemma 5 Θ(n) Schlüsselvergleiche benötigt.
Für ein Array der Länge 1 werden C(1) = Θ(1) viele Schlüssel verglichen,
und allgemein benötigt Mergesort zum Sortieren von n Elementen
l n m
j n k
C(n) = C
+C
+ Θ(n) ∈ Θ(n log n)
(9)
2
2
Schlüsselvergleiche. Die ersten beiden Summanden beschreiben die von
den rekursiven Aufrufen (in den Schritten 3 und 4) benötigten Schlüsselvergleiche, und Θ(n) ist die zur Verschmelzung der sortierten Teilfolgen
benötigten Vergleiche. Die oben beschriebene Anzahl an Schlüsselvergleichen gilt in jedem Fall, damit ergibt sich
Cmin (n) = Cavg (n) = Cmax (n) ∈ Θ(n log n).
(10)
Analog beträgt die Anzahl der Schlüsselbewegungen
Mmin (n) = Mavg (n) = Mmax (n) ∈ Θ(n log n),
(11)
da nach Lemma 5 beim Verschmelzen zweier Folgen mit Gesamtlänge n
auch Θ(n) viele Schlüssel bewegt werden.
Reines 2-Wege-Mergesort
Ist n = 2k eine Zweierpotenz, dann verschmilzt rekursives Mergesort zum Sortieren
von A[1..n] zunächst alle Teilfolgen der Länge 1, danach alle Teilfolgen der Länge 2,
danach alle Teilfolgen der Länge 4 usw. Reines 2-Wege-Mergesort greift diese Idee
(auch für allgemeine n) auf, vermeidet dabei aber die rekursiven Aufrufe. Im k-ten
Durchlauf wird das Array von links nach rechts durchlaufen, und je zwei Folgen
der Länge 2k−1 werden zu einer sortierten Folge der Länge 2k verschmolzen. Eine
Ausnahme bildet die Folge am rechten Rand des Arrays: Ist n keine Potenz von 2,
dann kann sie weniger als 2k−1 Elemente besitzen (und die Verschmelzung mit einer
anderen Teilfolge weniger als 2k Elemente). In jedem Durchlauf verdoppelt sich also
die Länge der sortierten Teilfolgen, und die gesamte Folge ist sortiert, wenn in einem
Durchlauf nur noch zwei Teilfolgen verschmolzen werden müssen.
Reines 2-WegeMergesort
Laufzeit und
Korrektheit
StraightMergesort(A)
1 length ← 1
2 while length < n do
3
right ← 0
4
while right+length < n do
5
left ← right+1
6
middle ← left+length−1
7
right ← min(middle+length, n)
8
Merge(A, left, middle, right)
9
length ← length∗2
. Länge bereits sortierter Teilfolgen
. Verschmilz Folgen d. Länge length
. A[1..right] ist abgearbeitet
. Es gibt noch mind. zwei Teilfolgen
. Linker Rand der ersten Teilfolge
. Rechter Rand der ersten Teilfolge
. Rechter Rand der zweite Teilfolge
. Verschmilz beide Teilfolgen
. Verdoppelte Länge der Teilfolgen
Theorem 7. Sei A[1..n] ein Array. Der Aufruf von StraightMergesort(A) sortiert A und benötigt in jedem Fall Θ(n log n) viele Schlüsselbewegungen und Vergleiche.
8
Natürliches 2-Wege-Mergesort
Natürliches 2-Wege-Mergesort vermeidet wie reines 2-Wege-Mergesort rekursive Aufrufe, beginnt aber initial mit der Verschmelzung von möglichst langen bereits sortierte Teilfolgen, während reines 2-Wege-Mergesort immer mit sortierten Folgen der
Länge 1 beginnt. Das zu sortierende Array A bestehe nun aus k längsten bereits sortierten Teilfolgen (sog. Runs) R1 , . . . , Rk . Ein neuer Run beginnt also genau dann,
wenn ein kleinerer Schlüssel auf einen grösseren folgt. Natürliches 2-Wege-Mergesort
verschmilzt R1 mit R2 , danach R3 mit R4 usw. Dieses Verfahren wird so lange wiederholt, bis es nur noch einen Run (das sortierte Array A) gibt.
NaturalMergesort(A)
1 repeat
2
right ← 0
. Elemente bis A[right] sind bearbeitet
3
while right < n do
. Finde und verschmilz die nächsten Runs
4
left ← right+1
. Linker Rand des ersten Runs
5
middle ← left
. A[left..middle] ist bereits sortiert
6
while (middle < n) and A[middle + 1] ≥ A[middle] do
7
middle ← middle+1 . Ersten Run um ein Element vergrössern
8
if middle < n then
. Es gibt einen zweiten Run
9
right ← middle+1
. Rechter Rand des zweiten Runs
10
while (right < n) and A[right + 1] ≥ A[right] do
11
right ← right+1
. Zweiten Run um ein Element vergrössern
12
Merge(A, left, middle, right)
13
else right ← n
. Es gibt keinen zweiten Run
14 until left = 1
Theorem 8. Der Algorithmus NaturalMergesort führt
Laufzeit
(a) im besten Fall keine Schlüsselbewegungen und n − 1 Vergleiche,
(b) im durchschnittlichen sowie im schlechtesten Fall Θ(n log n) Schlüsselbewegungen und Θ(n log n) Vergleiche aus.
Beweis.
(a) Im besten Fall ist das Array bereits sortiert, d.h. es besteht nur aus
einem Run. Dann ist A[middle + 1] ≥ A[middle] in Schritt 6 immer
wahr, und die Schleife terminiert für middle = n. Folglich wird in
Schritt 13 right ← n gesetzt, und die Schleife in Schritt 3 terminiert
nach einem Durchlauf. Es werden also keine keine Schlüssel bewegt,
und die Anzahl der Vergleiche beträgt Cmin (n) = n − 1.
(b) Im schlechtesten Fall wird die Schleife im dritten Schritt dlog ne
Mal durchlaufen. Wie beim reinen 2-Wege-Mergesort ergeben sich
damit
Cmax (n) ∈ Θ(n log n) und Mmax (n) ∈ Θ(n log n).
9
Natürliches
2-WegeMergesort
(12)
Zur Analyse des durchschnittlichen Falls überlegen wir zunächst,
dass eine zufällige Permutation von n Schlüsseln im Mittel n/2
Runs enthält: Für zwei benachbarte Schlüssel ki und ki+1 ist die
Wahrscheinlichkeit für ki < ki+1 genauso gross wie für ki > ki+1 ,
also 1/2. Ist ki > ki+1 , dann endet bei ki ein Run. Damit ergeben
sich im durchschnittlichen Fall n/2 Runs, und wir benötigen
Cavg (n) ∈ Θ(n log n) und Mavg (n) ∈ Θ(n log n)
Vergleiche bzw. Schlüsselbewegungen.
10
(13)