https://de.wikipedia.org/w/api.php?action=feedcontributions&feedformat=atom&user=ParAlgMergeSortWikipedia - Benutzerbeiträge [de]2025-05-29T14:14:57ZBenutzerbeiträgeMediaWiki 1.45.0-wmf.2https://de.wikipedia.org/w/index.php?title=Merge-Algorithmen&diff=198466970Merge-Algorithmen2020-04-04T13:22:08Z<p>ParAlgMergeSort: Parallele Sektion aus dem englischen Wikipedia übersetzt.</p>
<hr />
<div>'''Merge-Algorithmen''' (von {{enS|''merge''}} ‚verschmelzen‘) sind eine Familie von Algorithmen, die mehrere [[Sortierverfahren|sortierte]] Listen als Eingabe erhalten und eine einzelne sortierte Liste ausgeben, welche alle Elemente der Eingabelisten enthält. Merge-Algorithmen werden in vielen Algorithmen als [[Unterprogramm]] verwendet. Ein bekanntes Beispiel dafür ist [[Mergesort]].<br />
<br />
== Anwendung ==<br />
[[Datei:Merge sort algorithm diagram.svg|mini|hochkant=1.5|Beispiel für Mergesort]]<br />
Der Merge-Algorithmus spielt eine wichtige Rolle im [[Mergesort]] Algorithmus, einem [[Sortierverfahren#Vergleichsbasiertes Sortieren|vergleichsbasierten Sortieralgorithmus]]. Konzeptionell besteht der Mergesort-Algorithmus aus zwei Schritten:<br />
<br />
# Teile die Eingabe [[Rekursion#Weitere Anwendungen: Rekursion in der Programmierung|rekursiv]] in kürzere Listen von ungefähr gleicher Länge, bis jede Liste nur noch ein Element enthält. Eine Liste, welche nur ein Element enthält, ist nach Definition sortiert.<br />
# Verschmilz wiederholt die kürzeren Listen, bis eine einzelne Liste alle Elemente enthält. Diese Liste ist nun die fertig sortierte Liste.<br />
<br />
Der Merge-Algorithmus wird als Teil des Mergesort-Algorithmus immer wieder ausgeführt.<br />
<br />
Ein Beispiel für Mergesort wird im Bild dargestellt. Man beginnt mit einer unsortierten Liste aus 7 Zahlen. Das Array wird in 7 Partitionen aufgeteilt, wovon jede nur ein Element enthält. Die sortierten Listen werden dann verschmolzen, um längere sortierte Listen zu liefern, bis nur noch eine sortierte Liste übrig ist.<br />
<br />
== Verschmelzen von zwei Listen ==<br />
<br />
Das Verschmelzen von zwei sortierten Listen kann in linearer [[Zeitkomplexität]] und mit linearem Platz erfolgen. Der folgende [[Pseudocode]] zeigt einen Algorithmus, welcher zwei Listen <code>A</code> und <code>B</code> in eine neue Liste <code>C</code> verschmilzt.<ref name="skiena">{{cite book |last=Skiena |first=Steven |title=The Algorithm Design Manual |publisher=[[Springer Science+Business Media]] |edition=2nd |year=2010 |isbn=1-849-96720-2 |page=123}}</ref> Die Funktion <code>kopf</code> gibt das erste Element der Liste zurück. Entfernen des ersten Elements wird typischerweise über das Inkrementieren eines Pointers realisiert.<br />
<br />
'''programm''' merge(A, B)<br />
'''eingabe''' A, B : Liste<br />
'''ausgabe''' Liste<br />
<br />
C := neue leere Liste<br />
'''solange''' A ist nicht leer und B ist nicht leer<br />
'''wenn''' kopf(A) ≤ kopf(B) '''dann'''<br />
hänge kopf(A) an C an<br />
entferne den Kopf von A<br />
'''sonst'''<br />
hänge kopf(B) an C an<br />
entferne den Kopf von B<br />
<br />
''// Entweder A oder B ist leer. Nun muss noch die andere Liste an C angehängt werden.''<br />
'''solange''' A ist noch nicht leer<br />
hänge kopf(A) an C an<br />
entferne den Kopf von A<br />
'''solange''' B ist noch nicht leer<br />
hänge kopf(B) an C an<br />
entferne den Kopf von B<br />
<br />
'''ausgabe''' C<br />
<br />
Das Erstellen einer neuen Liste <code>C</code> kann vermieden werden. Dadurch wird der Algorithmus allerdings langsamer und schwerer zu verstehen.<ref>{{cite journal |last1=Katajainen |first1=Jyrki |first2=Tomi |last2=Pasanen |first3=Jukka |last3=Teuhola |title=Practical in-place mergesort |journal=Nordic J. Computing |volume=3 |issue=1 |year=1996 |pages=27–40 }}</ref><br />
<br />
== ''k''-Wege-Mischen ==<br />
Beim ''k''-Wege-Mischen werden k sortierte Listen zu einer einzelnen, sortierten Liste verschmolzen, welche die gleichen Elemente wie die Ursprungslisten enthält. Sei n die Gesamtzahl der Elemente. Dann ist n die Größe der Ausgabe-Liste und auch die Summe der Größen der Eingabe-Listen. Das Problem kann in einer Laufzeit von O(n log k) und mit einem Platzbedarf von O(n) gelöst werden. Es existieren verschiedene Algorithmen.<br />
<br />
=== Direktes ''k''-Wege-Mischen ===<br />
Die Idee von direktem ''k''-Wege-Mischen ist es, das kleinste Element aller k Listen zu finden und an die Ausgabe anzuhängen. Eine naive Implementierung wäre es, in jedem Schritt alle k Listen zu durchsuchen, um das Minimum zu finden. Diese Lösung hat eine Laufzeit von Θ(kn). Dies funktioniert prinzipiell, ist allerdings nicht besonders effizient.<br />
<br />
Die Laufzeit kann verbessert werden, indem das kleinste Element schneller gefunden werden kann. Über die Verwendung von [[Heap (Datenstruktur)|Heaps]] oder Turnierbäumen (tournament trees) kann das kleinste Element in Zeit O(log k) gefunden werden. Die resultierende Zeit für das Verschmelzen liegt dann insgesamt in O(n log k).<br />
<br />
Heaps werden in der Praxis häufig verwendet, allerdings besitzen Turnierbäume eine etwas bessere Laufzeit. Ein Heap benötigt etwa 2*log(k) Vergleiche in jedem Schritt, da er den Baum von der Wurzel nach unten zu den Blättern bearbeitet. Ein Turnierbaum dagegen benötigt nur log(k) Vergleiche, da er unten am Baum anfängt und sich mit nur einem Vergleich pro Ebene zur Wurzel nach oben arbeitet. Aus diesem Grund sollten Turnierbäume bevorzugt verwendet werden.<br />
<br />
=== Heap ===<br />
Der Heap-Algorithmus<ref>{{cite book<br />
|last1=Bentley<br />
|first1=Jon Louis<br />
|title=Programming Pearls<br />
|date=2000<br />
|publisher=Addison Wesley<br />
|isbn=0201657880<br />
|pages=147–162<br />
|edition=2nd}}</ref><br />
erstellt einen [[Heap (Datenstruktur)|min-Heap]] mit Zeigern zu den Eingabelisten. Die Zeiger werden nach dem Element sortiert, auf welches sie zeigen. Der Heap wird durch einen <code>heapify</code> Algorithmus in einer Laufzeit von O(k) erstellt. Danach speichert der Algorithmus iterativ das Element, auf das der Wurzel-Zeiger zeigt, in die Ausgabe und führt einen <code>increaseKey</code> Algorithmus auf dem Heap aus. Die Laufzeit für den <code>increaseKey</code> Algorithmus liegt in O(log k). Da n Elemente zu verschmelzen sind, beträgt die gesamte Laufzeit O(n log k).<br />
<br />
=== Turnierbaum ===<br />
[[Datei:Tournament tree.png|mini|Turnierbaum]]<br />
Der Turnierbaum (tournament tree)<ref name="knuth98"><br />
{{cite book| last = Knuth| first = Donald| authorlink = Donald Knuth| series = [[The Art of Computer Programming]]| volume= 3| title= Sorting and Searching| edition = 2nd| publisher = Addison-Wesley| year= 1998| chapter = Chapter 5.4.1. Multiway Merging and Replacement Selection| pages = 252–255| isbn = 0-201-89685-0| ref = harv}}</ref> basiert auf einem [[Turnierform|Turnier]] im [[Turnierform#K.-o.-System|K.-o.-System]], wie es auch im Sport benutzt wird. In jedem Spiel treten zwei der Eingabe-Elemente gegeneinander an. Der Gewinner wird nach oben weitergegeben. Aus diesem Grund bildet sich ein [[Binärbaum]] aus Spielen. Die Liste wird hier in aufsteigender Reihenfolge sortiert, somit ist der Gewinner eines Spiels jeweils das kleinere der Elemente.<br />
<br />
[[Datei:Loser tree.png|mini|Verlierer-Baum]]<br />
<br />
Beim ''k''-Wege-Mischen ist es effizienter, nur den Verlierer jedes Spiels abzuspeichern (siehe Grafik). Die daraus resultierende Datenstruktur wird Verlierer-Baum (''Loser tree'') genannt. Beim Aufbau des Baums oder beim Ersetzen eines Elements wird trotzdem der Gewinner nach oben weitergegeben. Der Baum wird also gefüllt wie bei einem normalen Sport-Turnier, allerdings wird in jedem Knoten nur der Verlierer gespeichert. Normalerweise wird oberhalb der Wurzel ein zusätzlicher Knoten eingefügt, welcher den gesamten Gewinner speichert.<br />
Jedes Blatt speichert einen Zeiger zu einer der Eingabe-Listen. Jeder innere Knoten speichert einen Wert und einen Zeiger zu einer der Eingabe-Listen. Der Wert enthält eine Kopie des ersten Elements der zugehörigen Liste.<br />
<br />
Der Algorithmus hängt iterativ das kleinste Element an die Rückgabe-Liste an und entfernt das Element dann von der zugehörigen Eingabe-Liste. Er aktualisiert dann alle Knoten auf dem Weg vom aktualisierten Blatt zur Wurzel (''replacement selection''). Das zuvor entfernte Element ist der gesamte Gewinner. Der Gewinner hat jedes Spiel auf dem Pfad zwischen Eingabe-Liste und Wurzel gewonnen. Wenn nun ein neues Element ausgewählt wird, muss dieses gegen die Verlierer der letzten Runde antreten. Durch die Verwendung eines Verlierer-Baums sind die jeweiligen Gegner bereits in den Knoten gespeichert. Der Verlierer jedes neu gespielten Spiels wird in den Knoten geschrieben und der Gewinner wird weiter nach oben in Richtung der Wurzel gegeben. Wenn die Wurzel erreicht wird, ist der gesamte Gewinner gefunden und es kann eine neue Runde des Verschmelzens gestartet werden.<br />
<br />
Die Bilder von Turnierbaum und Verlierer-Baum in diesem Abschnitt verwenden dieselben Daten und können zum besseren Verständnis miteinander verglichen werden.<br />
<br />
==== Algorithmus ====<br />
<br />
Ein Turnierbaum lässt sich als perfekter [[Binärbaum]] darstellen, indem an jede Liste [[Sentinel (Programmierung)|Sentinels]] hinzugefügt werden und die Anzahl der Eingabelisten durch leere Listen zu einer Zweierpotenz erweitert wird. Dann kann er in einem einzelnen Array dargestellt werden. Man erreicht das Eltern-Element, indem man den aktuellen Index durch 2 teilt.<br />
<br />
Wird eines der Blätter aktualisiert, werden alle Spiele von diesem Blatt aus nach oben neu ausgetragen. Im folgenden [[Pseudocode]] wird zum einfacheren Verständnis kein Array verwendet, sondern ein objektorientierter Ansatz. Zusätzlich wird davon ausgegangen, dass die Anzahl der eingegebenen Folgen eine Zweierpotenz ist.<br />
<br />
'''programm''' merge(L1, …, Ln)<br />
erstelleBaum(kopf von L1, …, Ln)<br />
'''solange''' Baum hat Elemente<br />
gewinner := baum.gewinner<br />
ausgabe gewinner.wert<br />
neu := gewinner.zeiger.nächsterEintrag<br />
spieleNeu(gewinner, neu) // Replacement selection<br />
<br />
'''programm''' spieleNeu(knoten, neu)<br />
verlierer, gewinner := spiele(knoten, neu)<br />
knoten.wert := verlierer.wert<br />
knoten.zeiger := verlierer.zeiger<br />
'''wenn''' knoten nicht Wurzel<br />
spieleNeu(knoten.parent, gewinner)<br />
<br />
'''programm''' erstelleBaum(elemente)<br />
nächsteEbene := new Array()<br />
'''solange''' elemente nicht leer<br />
el1 := elemente.take() // ein Element nehmen<br />
el2 := elemente.take()<br />
verlierer, gewinner := spiele(el1, el2)<br />
parent := new Node(el1, el2, verlierer)<br />
nächsteEbene.add(parent)<br />
'''wenn''' nächsteEbene.länge == 1<br />
'''ausgabe''' nächsteEbene // nur Wurzel<br />
'''sonst'''<br />
'''ausgabe''' erstelleBaum(nächsteEbene)<br />
<br />
==== Laufzeit ====<br />
<br />
Der initiale Aufbau des Baums erfolgt in Zeit Θ(k). In jedem Schritt des Verschmelzens müssen die Spiele auf dem Pfad vom neuen Element zur Wurzel neu ausgetragen werden. In jeder Ebene ist nur eine Vergleichsoperation notwendig. Da der Baum balanciert ist, enthält der Pfad von Eingabe-Liste zur Wurzel nur Θ(log k) Elemente. Insgesamt müssen n Elemente übertragen werden. Die resultierende Gesamtlaufzeit liegt also in Θ(n log k).<ref name="knuth98" /><br />
<br />
==== Beispiel ====<br />
<br />
Der folgende Abschnitt enthält ein detailliertes Beispiel für das erneute Spielen (replacement selection) und ein Beispiel für einen gesamten Merge-Prozess.<br />
<br />
===== Replacement selection =====<br />
<br />
Spiele werden auf dem Weg von unten nach oben neu ausgetragen. In jeder Ebene des Baums treten das im Knoten gespeicherte Element und das von unten erhaltene gegeneinander an. Der Gewinner wird immer weiter nach oben gegeben, bis am Ende der gesamte Gewinner gespeichert wird. Der Verlierer wird im jeweiligen Knoten des Baums gespeichert.<br />
<br />
[[Datei:Loser tree replacement selection.gif|centre|mini|Beispiel für replacement selection]]<br />
<br />
{| class="wikitable"<br />
|-<br />
! Schritt !! Aktion<br />
|-<br />
| 1 || Blatt 1 (gesamter Gewinner) wird durch 9, dem nächsten Element in der zugehörigen Eingabe-Liste, ersetzt.<br />
|-<br />
| 2 || Das Spiel 9 gegen 7 (Verlierer der letzten Runde) wird neu ausgetragen. 7 gewinnt, da es die kleinere Zahl ist. Aus diesem Grund wird 7 nach oben weitergegeben, während 9 im Knoten gespeichert wird.<br />
|-<br />
| 3 || Das Spiel 7 gegen 3 (Verlierer der letzten Runde) wird neu ausgetragen. 3 gewinnt, da es die kleinere Zahl ist. Aus diesem Grund wird 3 nach oben weitergegeben, während 7 im Knoten gespeichert wird.<br />
|-<br />
| 4 || Das Spiel 3 gegen 2 (Verlierer der letzten Runde) wird neu ausgetragen. 2 gewinnt, da es die kleinere Zahl ist. Aus diesem Grund wird 2 nach oben weitergegeben, während 3 im Knoten gespeichert wird.<br />
|-<br />
| 5 || Der neue gesamte Gewinner, 2, wird über der Wurzel gespeichert.<br />
|}<br />
<br />
===== Verschmelzen =====<br />
Zum eigentlichen Verschmelzen wird immer wieder das kleinste Element entnommen und mit dem nächsten Element der Eingabe-Liste ersetzt. Danach werden die Spiele bis zur Wurzel neu ausgetragen.<br />
<br />
Als Eingabe werden in diesem Beispiel vier sortierte Arrays benutzt.<br />
<br />
{2, 7, 16}<br />
{5, 10, 20}<br />
{3, 6, 21}<br />
{4, 8, 9}<br />
<br />
Der Algorithmus wird mit den Köpfen der Eingabelisten instanziiert. Aus diesen Elementen wird dann ein Baum aus Verlierern aufgebaut. Zum Verschmelzen wird das kleinste Element, 2, durch Lesen des obersten Elements im Baum bestimmt. Dieser Wert wird nun vom zugehörigen Eingabe-Array entfernt und durch den Nachfolger, 7, ersetzt. Die Spiele von dort aus zur Wurzel werden neu ausgetragen, wie es bereits im vorherigen Abschnitt beschrieben ist. Das nächste Element, das entfernt wird, ist 3. Beginnend mit dem nächsten Listenelement, 6, werden die Spiele wieder bis zur Wurzel neu ausgetragen. Dies wird so lange wiederholt, bis das gesamte Minimum oberhalb der Wurzel ''unendlich'' beträgt.<br />
<br />
[[Datei:Loser tree merge.gif|centre|mini|Visualisierung für den gesamten Algorithmus]]<br />
<br />
=== Laufzeitanalyse ===<br />
<br />
Es kann gezeigt werden, dass kein [[Sortierverfahren#Vergleichsbasiertes Sortieren|vergleichsbasierter]] Algorithmus zum ''k''-Wege-Mischen existieren kann, welcher eine schnellere Laufzeit als O(n log k) besitzt. Dies kann leicht durch Reduktion auf das vergleichsbasierte Sortieren bewiesen werden. Gäbe es einen schnelleren Algorithmus, könnte man einen vergleichsbasierten Sortieralgorithmus entwerfen, der schneller als O(n log n) sortiert. Der Algorithmus teilt die Eingabe in k=n Listen auf, die jeweils nur ein Element enthalten. Dann verschmilzt er die Listen. Wäre das Mergen in unter O(n log k) möglich, ist dies ein Widerspruch zum [[Sortierverfahren#Beweis der unteren Schranke für vergleichsbasiertes Sortieren|weit bekannten Ergebnis]], dass für Sortieren im worst-case eine untere Schranke von O(n log n) gilt.<br />
<br />
== Paralleles Mischen ==<br />
Eine parallele Version des binären Mischens dient als Baustein für einen [[Mergesort#Paralleler Mergesort|parallelen Mergesor]]<nowiki/>t Algorithmus. Der folgende Pseudocode demonstriert einen parallelen [[Teile-und-herrsche-Verfahren|Teile-und-Herrsche]] Mischalgorithmus (adaptiert von ''Cormen et al.<ref name=":2">{{Literatur |Autor=Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford |Titel=Introduction to algorithms |Hrsg= |Auflage=Third edition |Verlag=MIT Press |Ort=Cambridge, Mass. |Datum=2009 |ISBN=978-0-262-27083-0 |Seiten=}}</ref>''<sup>:800</sup>). Er operiert auf zwei sortierten Arrays <math>A</math> und <math>B</math> und schreibt den sortierten Output in das Array <math>C</math>. Die Notation <math>A[i...j]</math> bezeichnet den Teil von <math>A</math> von Index <math>i</math> bis exklusive <math>j</math>.<br />
<br />
'''algorithm''' merge(A[i...j], B[k...ℓ], C[p...q]) '''is'''<br />
'''inputs''' A, B, C : array<br />
i, j, k, ℓ, p, q : indices<br />
<br />
'''let''' m = j - i,<br />
n = ℓ - k<br />
<br />
'''if''' m < n '''then'''<br />
swap A and B ''// ensure that A is the larger array: i, j still belong to A; k, ℓ to B''<br />
swap m and n<br />
<br />
'''if''' m ≤ 0 '''then'''<br />
'''return''' ''// base case, nothing to merge''<br />
<br />
'''let''' r = ⌊(i + j)/2⌋<br />
'''let''' s = binary-search(A[r], B[k...ℓ])<br />
'''let''' t = p + (r - i) + (s - k)<br />
C[t] = A[r]<br />
<br />
'''in parallel do'''<br />
merge(A[i...r], B[k...s], C[p...t])<br />
merge(A[r+1...j], B[s...ℓ], C[t+1...q])<br />
<br />
Der Algorithmus beginnt indem entweder <math>A</math> oder <math>B</math> (abhängig welches Array mehr Elemente enthält) in in zwei Hälften aufgeteilt wird. Anschließend wird das andere Array in zwei Teile aufgeteilt: Der erste Teil enthält die Werte, die kleiner als der Mittelpunkt des ersten Arrays sind, während der zweite Teil alle Werte beinhaltet, die gleich groß oder größer als der Mittelpunkt sind. Die Unterroutine [[Binäre Suche|''binary-search'']] gibt den Index in <math>B</math> zurück, wo <math>A[r]</math> wäre, wenn es in <math>B</math> eingefügt wäre; dies ist immer eine Zahl zwischen <math>k</math> und <math>l</math>. Abschließend wird jede Hälfte rekursiv gemischt. Da die rekursiven Aufrufe unabhängig voneinander sind, können diese parallel ausgeführt werden. Ein Hybridansatz, bei dem ein sequentieller Algorithmus für den Rekursionsbasisfall benutzt wird, funktioniert gut in der Praxis<ref>{{Literatur |Autor=Victor J. Duvanenko |Titel=Parallel Merge |Hrsg= |Sammelwerk=Dr. Dobb's Journal |Band= |Nummer= |Auflage= |Verlag= |Ort= |Datum=2011 |ISBN= |Seiten= |Online= |Abruf=}}</ref>.<br />
<br />
Die [[:en:Analysis_of_parallel_algorithms#Overview|Arbeit]] für das Mischen von zwei Arrays mit insgesamt <math>n</math> Elementen beträgt <math>\mathcal O(n)</math>. Dies wäre die Laufzeit für eine sequentielle Ausführung des Algorithmus, welche optimal ist, da mindestens <math>n</math> Elemente in das Array <math>C</math> kopiert werden. Um den [[:en:Analysis_of_parallel_algorithms#Overview|Span]] des Algorithmus zu berechnen, ist es notwendig eine [[Differenzengleichung|Rekurrenzrelation]] aufzustellen und zu lösen. Da die zwei rekursiven Aufrufe von ''merge'' parallelisiert sind muss nur der teurere der beiden Aufrufe betrachtet werden. Im schlimmsten Fall beträgt die maximale Anzahl an Elementen in einem Aufruf <math display="inline">\frac 3 4 n</math>, da das größere Array perfekt in zwei Hälften aufgeteilt wird. Werden nun die <math>\Theta\left( \log(n)\right)</math> Kosten für die binäre Suche hinzugefügt, erhalten wir folgende Rekurrenz:<br />
<br />
<math>T_{\infty}^\text{merge}(n) = T_{\infty}^\text{merge}\left(\frac {3} {4} n\right) + \Theta\left( \log(n)\right)</math>.<br />
<br />
Die Lösung ist <math>T_{\infty}^\text{merge}(n) = \Theta\left(\log(n)^2\right)</math>. Dies ist also die Laufzeit auf einer idealen Maschine mit einer unbeschränkten Anzahl an Prozessoren''<ref name=":2" />''<sup>:801-802</sup>.<br />
<br />
'''Achtung:''' Diese parallele Mischroutine ist nicht [[:en:Sorting_algorithm#Stability|stabil]]. Wenn gleiche Elemente beim Aufteilen von <math>A</math> und <math>B</math> separiert werden, verschränken sie sich in <math>C</math>, außerdem wird das Tauschen von <math>A</math> und <math>B</math> die Ordnung zerstören, falls gleiche Items über beide Inputarrays verteilt sind. <br />
== Einzelnachweise ==<br />
<references /><br />
<br />
== Siehe auch ==<br />
* [[Donald Knuth]]. ''[[The Art of Computer Programming]]'', Volume 3: ''Sorting and Searching'', Third Edition. Addison-Wesley, 1997. ISBN 0-201-89685-0. Seiten&nbsp;158–160 con Abschnitt 5.2.4: Sorting by Merging. Abschnitt 5.3.2: Minimum-Comparison Merging, Seiten&nbsp;197–207.<br />
* [[Mergesort]]<br />
<br />
[[Kategorie:Sortieralgorithmus]]</div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Merge_Algorithmen&diff=198466900Benutzer:ParAlgMergeSort/sandbox/Merge Algorithmen2020-04-04T13:19:15Z<p>ParAlgMergeSort: /* Paralleles Mischen */</p>
<hr />
<div>== Paralleles Mischen ==<br />
Eine parallele Version des binären Mischens dient als Baustein für einen [[Mergesort#Paralleler Mergesort|parallelen Mergesor]]<nowiki/>t Algorithmus. Der folgende Pseudocode demonstriert einen parallelen [[Teile-und-herrsche-Verfahren|Teile-und-Herrsche]] Mischalgorithmus (adaptiert von ''Cormen et al.<ref name=":2">{{Literatur |Autor=Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford |Titel=Introduction to algorithms |Hrsg= |Auflage=Third edition |Verlag=MIT Press |Ort=Cambridge, Mass. |Datum=2009 |ISBN=978-0-262-27083-0 |Seiten=}}</ref>''<sup>:800</sup>). Er operiert auf zwei sortierten Arrays <math>A</math> und <math>B</math> und schreibt den sortierten Output in das Array <math>C</math>. Die Notation <math>A[i...j]</math> bezeichnet den Teil von <math>A</math> von Index <math>i</math> bis exklusive <math>j</math>.<br />
<br />
'''algorithm''' merge(A[i...j], B[k...ℓ], C[p...q]) '''is'''<br />
'''inputs''' A, B, C : array<br />
i, j, k, ℓ, p, q : indices<br />
<br />
'''let''' m = j - i,<br />
n = ℓ - k<br />
<br />
'''if''' m < n '''then'''<br />
swap A and B ''// ensure that A is the larger array: i, j still belong to A; k, ℓ to B''<br />
swap m and n<br />
<br />
'''if''' m ≤ 0 '''then'''<br />
'''return''' ''// base case, nothing to merge''<br />
<br />
'''let''' r = ⌊(i + j)/2⌋<br />
'''let''' s = binary-search(A[r], B[k...ℓ])<br />
'''let''' t = p + (r - i) + (s - k)<br />
C[t] = A[r]<br />
<br />
'''in parallel do'''<br />
merge(A[i...r], B[k...s], C[p...t])<br />
merge(A[r+1...j], B[s...ℓ], C[t+1...q])<br />
<br />
Der Algorithmus beginnt indem entweder <math>A</math> oder <math>B</math> (abhängig welches Array mehr Elemente enthält) in in zwei Hälften aufgeteilt wird. Anschließend wird das andere Array in zwei Teile aufgeteilt: Der erste Teil enthält die Werte, die kleiner als der Mittelpunkt des ersten Arrays sind, während der zweite Teil alle Werte beinhaltet, die gleich groß oder größer als der Mittelpunkt sind. Die Unterroutine [[Binäre Suche|''binary-search'']] gibt den Index in <math>B</math> zurück, wo <math>A[r]</math> wäre, wenn es in <math>B</math> eingefügt wäre; dies ist immer eine Zahl zwischen <math>k</math> und <math>l</math>. Abschließend wird jede Hälfte rekursiv gemischt. Da die rekursiven Aufrufe unabhängig voneinander sind, können diese parallel ausgeführt werden. Ein Hybridansatz, bei dem ein sequentieller Algorithmus für den Rekursionsbasisfall benutzt wird, funktioniert gut in der Praxis<ref>{{Literatur |Autor=Victor J. Duvanenko |Titel=Parallel Merge |Hrsg= |Sammelwerk=Dr. Dobb's Journal |Band= |Nummer= |Auflage= |Verlag= |Ort= |Datum=2011 |ISBN= |Seiten= |Online= |Abruf=}}</ref>.<br />
<br />
Die [[:en:Analysis_of_parallel_algorithms#Overview|Arbeit]] für das Mischen von zwei Arrays mit insgesamt <math>n</math> Elementen beträgt <math>\mathcal O(n)</math>. Dies wäre die Laufzeit für eine sequentielle Ausführung des Algorithmus, welche optimal ist, da mindestens <math>n</math> Elemente in das Array <math>C</math> kopiert werden. Um den [[:en:Analysis_of_parallel_algorithms#Overview|Span]] des Algorithmus zu berechnen, ist es notwendig eine [[Differenzengleichung|Rekurrenzrelation]] aufzustellen und zu lösen. Da die zwei rekursiven Aufrufe von ''merge'' parallelisiert sind muss nur der teurere der beiden Aufrufe betrachtet werden. Im schlimmsten Fall beträgt die maximale Anzahl an Elementen in einem Aufruf <math display="inline">\frac 3 4 n</math>, da das größere Array perfekt in zwei Hälften aufgeteilt wird. Werden nun die <math>\Theta\left( \log(n)\right)</math> Kosten für die binäre Suche hinzugefügt, erhalten wir folgende Rekurrenz:<br />
<br />
<math>T_{\infty}^\text{merge}(n) = T_{\infty}^\text{merge}\left(\frac {3} {4} n\right) + \Theta\left( \log(n)\right)</math>.<br />
<br />
Die Lösung ist <math>T_{\infty}^\text{merge}(n) = \Theta\left(\log(n)^2\right)</math>. Dies ist also die Laufzeit auf einer idealen Maschine mit einer unbeschränkten Anzahl an Prozessoren''<ref name=":2" />''<sup>:801-802</sup>.<br />
<br />
'''Achtung:''' Diese parallele Mischroutine ist nicht [[:en:Sorting_algorithm#Stability|stabil]]. Wenn gleiche Elemente beim Aufteilen von <math>A</math> und <math>B</math> separiert werden, verschränken sie sich in <math>C</math>, außerdem wird das Tauschen von <math>A</math> und <math>B</math> die Ordnung zerstören, falls gleiche Items über beide Inputarrays verteilt sind. <br />
<br />
<br /></div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Merge_Algorithmen&diff=198466827Benutzer:ParAlgMergeSort/sandbox/Merge Algorithmen2020-04-04T13:16:43Z<p>ParAlgMergeSort: /* Paralleles Mischen */</p>
<hr />
<div>== Paralleles Mischen ==<br />
Eine parallele Version des binären Mischens dient als Baustein für einen [[Mergesort#Paralleler Mergesort|parallelen Mergesor]]<nowiki/>t Algorithmus. Der folgende Pseudocode demonstriert einen parallelen [[Teile-und-herrsche-Verfahren|Teile-und-Herrsche]] Mischalgorithmus (adaptiert von ''Cormen et al.<ref name=":2">{{Literatur |Autor=Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford |Titel=Introduction to algorithms |Hrsg= |Auflage=Third edition |Verlag=MIT Press |Ort=Cambridge, Mass. |Datum=2009 |ISBN=978-0-262-27083-0 |Seiten=}}</ref>''<sup>:800</sup>'')''. Er operiert auf zwei sortierten Arrays <math>A</math> und <math>B</math> und schreibt den sortierten Output in das Array <math>C</math>. Die Notation <math>A[i...j]</math> bezeichnet den Teil von <math>A</math> von Index <math>i</math> bis exklusiv <math>j</math>.<br />
<br />
'''algorithm''' merge(A[i...j], B[k...ℓ], C[p...q]) '''is'''<br />
'''inputs''' A, B, C : array<br />
i, j, k, ℓ, p, q : indices<br />
<br />
'''let''' m = j - i,<br />
n = ℓ - k<br />
<br />
'''if''' m < n '''then'''<br />
swap A and B ''// ensure that A is the larger array: i, j still belong to A; k, ℓ to B''<br />
swap m and n<br />
<br />
'''if''' m ≤ 0 '''then'''<br />
'''return''' ''// base case, nothing to merge''<br />
<br />
'''let''' r = ⌊(i + j)/2⌋<br />
'''let''' s = binary-search(A[r], B[k...ℓ])<br />
'''let''' t = p + (r - i) + (s - k)<br />
C[t] = A[r]<br />
<br />
'''in parallel do'''<br />
merge(A[i...r], B[k...s], C[p...t])<br />
merge(A[r+1...j], B[s...ℓ], C[t+1...q])<br />
<br />
Der Algorithmus beginnt indem entweder <math>A</math> oder <math>B</math> (abhängig welches Array mehr Elemente enthält) in in zwei Hälften aufgeteilt wird. Anschließend wird das andere Array in zwei Teile aufgeteilt: Der erste Teil enthält die Werte, die kleiner als der Mittelpunkt des ersten Arrays sind, während der zweite Teil alle Werte beinhaltet, die gleich groß oder größer als der Mittelpunkt sind. Die Unterroutine [[Binäre Suche|''binary-search'']] gibt den Index in <math>B</math> zurück, wo <math>A[r]</math> wäre, wenn es in <math>B</math> eingefügt wäre; dies ist immer eine Zahl zwischen <math>k</math> und <math>l</math>. Abschließend wird jede Hälfte rekursiv gemischt. Da die rekursiven Aufrufe unabhängig voneinander sind, können diese parallel ausgeführt werden. Ein Hybridansatz, bei dem ein sequentieller Algorithmus für den Rekursionsbasisfall benutzt wird, funktioniert gut in der Praxis<ref>{{Literatur |Autor=Victor J. Duvanenko |Titel=Parallel Merge |Hrsg= |Sammelwerk=Dr. Dobb's Journal |Band= |Nummer= |Auflage= |Verlag= |Ort= |Datum=2011 |ISBN= |Seiten= |Online= |Abruf=}}</ref>.<br />
<br />
Die [[:en:Analysis_of_parallel_algorithms#Overview|Arbeit]] für das Mischen von zwei Arrays mit insgesamt <math>n</math> Elementen beträgt <math>\mathcal O(n)</math>. Dies wäre die Laufzeit für eine sequentielle Ausführung des Algorithmus, welche optimal ist, da mindestens <math>n</math> Elemente in das Array <math>C</math> kopiert werden. Um den [[:en:Analysis_of_parallel_algorithms#Overview|Span]] des Algorithmus zu berechnen, ist es notwendig eine [[Differenzengleichung|Rekurrenzrelation]] aufzustellen und zu lösen. Da die zwei rekursiven Aufrufe von ''merge'' parallelisiert sind muss nur der teurere der beiden Aufrufe betrachtet werden. Im schlimmsten Fall beträgt die maximale Anzahl an Elementen in einem Aufruf <math display="inline">\frac 3 4 n</math>, da das größere Array perfekt in zwei Hälften aufgeteilt wird. Werden nun die <math>\Theta\left( \log(n)\right)</math> Kosten für die binäre Suche hinzugefügt, erhalten wir folgende Rekurrenz:<br />
<br />
<math>T_{\infty}^\text{merge}(n) = T_{\infty}^\text{merge}\left(\frac {3} {4} n\right) + \Theta\left( \log(n)\right)</math>.<br />
<br />
Die Lösung ist <math>T_{\infty}^\text{merge}(n) = \Theta\left(\log(n)^2\right)</math> - dies ist also die Laufzeit auf einer idealen Maschine mit einer unbeschränkten Anzahl an Prozessoren''<ref name=":2" />''<sup>:801-802</sup>.<br />
<br />
'''Achtung:''' Diese parallele Mischroutine ist nicht [[:en:Sorting_algorithm#Stability|stabil]]. Wenn gleiche Elemente beim Aufteilen von <math>A</math> und <math>B</math> separiert werden, verschränken sie sich in <math>C</math>, außerdem wird das Tauschen von <math>A</math> und <math>B</math> die Ordnung zerstören, falls gleiche Items über beide Inputarrays verteilt sind. <br />
<br />
<br /></div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Merge_Algorithmen&diff=198464200Benutzer:ParAlgMergeSort/sandbox/Merge Algorithmen2020-04-04T13:06:37Z<p>ParAlgMergeSort: /* Paralleles Mischen */</p>
<hr />
<div>== Paralleles Mischen ==<br />
Eine parallele Version des binären Mischens dient als Baustein für einen [[Mergesort#Paralleler Mergesort|parallelen Mergesor]]<nowiki/>t Algorithmus. Der folgende Pseudocode demonstriert einen parallelen [[Teile-und-herrsche-Verfahren|Teile-und-Herrsche]] Mischalgorithmus (adaptiert von ''Cormen et al.<ref name="clrs">{{Introduction to Algorithms|3}}</ref>''<sup>:800</sup>'')''. Er operiert auf zwei sortierten Arrays <math>A</math> und <math>B</math> und schreibt den sortierten Output in das Array <math>C</math>. Die Notation <math>A[i...j]</math> bezeichnet den Teil von <math>A</math> von Index <math>i</math> bis exklusiv <math>j</math>.<br />
<br />
'''algorithm''' merge(A[i...j], B[k...ℓ], C[p...q]) '''is'''<br />
'''inputs''' A, B, C : array<br />
i, j, k, ℓ, p, q : indices<br />
<br />
'''let''' m = j - i,<br />
n = ℓ - k<br />
<br />
'''if''' m < n '''then'''<br />
swap A and B ''// ensure that A is the larger array: i, j still belong to A; k, ℓ to B''<br />
swap m and n<br />
<br />
'''if''' m ≤ 0 '''then'''<br />
'''return''' ''// base case, nothing to merge''<br />
<br />
'''let''' r = ⌊(i + j)/2⌋<br />
'''let''' s = binary-search(A[r], B[k...ℓ])<br />
'''let''' t = p + (r - i) + (s - k)<br />
C[t] = A[r]<br />
<br />
'''in parallel do'''<br />
merge(A[i...r], B[k...s], C[p...t])<br />
merge(A[r+1...j], B[s...ℓ], C[t+1...q])<br />
<br />
Der Algorithmus beginnt indem entweder <math>A</math> oder <math>B</math> (abhängig welches Array mehr Elemente enthält) in in zwei Hälften aufgeteilt wird. Anschließend wird das andere Array in zwei Teile aufgeteilt: Der erste Teil enthält die Werte, die kleiner als der Mittelpunkt des ersten Arrays sind, während der zweite Teil alle Werte beinhaltet, die gleich groß oder größer als der Mittelpunkt sind. Die Unterroutine [[Binäre Suche|''binary-search'']] gibt den Index in <math>B</math> zurück, wo <math>A[r]</math> wäre, wenn es in <math>B</math> eingefügt wäre; dies ist immer eine Zahl zwischen <math>k</math> und <math>l</math>. Abschließend wird jede Hälfte rekursiv gemischt. Da die rekursiven Aufrufe unabhängig voneinander sind, können diese parallel ausgeführt werden. Ein Hybridansatz, bei dem ein sequentieller Algorithmus für den Rekursionsbasisfall benutzt wird, funktioniert gut in der Praxis.<br />
<br />
Die [[:en:Analysis_of_parallel_algorithms#Overview|Arbeit]] für das Mischen von zwei Arrays mit insgesamt <math>n</math> Elementen beträgt <math>\mathcal O(n)</math>. Dies wäre die Laufzeit für eine sequentielle Ausführung des Algorithmus, welche optimal ist, da mindestens <math>n</math> Elemente in das Array <math>C</math> kopiert werden. Um den [[:en:Analysis_of_parallel_algorithms#Overview|Span]] des Algorithmus zu berechnen, ist es notwendig eine [[Differenzengleichung|Rekurrenzrelation]] aufzustellen und zu lösen. Da die zwei rekursiven Aufrufe von ''merge'' parallelisiert sind muss nur der teurere der beiden Aufrufe betrachtet werden. Im schlimmsten Fall beträgt die maximale Anzahl an Elementen in einem Aufruf <math display="inline">\frac 3 4 n</math>, da das größere Array perfekt in zwei Hälften aufgeteilt wird. Werden nun die <math>\Theta\left( \log(n)\right)</math> Kosten für die binäre Suche hinzugefügt, erhalten wir folgende Rekurrenz:<br />
<br />
<math>T_{\infty}^\text{merge}(n) = T_{\infty}^\text{merge}\left(\frac {3} {4} n\right) + \Theta\left( \log(n)\right)</math>.<br />
<br />
Die Lösung ist <math>T_{\infty}^\text{merge}(n) = \Theta\left(\log(n)^2\right)</math> - dies ist also die Laufzeit auf einer idealen Maschine mit einer unbeschränkten Anzahl an Prozessoren.<br />
<br />
'''Achtung:''' Diese parallele Mischroutine ist nicht [[:en:Sorting_algorithm#Stability|stabil]]. Wenn gleiche Elemente beim Aufteilen von <math>A</math> und <math>B</math> separiert werden, verschränken sie sich in <math>C</math>, außerdem wird das Tauschen von <math>A</math> und <math>B</math> die Ordnung zerstören, falls gleiche Items über beide Inputarrays verteilt sind. <br />
<br />
<br /></div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Merge_Algorithmen&diff=198462054Benutzer:ParAlgMergeSort/sandbox/Merge Algorithmen2020-04-04T12:48:07Z<p>ParAlgMergeSort: /* Paralleles Mischen */</p>
<hr />
<div>== Paralleles Mischen ==<br />
Eine parallele Version des binären Mischens dient als Baustein für einen [[Mergesort#Paralleler Mergesort|parallelen Mergesor]]<nowiki/>t Algorithmus. Der folgende Pseudocode demonstriert einen parallelen [[Teile-und-herrsche-Verfahren|Teile-und-Herrsche]] Mischalgorithmus (adaptiert von ''Cormen et al.)''. Er operiert auf zwei sortierten Arrays <math>A</math> und <math>B</math> und schreibt den sortierten Output in das Array <math>C</math>. Die Notation <math>A[i...j]</math> bezeichnet den Teil von <math>A</math> von Index <math>i</math> bis exklusiv <math>j</math>.<br />
<br />
'''algorithm''' merge(A[i...j], B[k...ℓ], C[p...q]) '''is'''<br />
'''inputs''' A, B, C : array<br />
i, j, k, ℓ, p, q : indices<br />
<br />
'''let''' m = j - i,<br />
n = ℓ - k<br />
<br />
'''if''' m < n '''then'''<br />
swap A and B ''// ensure that A is the larger array: i, j still belong to A; k, ℓ to B''<br />
swap m and n<br />
<br />
'''if''' m ≤ 0 '''then'''<br />
'''return''' ''// base case, nothing to merge''<br />
<br />
'''let''' r = ⌊(i + j)/2⌋<br />
'''let''' s = binary-search(A[r], B[k...ℓ])<br />
'''let''' t = p + (r - i) + (s - k)<br />
C[t] = A[r]<br />
<br />
'''in parallel do'''<br />
merge(A[i...r], B[k...s], C[p...t])<br />
merge(A[r+1...j], B[s...ℓ], C[t+1...q])<br />
<br />
Der Algorithmus beginnt indem entweder <math>A</math> oder <math>B</math> (abhängig welches Array mehr Elemente enthält) in in zwei Hälften aufgeteilt wird. Anschließend wird das andere Array in zwei Teile aufgeteilt: Der erste Teil enthält die Werte, die kleiner als der Mittelpunkt des ersten Arrays sind, während der zweite Teil alle Werte beinhaltet, die gleich groß oder größer als der Mittelpunkt sind. Die Unterroutine [[Binäre Suche|''binary-search'']] gibt den Index in <math>B</math> zurück, wo <math>A[r]</math> wäre, wenn es in <math>B</math> eingefügt wäre; dies ist immer eine Zahl zwischen <math>k</math> und <math>l</math>. Abschließend wird jede Hälfte rekursiv gemischt. Da die rekursiven Aufrufe unabhängig voneinander sind, können diese parallel ausgeführt werden. Ein Hybridansatz, bei dem ein sequentieller Algorithmus für den Rekursionsbasisfall benutzt wird, funktioniert gut in der Praxis.<br />
<br />
Die [[:en:Analysis_of_parallel_algorithms#Overview|Arbeit]] für das Mischen von zwei Arrays mit insgesamt <math>n</math> Elementen beträgt <math>\mathcal O(n)</math>. Dies wäre die Laufzeit für eine sequentielle Ausführung des Algorithmus, welche optimal ist, da mindestens <math>n</math> Elemente in das Array <math>C</math> kopiert werden. Um den [[:en:Analysis_of_parallel_algorithms#Overview|Span]] des Algorithmus zu berechnen, ist es notwendig eine [[Differenzengleichung|Rekurrenzrelation]] aufzustellen und zu lösen. Da die zwei rekursiven Aufrufe von ''merge'' parallelisiert sind muss nur der teurere der beiden Aufrufe betrachtet werden. Im schlimmsten Fall beträgt die maximale Anzahl an Elementen in einem Aufruf <math display="inline">\frac 3 4 n</math>, da das größere Array perfekt in zwei Hälften aufgeteilt wird. Werden nun die <math>\Theta\left( \log(n)\right)</math> Kosten für die binäre Suche hinzugefügt, erhalten wir folgende Rekurrenz:<br />
<br />
<math>T_{\infty}^\text{merge}(n) = T_{\infty}^\text{merge}\left(\frac {3} {4} n\right) + \Theta\left( \log(n)\right)</math>.<br />
<br />
Die Lösung ist <math>T_{\infty}^\text{merge}(n) = \Theta\left(\log(n)^2\right)</math> - dies ist also die Laufzeit auf einer idealen Maschine mit einer unbeschränkten Anzahl an Prozessoren.<br />
<br />
'''Achtung:''' Diese parallele Mischroutine ist nicht stabil. Wenn gleiche Elemente beim Aufteilen von <math>A</math> und <math>B</math> separiert werden<br />
<br />
<br /></div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Merge_Algorithmen&diff=198461066Benutzer:ParAlgMergeSort/sandbox/Merge Algorithmen2020-04-04T12:16:53Z<p>ParAlgMergeSort: /* Paralleles Mischen */</p>
<hr />
<div>== Paralleles Mischen ==<br />
Eine parallele Version des binären Mischens dient als Baustein für einen [[Mergesort#Paralleler Mergesort|parallelen Mergesor]]<nowiki/>t Algorithmus. Der folgende Pseudocode demonstriert einen parallelen [[Teile-und-herrsche-Verfahren|Teile-und-Herrsche]] Mischalgorithmus (adaptiert von ''Cormen et al.)''. Er operiert auf zwei sortierten Arrays <math>A</math> und <math>B</math> und schreibt den sortierten Output in das Array <math>C</math>. Die Notation <math>A[i...j]</math> bezeichnet den Teil von <math>A</math> von Index <math>i</math> bis exklusiv <math>j</math>.<br />
<br />
'''algorithm''' merge(A[i...j], B[k...ℓ], C[p...q]) '''is'''<br />
'''inputs''' A, B, C : array<br />
i, j, k, ℓ, p, q : indices<br />
<br />
'''let''' m = j - i,<br />
n = ℓ - k<br />
<br />
'''if''' m < n '''then'''<br />
swap A and B ''// ensure that A is the larger array: i, j still belong to A; k, ℓ to B''<br />
swap m and n<br />
<br />
'''if''' m ≤ 0 '''then'''<br />
'''return''' ''// base case, nothing to merge''<br />
<br />
'''let''' r = ⌊(i + j)/2⌋<br />
'''let''' s = binary-search(A[r], B[k...ℓ])<br />
'''let''' t = p + (r - i) + (s - k)<br />
C[t] = A[r]<br />
<br />
'''in parallel do'''<br />
merge(A[i...r], B[k...s], C[p...t])<br />
merge(A[r+1...j], B[s...ℓ], C[t+1...q])<br />
<br />
Der Algorithmus beginnt, indem entweder <math>A</math> oder <math>B</math> (abhängig welches Array mehr Elemente enthält) in in zwei Hälften aufgeteilt wird. Anschließend wird das andere Array in zwei Teile aufgeteilt: der erste Teil enthält die Werte, die kleiner als der Mittelpunkt des ersten Arrays sind, während der zweite Teil alle Werte beinhaltet, die gleich groß oder größer als der Mittelpunkt sind. (Die [[Binäre Suche]] Unterroutine gibt den Index in <math>B</math> zurück, wo <math>A[r]</math> wäre, wenn es in <math>B</math> eingefügt wäre; dies ist immer eine Zahl zwischen <math>k</math> und <math>l</math>.) Abschließend wird jede Hälfte rekursiv gemischt. Da die rekursiven Aufrufe unabhängig voneinander sind, können diese parallel ausgeführt werden. Ein Hybridansatz, bei dem ein sequentieller Algorithmus für den Rekursionsbasisfall benutzt wird, funktioniert gut in der Praxis. Die [[:en:Analysis_of_parallel_algorithms#Overview|Arbeit]], die vom Algorithmus verrichtet wird, um zwei Elemente mit insgesamt <math>n</math> Elemente zu mischen, beträgt <math>\mathcal O(n)</math>. (Dies wäre die Laufzeit für eine sequentielle Ausführung des Algorithmus.) Dies ist optimal, da mindestens <math>n</math> Elemente in das Array <math>C</math> kopiert werden. Um den [[:en:Analysis_of_parallel_algorithms#Overview|Span]]<br />
<br /></div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Merge_Algorithmen&diff=198460257Benutzer:ParAlgMergeSort/sandbox/Merge Algorithmen2020-04-04T11:50:08Z<p>ParAlgMergeSort: AZ: Die Seite wurde neu angelegt: == Paralleles Mischen == Eine parallele Version des binären Mischens dient als Baustein für einen …</p>
<hr />
<div>== Paralleles Mischen ==<br />
Eine parallele Version des binären Mischens dient als Baustein für einen [[Mergesort#Paralleler Mergesort|parallelen Mergesor]]<nowiki/>t Algorithmus.</div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Mergesort&diff=197541632Mergesort2020-03-07T15:49:46Z<p>ParAlgMergeSort: /* Praktische Anpassung und Anwendung */ Doppelte Referenz entfernt</p>
<hr />
<div>[[Datei:Merge sort animation2.gif|miniatur|300px|Beispiel, wie Mergesort eine Liste sortiert. Die Listenelemente werden durch Punkte dargestellt. Die waagerechte Achse gibt an, wo sich ein Element in der Liste befindet, die senkrechte Achse gibt an, wie groß ein Element ist.]]<br />
<br />
'''Mergesort''' (von {{enS|''merge''}} ‚verschmelzen‘ und {{lang|en|''sort''}} ‚sortieren‘) ist ein [[Stabiles Sortierverfahren|stabiler]] [[Sortierverfahren|Sortieralgorithmus]], der nach dem Prinzip ''[[Teile und herrsche (Informatik)|teile und herrsche]]'' (divide and conquer) arbeitet. Er wurde erstmals [[1945]] durch [[John von Neumann]] vorgestellt.<ref>{{Literatur | Autor=Donald E. Knuth | Titel=The Art of Computer Programming. Volume 3: Sorting and Searching. | Auflage=2 | Verlag=Addison-Wesley | Ort= | Jahr=1998 | Seiten=158 }}</ref><br />
<br />
== Funktionsweise ==<br />
Mergesort betrachtet die zu sortierenden Daten als Liste und zerlegt sie in kleinere Listen, die jede für sich sortiert werden. Die sortierten kleinen Listen werden dann im Reißverschlussverfahren zu größeren Listen zusammengefügt (engl. {{lang|en|''(to) merge''}}), bis wieder eine sortierte Gesamtliste erreicht ist. Das Verfahren arbeitet bei Arrays in der Regel nicht [[in-place]], es sind dafür aber (trickreiche) Implementierungen bekannt, in welchen die Teil-Arrays üblicherweise rekursiv zusammengeführt werden.<ref>{{Internetquelle|url=https://github.com/h2database/h2database/blob/2cf822269945e25973aa6f1d412f47f7254ce383/h2/src/tools/org/h2/dev/sort/InPlaceStableMergeSort.java|titel=h2database/h2database|werk=GitHub|zugriff=2016-09-01}}</ref> [[Liste (Datenstruktur)|Verkettete Listen]] sind besonders geeignet zur Implementierung von Mergesort, dabei ergibt sich die in-place-Sortierung fast von selbst.<br />
<br />
=== Veranschaulichung der Funktionsweise ===<br />
[[Datei:Mergesort.png|mini|Funktionsweise]]<br />
<br />
Das Bild veranschaulicht die drei wesentlichen Schritte eines [[Teile und herrsche (Informatik)|Teile-und-herrsche]]-Verfahrens, wie sie im Rahmen von Mergesort umgesetzt werden. Der Teile-Schritt ist ersichtlich trivial (die Daten werden einfach in zwei Hälften aufgeteilt). Die wesentliche Arbeit wird beim Verschmelzen (merge) geleistet – daher rührt auch der Name des Algorithmus. Bei [[Quicksort]] ist hingegen der Teile-Schritt aufwendig und der Merge-Schritt einfacher (nämlich eine [[Konkatenation (Listen)|Konkatenierung]]).<br />
<br />
Bei der Betrachtung des in der Grafik dargestellten Verfahrens sollte man sich allerdings bewusst machen, dass es sich hier nur um eine von mehreren [[Rekursion]]sebenen handelt. So könnte etwa die Sortierfunktion, welche die beiden Teile 1 und 2 sortieren soll, zu dem Ergebnis kommen, dass diese Teile immer noch zu groß für die Sortierung sind. Beide Teile würden dann wiederum aufgeteilt und der Sortierfunktion rekursiv übergeben, so dass eine weitere Rekursionsebene geöffnet wird, welche dieselben Schritte abarbeitet. Im Extremfall (der bei Mergesort sogar der Regelfall ist) wird das Aufteilen so weit fortgesetzt, bis die beiden Teile nur noch aus einzelnen Datenelementen bestehen.<br />
<br />
== Implementierung ==<br />
Der folgende [[Pseudocode]] illustriert die Arbeitsweise des [[Algorithmus]], wobei ''liste'' die zu sortierenden Elemente enthält.<br />
<br />
funktion mergesort(liste);<br />
falls (Größe von liste <= 1) dann antworte liste<br />
sonst<br />
halbiere die liste in linkeListe, rechteListe<br />
linkeListe = mergesort(linkeListe)<br />
rechteListe = mergesort(rechteListe)<br />
antworte merge(linkeListe, rechteListe)<br />
<br />
funktion merge(linkeListe, rechteListe);<br />
neueListe<br />
solange (linkeListe und rechteListe nicht leer)<br />
| falls (erstes Element der linkeListe <= erstes Element der rechteListe)<br />
| dann füge erstes Element linkeListe in die neueListe hinten ein und entferne es aus linkeListe<br />
| sonst füge erstes Element rechteListe in die neueListe hinten ein und entferne es aus rechteListe<br />
solange_ende<br />
solange (linkeListe nicht leer)<br />
| füge erstes Element linkeListe in die neueListe hinten ein und entferne es aus linkeListe<br />
solange_ende<br />
solange (rechteListe nicht leer)<br />
| füge erstes Element rechteListe in die neueListe hinten ein und entferne es aus rechteListe<br />
solange_ende<br />
antworte neueListe<br />
<br />
== Beispiel ==<br />
[[Datei:Mergesort example.png|269px|links]]<br />
<br />
Im letzten Verschmelzungsschritt ist das Reißverschlussverfahren beim Verschmelzen (in der Abb. „Mischen:“) angedeutet. Blaue Pfeile verdeutlichen den Aufteilungsschritt, grüne Pfeile die Verschmelzungsschritte.<br />
<br />
Es folgt ein Beispielcode analog zum obigen Abschnitt "Implementierung" für den rekursiven Sortieralgorithmus. Er teilt rekursiv absteigend die Eingabe in 2 kleinere Listen, bis diese trivialerweise sortiert sind, und verschmilzt sie auf dem rekursiven Rückweg, wodurch sie sortiert werden.<br />
'''function''' merge_sort(list ''x'')<br />
<br />
'''if''' length(''x'') ≤ 1 '''then'''<br />
'''return''' ''x'' // Kurzes ''x'' ist trivialerweise sortiert.<br />
<br />
'''var''' ''l'' := empty list<br />
'''var''' ''r'' := empty list<br />
'''var''' ''i'', ''nx'' := length(''x'')−1<br />
// Teile ''x'' in die zwei Hälften ''l'' und ''r'' ...<br />
'''for''' ''i'' := 0 '''to''' floor(''nx''/2) '''do'''<br />
append ''x''[''i''] to ''l''<br />
'''for''' ''i'' := floor(''nx''/2)+1 '''to''' ''nx'' '''do'''<br />
append ''x''[''i''] to ''r''<br />
// ... und sortiere beide (einzeln).<br />
''l'' := merge_sort(''l'')<br />
''r'' := merge_sort(''r'')<br />
// Verschmelze die sortierten Hälften.<br />
'''return''' merge(''l'', ''r'')<br />
Beispielcode zum Verschmelzen zweier sortierter Listen.<br />
'''function''' merge(list ''l'', list ''r'')<br />
'''var''' ''y'' := empty list // Ergebnisliste<br />
<br />
'''var''' ''nl'' := length(''l'')−1<br />
'''var''' ''nr'' := length(''r'')−1<br />
'''var''' ''i'', ''il'' := 0<br />
'''for''' ''i'' := 0 '''to''' ''nl''+''nr+1'' '''do'''<br />
'''if''' ''il'' > ''nl'' '''then'''<br />
append ''r''[''i''−''il''] to ''y''<br />
'''continue'''<br />
'''if''' ''il'' < ''i''−''nr'' '''then'''<br />
append ''l''[''il''] to ''y''<br />
''il'' := ''il''+1<br />
'''continue'''<br />
// Jetzt ist 0 ≤ ''il'' ≤ ''nl'' und 0 ≤ ''i''−''il'' ≤ ''nr''.<br />
'''if''' ''l''[''il''] ≤ ''r''[''i''−''il''] '''then'''<br />
append ''l''[''il''] to ''y''<br />
''il'' := ''il''+1<br />
'''else'''<br />
append ''r''[''i''−''il''] to ''y''<br />
<br />
'''return''' ''y''<br />
<br />
===C++ 11===<br />
Eine Implementierung in der Programmiersprache C++ unter Verwendung von [[Zeigerarithmetik]] könnte folgendermaßen aussehen:<br />
<br />
<syntaxhighlight lang="cpp"><br />
#ifndef ALGORITHM_H<br />
#define ALGORITHM_H<br />
<br />
#include <functional><br />
<br />
namespace ExampleNamespace<br />
{<br />
class Algorithm<br />
{<br />
public:<br />
Algorithm() = delete;<br />
template<typename T><br />
static auto ptrDiff(T const * const begin, T const * const end)<br />
{<br />
return end - begin;<br />
}<br />
template<typename T><br />
static auto midPtr(T* const begin, long long const & ptr_diff)<br />
{<br />
return begin + ptr_diff / 2u;<br />
}<br />
template<typename T><br />
static auto midPtr(T* const begin, T* const end)<br />
{<br />
return midPtr(begin, ptrDiff(begin, end));<br />
}<br />
static auto continueSplit(long long const & ptr_diff)<br />
{<br />
return ptr_diff > 1u;<br />
}<br />
template<typename T><br />
static auto continueSplit(T const * const begin, T const * const end)<br />
{<br />
return continueSplit(ptrDiff(begin, end));<br />
}<br />
template<typename T><br />
static auto mergeSort(T* const begin, T* const end)<br />
{<br />
mergeSort(begin, end, std::less<T>());<br />
}<br />
template<typename T, typename Compare><br />
static auto mergeSort(T* const begin, T* const end, Compare&& comp)<br />
{<br />
auto ptr_diff = ptrDiff(begin, end);<br />
if (ptr_diff) {<br />
auto* temp = new T[ptr_diff];<br />
mergeSort(begin, end, temp, comp);<br />
delete[] temp;<br />
}<br />
}<br />
template<typename T><br />
static auto copyRange(T const * begin, T const * const end, T* dst)<br />
{<br />
copyRangeOverwrite(begin, end, dst);<br />
}<br />
template<typename T><br />
static auto copyRangeOverwrite(T const * begin, T const * const end, T*& dst)<br />
{<br />
while (begin != end) {<br />
*dst++ = *begin++;<br />
}<br />
}<br />
private:<br />
template<typename T, typename Compare><br />
static void mergeSort(T* const begin, T* const end, T* temp, Compare&& comp)<br />
{<br />
auto ptr_diff = ptrDiff(begin, end);<br />
if (continueSplit(ptr_diff)) {<br />
auto * const mid = midPtr(begin, ptr_diff);<br />
mergeSort(begin, mid, temp, comp);<br />
mergeSort(mid, end, temp, comp);<br />
merge(begin, mid, end, temp, comp);<br />
}<br />
}<br />
template<typename T, typename Compare><br />
static auto merge(T* begin, T const * const mid, T const * const end, T* temp, Compare&& comp)<br />
{<br />
copyRange(begin, end, temp);<br />
auto* right_temp = temp + ptrDiff(begin, mid);<br />
auto const * const left_temp_end = right_temp;<br />
auto const * const right_temp_end = temp + ptrDiff(begin, end);<br />
mergeResults(temp, right_temp, right_temp_end, begin, comp);<br />
copyRangeOverwrite(temp, left_temp_end, begin);<br />
copyRangeOverwrite(right_temp, right_temp_end, begin);<br />
}<br />
template<typename T, typename Compare><br />
static auto mergeResults(T*& begin, T*& mid, T const * const end, T*& dst, Compare&& comp)<br />
{<br />
auto const * const mid_ptr = mid;<br />
while (begin != mid_ptr && mid != end) {<br />
*dst++ = comp(*begin, *mid) ? *begin++ : *mid++;<br />
}<br />
}<br />
};<br />
}<br />
<br />
#endif // !ALGORITHM_H<br />
</syntaxhighlight><br />
<br />
== Komplexität ==<br />
Mergesort ist ein stabiles Sortierverfahren, vorausgesetzt der Merge-Schritt ist entsprechend implementiert. Seine [[Komplexität (Informatik)|Komplexität]] beträgt im Worst-, Best- und Average-Case in [[Landau-Symbole|Landau-Notation]] ausgedrückt stets <math> \mathcal{O}(n \cdot \log (n)) </math>.<br />
<br />
Damit ist Mergesort hinsichtlich der Komplexität Quicksort grundsätzlich überlegen, da Quicksort (ohne besondere Vorkehrungen) ein {{nowrap|[[Worst Case|Worst-Case]]-Verhalten}} von <math> \Theta(n^2) </math> besitzt. Es benötigt jedoch zusätzlichen Speicherplatz (der Größenordnung <math>\mathcal{O}(n)</math>), ist also kein In-place-Verfahren.<br />
<br />
Für die Laufzeit <math>T(n)</math> von Mergesort bei <math>n</math> zu sortierenden Elementen gilt die Rekursionsformel<br />
<br />
:<math>T(n)= \underbrace{T(\lfloor \tfrac{n}{2} \rfloor)}_{\text{Aufwand, den einen Teil zu sortieren}} <br />
+ \underbrace{T(\lceil \tfrac{n}{2} \rceil)}_{\text{Aufwand, den anderen Teil zu sortieren}} <br />
+ \underbrace{\mathcal{O}(n)}_{\text{Aufwand, die beiden Teile zu verschmelzen}} </math><br />
<br />
mit dem Rekursionsanfang <math>T(1)=1</math>.<br />
<br />
Nach dem [[Master-Theorem]] kann die Rekursionsformel durch <math>2 \, T(\lfloor \tfrac{n}{2} \rfloor) + n </math> bzw. <math>2 \, T(\lceil \tfrac{n}{2} \rceil) + n </math> approximiert werden mit jeweils der Lösung (2. Fall des Mastertheorems, s. dort) <math>T(n)=\mathcal{O}(n \cdot \log (n)) </math>.<br />
<br />
{| class="wikitable left mw-collapsible mw-collapsed font-size: 105.3%;"<br />
|style="text-align:left; font-size: 95%;"| '''Durchschnittliche und maximale Anzahl Vergleiche''' &nbsp; &nbsp; &nbsp; &nbsp; <br />
|-<br />
|<br />
Sind <math>l_0,l_1</math> die Längen der zu verschmelzenden und vorsortierten Folgen <math>F_0,F_1 ,</math> dann gilt für die Anzahl <math>M</math> der erforderlichen Vergleiche fürs sortierende Verschmelzen<br />
: <math>\min(l_0,l_1) \le M \le l_0+l_1-1 </math>,<br />
da erstens eine Folge komplett vor der anderen liegen kann, d.&nbsp;h., es ist <math>F_0[l_0] \prec F_1[1]</math> bzw. <math>F_1[l_1] \prec F_0[1] , </math> oder es ist zweitens <math>F_0[l_0-1] \prec F_1[l_1] \prec F_0[l_0]</math> (bzw. umgekehrt), sodass die Elemente bis zum letzten Element in jeder Folge verglichen werden müssen. Dabei ist jeweils angenommen, dass das Vergleichen der zwei Folgen bei den Elementen mit niedrigem Index beginnt. Mit<br />
: <math>V_m(l_0+l_1) := l_0+l_1-1</math><br />
(Subskript <math>m </math> für ''maximal'') sei die maximale Anzahl der Vergleiche fürs ''Verschmelzen'' bezeichnet.<br />
Für die maximale Anzahl <math>S_m(n)</math> an Vergleichen für einen ganzen ''Mergesort''-Lauf von <math>n</math> Elementen errechnet sich daraus<br />
: <math>S_m(n) = n l-2^l+1 ,</math> mit <math>l:=\lceil \log_2(n) \rceil .</math><br />
<math>S_m(n) </math> ist die {{OEIS|A001855}}.<br />
<br />
Für eine Gleichverteilung lässt sich auch die durchschnittliche Anzahl <math>V_\varnothing(l_0,l_1)</math> (Subskript <math>{}_\varnothing </math> für ''durchschnittlich'') der Vergleiche genau berechnen, und zwar ist für <math>l_1 = l_0</math><br />
: <math><br />
\begin{align}<br />
V_\varnothing(l_0,l_0) \; \; \; \; \; & = \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1} k\right) \, \bigg/ \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1}\right) \\<br />
& = 2 \, l_0^2/(l_0+1)<br />
\end{align}</math><br />
und für <math>l_1 = l_0-1</math><br />
: <math><br />
\begin{align}<br />
V_\varnothing(l_0,l_0-1) & = \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1} k \; + \sum_{k=l_0-1}^{2 l_0-2} \binom{k-1}{l_0-2} k\right) \\<br />
& \; \bigg/ \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1} \; \; \; + \sum_{k=l_0-1}^{2 l_0-2} \binom{k-1}{l_0-2} \; \; \right) \\<br />
& = 2 \, l_0^2/(l_0+1)-1 .<br />
\end{align}</math><br />
Dabei ist <math>k</math> die Anzahl der Vergleiche für die Anordnung<br />
: <math>\underbrace{u\,v \dotso w\,0}_{k} \, \underbrace{1\,1 \dotso 1}_{l_1+l_0-k}</math> &nbsp; ,<br />
wobei <math>0</math> für das letzte (das am höchsten sortierende) Element in der Folge <math>F_0</math> steht, die (nicht von einem Element aus <math>F_0</math> unterbrochenen) {{nowrap|<math>1</math>en}} zu <math>F_1</math> gehören und <math>u,v,w</math> (die auch fehlen können) entweder zu <math>F_0</math> oder zu <math>F_1</math> gehören. Der in den Summenformeln beigegebene [[Binomialkoeffizient]] zählt die verschiedenen Möglichkeiten für <math>u\,v \dotso w .</math><br />
<br />
Für die durchschnittliche Anzahl <math>S_\varnothing(n)</math> an Vergleichen für einen ganzen Mergesort-Lauf von <math>n</math> Elementen errechnet man daraus<br />
{| class="wikitable" style="text-align:right;"<br />
|-<br />
! <math>n</math> !! <math>S_\varnothing(n)</math> !! <math>S_m(n)</math> !! <math>(S_m(n)-</math><br><math>S_\varnothing(n))/n</math><br />
|-<br />
| <small>2</small>||<small>1,0000</small>||<small>1</small>||<small>0,00000</small><br />
|-<br />
|<small>3</small>||<small>2,6667</small>||<small>3</small>||<small>0,11111</small><br />
|-<br />
|<small>4</small>||<small>4,6667</small>||<small>5</small>||<small>0,08333</small><br />
|-<br />
|<small>6</small>||<small>9,8333</small>||<small>11</small>||<small>0,19444</small><br />
|-<br />
|<small>8</small>||<small>15,733</small>||<small>17</small>||<small>0,15833</small><br />
|-<br />
|<small>12</small>||<small>29,952</small>||<small>33</small>||<small>0,25397</small><br />
|-<br />
|<small>16</small>||<small>45,689</small>||<small>49</small>||<small>0,20694</small><br />
|-<br />
|<small>24</small>||<small>82,059</small>||<small>89</small>||<small>0,28922</small><br />
|-<br />
|<small>32</small>||<small>121,50</small>||<small>129</small>||<small>0,23452</small><br />
|-<br />
|<small>48</small>||<small>210,20</small>||<small>225</small>||<small>0,30839</small><br />
|-<br />
|<small>64</small>||<small>305,05</small>||<small>321</small>||<small>0,24920</small><br />
|-<br />
|<small>96</small>||<small>514,44</small>||<small>545</small>||<small>0,31838</small><br />
|-<br />
|<small>128</small>||<small>736,13</small>||<small>769</small>||<small>0,25677</small><br />
|-<br />
|<small>192</small>||<small>1218,9</small>||<small>1281</small>||<small>0,32348</small><br />
|-<br />
|<small>256</small>||<small>1726,3</small>||<small>1793</small>||<small>0,26061</small><br />
|-<br />
|<small>384</small>||<small>2819,8</small>||<small>2945</small>||<small>0,32606</small><br />
|-<br />
|<small>512</small>||<small>3962,6</small>||<small>4097</small>||<small>0,26255</small><br />
|-<br />
|<small>768</small>||<small>6405,6</small>||<small>6657</small>||<small>0,32736</small><br />
|-<br />
|<small>1024</small>||<small>8947,2</small>||<small>9217</small>||<small>0,26352</small><br />
|-<br />
|<small>1536</small>||<small>14345,0</small>||<small>14849</small>||<small>0,32801</small><br />
|-<br />
|<small>2048</small>||<small>19940,0</small>||<small>20481</small>||<small>0,26401</small><br />
|-<br />
|<small>3072</small>||<small>31760,0</small>||<small>32769</small>||<small>0,32833</small><br />
|-<br />
|<small>4096</small>||<small>43974,0</small>||<small>45057</small>||<small>0,26426</small><br />
|-<br />
|<small>6144</small>||<small>69662,0</small>||<small>71681</small>||<small>0,32849</small><br />
|-<br />
|<small>8192</small>||<small>96139,0</small>||<small>98305</small>||<small>0,26438</small><br />
|-<br />
|<small>12288</small>||<small>1,5161E5</small>||<small>155649</small>||<small>0,32857</small><br />
|-<br />
|<small>16384</small>||<small>2,0866E5</small>||<small>212993</small>||<small>0,26444</small><br />
|-<br />
|<small>24576</small>||<small>3,278E5</small>||<small>335873</small>||<small>0,32862</small><br />
|-<br />
|<small>32768</small>||<small>4,5009E5</small>||<small>458753</small>||<small>0,26447</small><br />
|-<br />
|<small>49152</small>||<small>7,0474E5</small>||<small>720897</small>||<small>0,32864</small><br />
|-<br />
|<small>65536</small>||<small>9,6571E5</small>||<small>983041</small>||<small>0,26448</small><br />
|-<br />
|<small>98304</small>||<small>1,5078E6</small>||<small>1540097</small>||<small>0,32865</small><br />
|-<br />
|<small>131072</small>||<small>2,0625E6</small>||<small>2097153</small>||<small>0,26449</small><br />
|-<br />
|<small>196608</small>||<small>3,2122E6</small>||<small>3276801</small>||<small>0,32865</small><br />
|-<br />
|<small>262144</small>||<small>4,3871E6</small>||<small>4456449</small>||<small>0,26450</small><br />
|-<br />
|<small>393216</small>||<small>6,8176E6</small>||<small>6946817</small>||<small>0,32865</small><br />
|-<br />
|<small>524288</small>||<small>9,2985E6</small>||<small>9437185</small>||<small>0,26450</small><br />
|-<br />
|<small>786432</small>||<small>1,4422E7</small>||<small>14680065</small>||<small>0,32865</small><br />
|-<br />
|<small>1048576</small>||<small>1,9646E7</small>||<small>19922945</small>||<small>0,26450</small><br />
|-<br />
|<small>1572864</small>||<small>3,0416E7</small>||<small>30932993</small>||<small>0,32866</small><br />
|-<br />
|<small>2097152</small>||<small>4,1388E7</small>||<small>41943041</small>||<small>0,26450</small><br />
|-<br />
|<small>3145728</small>||<small>6,3978E7</small>||<small>65011713</small>||<small>0,32866</small><br />
|-<br />
|<small>4194304</small>||<small>8,6971E7</small>||<small>88080385</small>||<small>0,26450</small><br />
|-<br />
|<small>6291456</small>||<small>1,3425E8</small>||<small>136314881</small>||<small>0,32866</small><br />
|-<br />
|<small>8388608</small>||<small>1,8233E8</small>||<small>184549377</small>||<small>0,26450</small><br />
|-<br />
|<small>12582912</small>||<small>2,8108E8</small>||<small>285212673</small>||<small>0,32866</small><br />
|-<br />
|<small>16777216</small>||<small>3,8144E8</small>||<small>385875969</small>||<small>0,26450</small><br />
|}<br />
und findet <math>S_m(n)-0{,}3286560975 \, n \le S_\varnothing(n) \le S_m(n) .</math><br />
|}<br />
<br />
== Korrektheit und Terminierung ==<br />
Der Rekursionsabbruch stellt die [[Terminiertheit|Terminierung]] von Mergesort offensichtlich sicher, so dass lediglich noch die [[Korrektheit (Informatik)|Korrektheit]] gezeigt werden muss. Dies geschieht, indem wir folgende Behauptung beweisen:<br />
<br />
'''Behauptung''': In Rekursionstiefe <math>i</math> werden die sortierten Teillisten aus Rekursionstiefe <math>i{+}1</math> korrekt sortiert.<br />
<br />
'''Beweis''': Sei [[Ohne Beschränkung der Allgemeinheit|o.&nbsp;B.&nbsp;d.&nbsp;A.]] die <math>(i{+}1)</math>-te Rekursion die tiefste. Dann sind die Teillisten offensichtlich sortiert, da sie einelementig sind. Somit ist ein Teil der Behauptung schon mal gesichert. Nun werden diese sortierten Teillisten eine Rekursionsebene nach oben, also in die <math>i</math>-te Rekursion übergeben. Dort werden diese nach Konstruktion der ''merge''-Prozedur von Mergesort korrekt sortiert. Somit ist unsere Behauptung erfüllt und die totale Korrektheit von Mergesort bewiesen.<br />
<br />
== Natural Mergesort ==<br />
'''Natural Mergesort''' ''(natürliches Mergesort)'' ist eine Erweiterung von Mergesort, die<br />
bereits vorsortierte Teilfolgen, so genannte ''runs'', innerhalb der zu sortierenden Startliste ausnutzt. Die Basis für den Mergevorgang bilden hier nicht die rekursiv oder iterativ gewonnenen Zweiergruppen, sondern die in einem ersten Durchgang zu bestimmenden ''runs'':<br />
<br />
Startliste : 3--4--2--1--7--5--8--9--0--6<br />
Runs bestimmen: 3--4 2 1--7 5--8--9 0--6<br />
Merge : 2--3--4 1--5--7--8--9 0--6<br />
Merge : 1--2--3--4--5--7--8--9 0--6<br />
Merge : 0--1--2--3--4--5--6--7--8--9<br />
<br />
Diese Variante hat den Vorteil, dass sortierte Folgen „erkannt“ werden und die Komplexität im Best-Case <math> \mathcal{O}(n) </math> beträgt. Average- und Worst-Case-Verhalten ändern sich hingegen nicht.<br />
<br />
Außerdem eignet sich Mergesort gut für größere Datenmengen, die nicht mehr im Hauptspeicher gehalten werden können – es müssen jeweils nur beim Verschmelzen in jeder Ebene zwei ''Listen'' vom externen Zwischenspeicher (z.&nbsp;B. Festplatte) gelesen und eine dorthin geschrieben werden. Eine Variante nutzt den verfügbaren Hauptspeicher besser aus (und minimiert Schreib-/Lesezugriffe auf der Festplatte), indem mehr als nur zwei Teil-Listen gleichzeitig vereinigt werden, und damit die Rekursionstiefe abnimmt.<br />
<br />
== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit entwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen beschrieben werden, die Teilen-Phase und die anschließende Misch-Phase. Die erste besteht aus vielen rekursiven Aufrufen, die immer wieder den gleichen Aufteilungsprozess durchführen, bis die Teilsequenzen trivial sortiert sind (mit einem oder keinem Element). Ein intuitiver Ansatz ist es, diese rekursiven Aufrufe zu parallelisieren.<ref name=":2">{{Literatur |Autor=Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford |Titel=Introduction to algorithms |Hrsg= |Sammelwerk= |Band= |Nummer= |Auflage=Third edition |Verlag=MIT Press |Ort=Cambridge, Mass. |Datum=2009 |ISBN=978-0-262-27083-0 |Seiten= |Online=https://www.worldcat.org/oclc/676697295 |Abruf=2020-03-06}}</ref> Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Rekursion unter Verwendung der Schlüsselwörter [[:en:Fork–join_model|fork and join]].<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals der parallelen Ausführung ist.<br />
<br />
=== Mergesort mit paralleler Mischmethode ===<br />
Ein besserer Parallelismus kann durch eine parallele [[Merge-Algorithmen|Mischmethode]] erreicht werden. [[:en:Introduction_to_Algorithms|Cormen et al.]] präsentieren eine binäre Variante, welche zwei sortierte Teilsequenzen in eine sortierte Ausgabesequenz mischt. Eine ausführlichere Beschreibung findet sich [[:en:Merge_algorithm#Parallel_merge|hier]]. <ref name=":2" /><br />
<br />
In der längeren der beiden Sequenzen (falls ungleich lang) wird das Element des mittleren Indexes ausgewählt. Seine Position in der anderen Sequenz wird so bestimmt, dass die Sequenz sortiert bliebe, wenn dieses Element an der bestimmten Stelle eingefügt werden würde. So weiß man, wie viele Elemente insgesamt kleiner sind als das Pivot Element und die finale Position des Pivots kann in der Ausgabesequenz berechnet werden. Für die so erzeugten Teilfolgen der kleineren und größeren Elemente wird die Mischmethode wieder parallel ausgeführt, bis der Basisfall der Rekursion erreicht ist. <br />
<br />
Der folgende Pseudocode illustriert den Mergesort mit modifizierter paralleler Mischmethode (aus Cormen et al.).<br />
<br />
/**<br />
* A: Input array<br />
* B: Output array<br />
* lo: lower bound<br />
* hi: upper bound<br />
* off: offset<br />
*/<br />
'''algorithm''' parallelMergesort(A, lo, hi, B, off) '''is'''<br />
len := hi - lo + 1<br />
'''if''' len == 1 '''then'''<br />
<nowiki> B[off] := A[lo]</nowiki><br />
'''else''' let T[1..len] be a new array<br />
mid := ⌊(lo + hi) / 2⌋ <br />
mid' := mid - lo + 1<br />
'''fork''' parallelMergesort(A, lo, mid, T, 1)<br />
parallelMergesort(A, mid + 1, hi, T, mid' + 1) <br />
'''join''' <br />
parallelMerge(T, 1, mid', mid' + 1, len, B, off)<br />
Um eine Rekurrenzrelation für den Worst Case zu erhalten müssen die rekursiven Aufrufe von parallelMergesort aufgrund der parallelen Ausführung nur einmal aufgeführt werden. Man erhält<br />
<br />
<math display="inline">T_{\infty}^{\text{sort} }(n) = T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + T_{\infty}^{\text{merge}}(n) <br />
= T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + \Theta \left( \log(n)^2\right)</math>.<br />
<br />
Für genauere Informationen über die Komplexität der parallelen Mischmethode, siehe [[:en:Merge_algorithm#Parallel_merge|Merge algorithm]].<br />
<br />
Die Lösung dieser Rekurrenz ist<br />
<br />
<math display="inline">T_{\infty}^{\text{sort}} = \Theta \left ( \log(n)^3 \right)</math>.<br />
<br />
Dieser Algorithmus erreicht eine Parallelisierbarkeit von <math>\Theta \biggr({n \over (\log n)^2}\biggr)</math>, was um einiges besser ist als der Parallelismus des vorherigen Algorithmus. Solch ein Sortieralgorithmus kann, wenn er mit einem schnellen stabilen sequentiellen Sortieralgorithmus und einer sequentiellen Mischmethode als Basisfall für das Mischen von zwei kleinen Sequenzen ausgestattet ist gut in der Praxis funktionieren.<ref>Victor J. Duvanenko "Parallel Merge Sort" Dr. Dobb's Journal & blog[https://duvanenko.tech.blog/2018/01/13/parallel-merge-sort/] and GitHub repo C++ implementation [https://github.com/DragonSpit/ParallelAlgorithms]</ref><br />
<br />
=== Paralleler Mehrwege-Mergesort ===<br />
Es wirkt unnatürlich, Mergesort Algorithmen auf binäre Mischmethoden zu beschränken, da oftmals mehr als zwei Prozessoren zur Verfügung stehen. Ein besserer Ansatz wäre es, ein [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] zu realisieren. Diese Generalisierung mischt im Gegensatz zum binären Mischen <math>k</math> sortierte Sequenzen zu einer sortierten Sequenz. Diese Misch-Variante eignet sich gut zur Beschreibung eines Sortieralgorithmus auf einem [[Parallel Random Access Machine|PRAM]].<ref>Peter Sanders, Johannes Singler. 2008. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg08/singler.pdf</ref><ref name=":02">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
==== Grundidee ====<br />
[[File:Parallel_multiway_mergesort_process.svg|verweis=https://en.wikipedia.org/wiki/File:Parallel_multiway_mergesort_process.svg|alternativtext=|mini|Der parallele Mehrwege-Mergesort Algorithmus auf vier Prozessoren <math>t_0</math> bis <math>t_3</math>.]]<br />
Gegeben sei eine Folge von <math>n</math> Elementen. Ziel ist es, diese Sequenz mit <math>p</math> verfügbaren Prozessoren zu sortieren. Die Elemente sind dabei gleich auf alle Prozessoren aufgeteilt und werden zunächst lokal mit einem sequentiellen [[Sortierverfahren|Sortieralgorithmus]] vorsortiert. Dementsprechend bestehen die Daten nun aus sortierten Folgen <math>S_1, ..., S_p</math> der Länge <math display="inline">\lceil \frac{n}{p} \rceil</math>. Der Einfachheit halber sei <math>n</math> eine Vielfaches von <math>p</math>, so dass für <math>i = 1, ..., p</math> gilt: <math display="inline">\left\vert S_i \right\vert = \frac{n}{p}</math>. Jede dieser Sequenzen wird wiederum in <math>p</math> Teilsequenzen <math>S_{i,1}, ..., S_{i,p}</math> partitioniert, indem für <math>j = 1,..., p</math> die Trennelemente <math>v_j</math> mit globalem Rang <math display="inline">k = j \frac{n}{p}</math> bestimmt werden. Die korrespondierenden Indizes werden in jeder Folge <math>S_i</math> mit binärer Sucher ermittelt, sodass die Folgen anhand der Indizes aufgeteilt werden könne. Formal definiert gilt somit <math>S_{i,j} := \{x \in S_i | rank(v_{j-1}) < rank(x) \le rank(v_j)\}</math>.<br />
<br />
Nun werden die Elemente von <math>S_{1,i}, ..., S_{p,i}</math> dem Prozessor <math>i</math> zugeteilt. Dies sind alle Elemente vom globalen Rang <math display="inline">(i-1) \frac{n}{p}</math> bis zum Rang <math display="inline">i \frac{n}{p}</math>, die über die <math>S_i</math> verteilt sind. So erhält erhält jeder Prozessor eine Folge von sortierten Sequenzen. Aus der Tatsache, dass der Rang <math>k</math> der Trennelemente <math>v_i</math> global gewählt wurde, ergeben sich zwei wichtige Eigenschaften: Zunächst sind die Trennelemente so gewählt, dass jeder Prozessor nach der Zuteilung der neuen Daten immer noch mit <math display="inline">n/p</math> Elementen betraut ist. Der Algorithmus besitzt also eine perfekte [[Lastverteilung (Informatik)|Lastverteilung]]. Außerdem sind alle Elemente des Prozessors <math>i</math> kleiner oder gleich der Elemente des Prozessors <math>i+1</math>. Wenn nun jeder Prozessor ein [[Merge-Algorithmen#k-Wege-Mischen|p-Wege-Mischen]] lokal durchführt, sind aufgrund dieser Eigenschaft die Elemente global sortiert. Somit müssen die Ergebnisse nur in der Reihenfolge der Prozessoren zusammengesetzt werden.<br />
<br />
==== Trennelementbestimmung ====<br />
In der einfachsten Form sind <math>p</math> sortierte Folgen <math>S_1, ..., S_p</math> gleichverteilt auf <math>p</math> Prozessoren sowie ein Rang <math>k</math> gegeben. Gesucht ist nun ein Trennelement <math>x</math> mit globalem Rang <math>k</math> in der Vereinigung der Folgen. Damit kann jede Folge <math>S_i</math> an einem Index <math>l_i</math> in zwei Teile aufgeteilt werden: Der untere Teil besteht nur aus Elementen, die kleiner <math>x</math> sind, während der obere Teil alle Elemente enthält, welche größer oder gleich als <math>x</math> sind. <br />
<br />
Der hier vorgestellte sequentielle Algorithmus gibt die Indizes der Trennungen zurück, also die Indizes <math>l_i</math>in den Folgen <math>S_i</math>, sodass <math>S_i[l_i]</math> einen global kleineren Rang als <math>k</math> hat und <math>\mathrm{rank}\left(S_i[l_i+1]\right) \ge k</math> ist.<ref>Peter Sanders. 2019. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg19/vorlesung.pdf</ref><br />
'''algorithm'''<nowiki> msSelect(S : Array of sorted Sequences [S_1,..,S_p], k : int) </nowiki>'''is'''<br />
'''for''' i = 1 '''to''' p '''do''' <br />
(l_i, r_i) = (0, |S_i|-1)<br />
<br />
'''while''' there exists i: l_i < r_i '''do'''<br />
//pick Pivot Element in S_j[l_j],..,S_j[r_j], chose random j uniformly<br />
v := pickPivot(S, l, r)<br />
'''for''' i = 1 '''to''' p '''do''' <br />
m_i = binarySearch(v, S_i[l_i, r_i]) //sequentially<br />
'''if''' m_1 + ... + m_p >= k '''then''' //m_1+ ... + m_p is the global rank of v<br />
r := m //vector assignment<br />
'''else'''<br />
l := m <br />
<br />
'''return''' l<br />
Für die Komplexitätsanalyse wurde das PRAM-Modell gewählt. Die p-fache Ausführung der ''binarySearch'' Methode hat eine Laufzeit in <math>\mathcal{O}\left(p\log\left(n/p\right)\right)</math>, falls die Daten über alle <math>p</math> Prozessoren gleichverteilt anliegen. Die erwartete Rekursionstiefe beträgt wie im [[Quickselect]] Algorithmus <math>\mathcal{O}\left(\log\left( \textstyle \sum_i |S_i| \right)\right) = \mathcal{O}(\log(n))</math>. Somit ist die gesamte erwartete Laufzeit <math>\mathcal{O}\left(p\log(n/p)\log(n)\right)</math>.<br />
<br />
Angewandt auf den parallelen Mehrwege-Mergesort muss die ''msSelect'' Methode parallel ausgeführt werden, um alle Trennelemente vom Rang <math display="inline"> i \frac n p</math> gleichzeitig zu finden. Dies kann anschließend verwendet werden, um jede Folge in <math>p</math> Teile zu zerschneiden. Es ergibt sich die gleiche Gesamtlaufzeit <math>\mathcal{O}\left(p\, \log(n/p)\log(n)\right)</math>.<br />
<br />
==== Pseudocode ====<br />
Hier ist der komplette Pseudocode für den parallelen Mehrwege-Mergesort. Dabei wird eine Barriere-Synchronisation vor und nach der Trennelementbestimmung angenommen, sodass jeder Prozessor seine Trennelemente und die Partitionierung seiner Sequenz richtig berechnen kann. <br />
/**<br />
* d: Unsorted Array of Elements<br />
* n: Number of Elements<br />
* p: Number of Processors<br />
* return Sorted Array<br />
*/<br />
'''algorithm''' parallelMultiwayMergesort(d : Array, n : int, p : int) '''is'''<br />
o := '''new''' Array[0, n] // the output array<br />
'''for''' i = 1 '''to''' p '''do in parallel''' // each processor in parallel<br />
<nowiki> S_i := d[(i-1) * n/p, i * n/p] // Sequence of length n/p</nowiki><br />
sort(S_i) // sort locally<br />
'''synch'''<br />
v_i := msSelect([S_1,...,S_p], i * n/p) // element with global rank i * n/p<br />
'''synch'''<br />
(S_i,1 ,..., S_i,p) := sequence_partitioning(si, v_1, ..., v_p) // split s_i into subsequences<br />
<br />
o[(i-1) * n/p, i * n/p] := kWayMerge(s_1,i, ..., s_p,i) // merge and assign to output array<br />
<br />
'''return''' o<br />
<br />
==== Analyse ====<br />
Zunächst sortiert jeder Prozessor die zugewiesenen <math>n/p</math> Elemente lokal mit einem vergleichsbasiertem Sortieralgorithmus der Komplexität <math>\mathcal{O}\left( n/p \; \log ( n/p) \right)</math>. Anschließend können die Trennelemente in Zeit <math>\mathcal{O}\left(p \,\log(n/p) \log (n) \right)</math> bestimmt werden. Schließlich müssen jede Gruppe von <math>p</math> Teilstücken gleichzeitig von jedem Prozessor zusammen gemischt werden. Dies hat eine Laufzeit von <math>\mathcal{O}(\log(p)\; n/p )</math>, indem ein sequentieller [[:en:Merge_algorithm|k-Wege Mischalgorithmus]] verwendet wird. Somit ergibt sich eine Gesamtlaufzeit von<br />
<br />
<math>\mathcal{O}\left( \frac n p \log\left(\frac n p\right) + p \log \left( \frac n p\right) \log (n) + \frac n p \log (p) \right)</math>.<br />
<br />
==== Praktische Anpassung und Anwendung ====<br />
Der Mehrwege-Mergesort Algorithmus ist durch seine hohe Parallelität, was den Einsatz vieler Prozessoren ermöglicht, sehr skalierbar. Dies macht den Algorithmus zu einem brauchbaren Kandidaten für das Sortieren großer Datenmengen, wie sie beispielsweise in [[Rechnerverbund|Computer-Clustern]] verarbeitet werden. Da der Speicher in solchen Systemen in der Regel keine limitierende Ressource darstellt, ist der Nachteil der Speicherkomplexität von Mergesort vernachlässigbar. Allerdings werden in solchen Systemen andere Faktoren wichtig, die bei der Modellierung auf einer [[Parallel Random Access Machine|PRAM]] nicht berücksichtigt werden. Hier sind unter anderem die folgenden Aspekte zu berücksichtigen: Die [[Speicherhierarchie]], wenn die Daten nicht in den Cache der Prozessoren passen, oder der Kommunikationsaufwand beim Datenaustausch zwischen den Prozessoren, der zu einem Engpass werden könnte, wenn auf die Daten nicht mehr über den gemeinsamen Speicher zugegriffen werden kann.<br />
<br />
[[Peter Sanders|Sanders]] et al. haben in ihrem Paper einen [[Bulk Synchronous Parallel Computers|bulk synchronous parallel]]-Algorithmus für einen mehrstufigen Mehrwege-Mergesort vorgestellt, der <math>p</math> Prozessoren in <math>r</math> Gruppen der Größe <math>p'</math> unterteilt. Alle Prozessoren sortieren zuerst lokal. Im Gegensatz zu einem einstufigen Mehrwege-Mergesort werden diese Sequenzen dann in <math>r</math> Teile aufgeteilt und den entsprechenden Prozessorgruppen zugeordnet. Diese Schritte werden innerhalb dieser Gruppen rekursiv wiederholt. So wird die Kommunikation reduziert und insbesondere Probleme mit vielen kleinen Nachrichten vermieden. Die hierarchische Struktur des zugrundeliegenden realen Netzwerks (z.B. [[Rack|Racks]], [[Rechnerverbund|Cluster]],...) kann zur Definition der Prozessorgruppen verwendet werden.<ref name=":02" /><br />
<br />
=== Weitere Varianten ===<br />
Mergesort war einer der ersten Sortieralgorithmen, bei dem ein optimaler [[Speedup]] erreicht wurde, wobei Richard Cole einen cleveren Subsampling-Algorithmus verwendete, um die O(1)-Zusammenführung sicherzustellen.<ref>{{Literatur |Autor=Richard Cole |Titel=Parallel Merge Sort |Sammelwerk=SIAM Journal on Computing |Band=17 |Nummer=4 |Datum=1988-08 |ISSN=0097-5397 |DOI=10.1137/0217049 |Seiten=770–785 |Online=http://epubs.siam.org/doi/10.1137/0217049 |Abruf=2020-03-06}}</ref> Andere ausgeklügelte parallele Sortieralgorithmen können die gleichen oder bessere Zeitschranken mit einer niedrigeren Konstante erreichen. David Powers beschrieb beispielsweise 1991 einen parallelisierten [[Quicksort]] (und einen verwandten [[Radixsort]]), der durch implizite Partitionierung in <math>O(\log n)</math> Zeit auf einer [[Parallel Random Access Machine|CRCW-Parallel Random Access Machine (PRAM)]] mit <math>n</math> Prozessoren arbeiten kann.<ref>Powers, David M. W. [http://citeseer.ist.psu.edu/327487.html Parallelized Quicksort and Radixsort with Optimal Speedup], ''Proceedings of International Conference on Parallel Computing Technologies''. [[Novosibirsk]]. 1991.</ref> Powers zeigt ferner, dass eine Pipeline-Version von Batchers Bitonic Mergesort in <math>O((\log n)^2)</math> Zeit auf einem Butterfly-Sortiernetzwerk in der Praxis schneller ist als sein <math>O(\log n)</math> Sortieralgorithmus auf einer PRAM, und er bietet eine detaillierte Diskussion der versteckten Overheads beim Vergleich, bei der Radix- und der Parallelsortierung.<ref>David M. W. Powers, [http://david.wardpowers.info/Research/AI/papers/199501-ACAW-PUPC.pdf Parallel Unification: Practical Complexity], Australasian Computer Architecture Workshop, Flinders University, January 1995</ref><br />
<br />
<br />
== Sonstiges ==<br />
Da Mergesort die Startliste sowie alle Zwischenlisten sequenziell abarbeitet, eignet er sich besonders zur Sortierung von [[Liste (Datenstruktur)|verketteten Listen]]. Für [[Feld (Datentyp)|Arrays]] wird normalerweise ein temporäres Array derselben Länge des zu sortierenden Arrays als Zwischenspeicher verwendet (das heißt Mergesort arbeitet normalerweise nicht [[in-place]], s. o.). Quicksort dagegen benötigt kein temporäres Array.<br />
<br />
Die [[Silicon Graphics|SGI]]-Implementierung der [[Standard Template Library|Standard Template Library (STL)]] verwendet den Mergesort als Algorithmus zur stabilen Sortierung.<ref>http://www.sgi.com/tech/stl/stable_sort.html stable_sort</ref><br />
<br />
== Literatur ==<br />
* Robert Sedgewick: ''Algorithmen.'' Pearson Studium, Februar 2002, ISBN 3-8273-7032-9<br />
<br />
== Weblinks ==<br />
* [http://www.hermann-gruber.com/lehre/sorting/Merge/Merge.html Visualisierung und Tutorial für Mergesort, mit Darstellung der Rekursion]<br />
* [http://www.iti.fh-flensburg.de/lang/algorithmen/sortieren/merge/merge.htm Demonstration des Merge-Vorgangs] ([[Java-Applet]])<br />
<br />
== Einzelnachweise ==<br />
<references /><br />
<br />
[[Kategorie:Sortieralgorithmus]]<br />
<br />
[[no:Sorteringsalgoritme#Flettesortering]]</div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Mergesort&diff=197540495Mergesort2020-03-07T15:09:18Z<p>ParAlgMergeSort: /* Grundidee */</p>
<hr />
<div>[[Datei:Merge sort animation2.gif|miniatur|300px|Beispiel, wie Mergesort eine Liste sortiert. Die Listenelemente werden durch Punkte dargestellt. Die waagerechte Achse gibt an, wo sich ein Element in der Liste befindet, die senkrechte Achse gibt an, wie groß ein Element ist.]]<br />
<br />
'''Mergesort''' (von {{enS|''merge''}} ‚verschmelzen‘ und {{lang|en|''sort''}} ‚sortieren‘) ist ein [[Stabiles Sortierverfahren|stabiler]] [[Sortierverfahren|Sortieralgorithmus]], der nach dem Prinzip ''[[Teile und herrsche (Informatik)|teile und herrsche]]'' (divide and conquer) arbeitet. Er wurde erstmals [[1945]] durch [[John von Neumann]] vorgestellt.<ref>{{Literatur | Autor=Donald E. Knuth | Titel=The Art of Computer Programming. Volume 3: Sorting and Searching. | Auflage=2 | Verlag=Addison-Wesley | Ort= | Jahr=1998 | Seiten=158 }}</ref><br />
<br />
== Funktionsweise ==<br />
Mergesort betrachtet die zu sortierenden Daten als Liste und zerlegt sie in kleinere Listen, die jede für sich sortiert werden. Die sortierten kleinen Listen werden dann im Reißverschlussverfahren zu größeren Listen zusammengefügt (engl. {{lang|en|''(to) merge''}}), bis wieder eine sortierte Gesamtliste erreicht ist. Das Verfahren arbeitet bei Arrays in der Regel nicht [[in-place]], es sind dafür aber (trickreiche) Implementierungen bekannt, in welchen die Teil-Arrays üblicherweise rekursiv zusammengeführt werden.<ref>{{Internetquelle|url=https://github.com/h2database/h2database/blob/2cf822269945e25973aa6f1d412f47f7254ce383/h2/src/tools/org/h2/dev/sort/InPlaceStableMergeSort.java|titel=h2database/h2database|werk=GitHub|zugriff=2016-09-01}}</ref> [[Liste (Datenstruktur)|Verkettete Listen]] sind besonders geeignet zur Implementierung von Mergesort, dabei ergibt sich die in-place-Sortierung fast von selbst.<br />
<br />
=== Veranschaulichung der Funktionsweise ===<br />
[[Datei:Mergesort.png|mini|Funktionsweise]]<br />
<br />
Das Bild veranschaulicht die drei wesentlichen Schritte eines [[Teile und herrsche (Informatik)|Teile-und-herrsche]]-Verfahrens, wie sie im Rahmen von Mergesort umgesetzt werden. Der Teile-Schritt ist ersichtlich trivial (die Daten werden einfach in zwei Hälften aufgeteilt). Die wesentliche Arbeit wird beim Verschmelzen (merge) geleistet – daher rührt auch der Name des Algorithmus. Bei [[Quicksort]] ist hingegen der Teile-Schritt aufwendig und der Merge-Schritt einfacher (nämlich eine [[Konkatenation (Listen)|Konkatenierung]]).<br />
<br />
Bei der Betrachtung des in der Grafik dargestellten Verfahrens sollte man sich allerdings bewusst machen, dass es sich hier nur um eine von mehreren [[Rekursion]]sebenen handelt. So könnte etwa die Sortierfunktion, welche die beiden Teile 1 und 2 sortieren soll, zu dem Ergebnis kommen, dass diese Teile immer noch zu groß für die Sortierung sind. Beide Teile würden dann wiederum aufgeteilt und der Sortierfunktion rekursiv übergeben, so dass eine weitere Rekursionsebene geöffnet wird, welche dieselben Schritte abarbeitet. Im Extremfall (der bei Mergesort sogar der Regelfall ist) wird das Aufteilen so weit fortgesetzt, bis die beiden Teile nur noch aus einzelnen Datenelementen bestehen.<br />
<br />
== Implementierung ==<br />
Der folgende [[Pseudocode]] illustriert die Arbeitsweise des [[Algorithmus]], wobei ''liste'' die zu sortierenden Elemente enthält.<br />
<br />
funktion mergesort(liste);<br />
falls (Größe von liste <= 1) dann antworte liste<br />
sonst<br />
halbiere die liste in linkeListe, rechteListe<br />
linkeListe = mergesort(linkeListe)<br />
rechteListe = mergesort(rechteListe)<br />
antworte merge(linkeListe, rechteListe)<br />
<br />
funktion merge(linkeListe, rechteListe);<br />
neueListe<br />
solange (linkeListe und rechteListe nicht leer)<br />
| falls (erstes Element der linkeListe <= erstes Element der rechteListe)<br />
| dann füge erstes Element linkeListe in die neueListe hinten ein und entferne es aus linkeListe<br />
| sonst füge erstes Element rechteListe in die neueListe hinten ein und entferne es aus rechteListe<br />
solange_ende<br />
solange (linkeListe nicht leer)<br />
| füge erstes Element linkeListe in die neueListe hinten ein und entferne es aus linkeListe<br />
solange_ende<br />
solange (rechteListe nicht leer)<br />
| füge erstes Element rechteListe in die neueListe hinten ein und entferne es aus rechteListe<br />
solange_ende<br />
antworte neueListe<br />
<br />
== Beispiel ==<br />
[[Datei:Mergesort example.png|269px|links]]<br />
<br />
Im letzten Verschmelzungsschritt ist das Reißverschlussverfahren beim Verschmelzen (in der Abb. „Mischen:“) angedeutet. Blaue Pfeile verdeutlichen den Aufteilungsschritt, grüne Pfeile die Verschmelzungsschritte.<br />
<br />
Es folgt ein Beispielcode analog zum obigen Abschnitt "Implementierung" für den rekursiven Sortieralgorithmus. Er teilt rekursiv absteigend die Eingabe in 2 kleinere Listen, bis diese trivialerweise sortiert sind, und verschmilzt sie auf dem rekursiven Rückweg, wodurch sie sortiert werden.<br />
'''function''' merge_sort(list ''x'')<br />
<br />
'''if''' length(''x'') ≤ 1 '''then'''<br />
'''return''' ''x'' // Kurzes ''x'' ist trivialerweise sortiert.<br />
<br />
'''var''' ''l'' := empty list<br />
'''var''' ''r'' := empty list<br />
'''var''' ''i'', ''nx'' := length(''x'')−1<br />
// Teile ''x'' in die zwei Hälften ''l'' und ''r'' ...<br />
'''for''' ''i'' := 0 '''to''' floor(''nx''/2) '''do'''<br />
append ''x''[''i''] to ''l''<br />
'''for''' ''i'' := floor(''nx''/2)+1 '''to''' ''nx'' '''do'''<br />
append ''x''[''i''] to ''r''<br />
// ... und sortiere beide (einzeln).<br />
''l'' := merge_sort(''l'')<br />
''r'' := merge_sort(''r'')<br />
// Verschmelze die sortierten Hälften.<br />
'''return''' merge(''l'', ''r'')<br />
Beispielcode zum Verschmelzen zweier sortierter Listen.<br />
'''function''' merge(list ''l'', list ''r'')<br />
'''var''' ''y'' := empty list // Ergebnisliste<br />
<br />
'''var''' ''nl'' := length(''l'')−1<br />
'''var''' ''nr'' := length(''r'')−1<br />
'''var''' ''i'', ''il'' := 0<br />
'''for''' ''i'' := 0 '''to''' ''nl''+''nr+1'' '''do'''<br />
'''if''' ''il'' > ''nl'' '''then'''<br />
append ''r''[''i''−''il''] to ''y''<br />
'''continue'''<br />
'''if''' ''il'' < ''i''−''nr'' '''then'''<br />
append ''l''[''il''] to ''y''<br />
''il'' := ''il''+1<br />
'''continue'''<br />
// Jetzt ist 0 ≤ ''il'' ≤ ''nl'' und 0 ≤ ''i''−''il'' ≤ ''nr''.<br />
'''if''' ''l''[''il''] ≤ ''r''[''i''−''il''] '''then'''<br />
append ''l''[''il''] to ''y''<br />
''il'' := ''il''+1<br />
'''else'''<br />
append ''r''[''i''−''il''] to ''y''<br />
<br />
'''return''' ''y''<br />
<br />
===C++ 11===<br />
Eine Implementierung in der Programmiersprache C++ unter Verwendung von [[Zeigerarithmetik]] könnte folgendermaßen aussehen:<br />
<br />
<syntaxhighlight lang="cpp"><br />
#ifndef ALGORITHM_H<br />
#define ALGORITHM_H<br />
<br />
#include <functional><br />
<br />
namespace ExampleNamespace<br />
{<br />
class Algorithm<br />
{<br />
public:<br />
Algorithm() = delete;<br />
template<typename T><br />
static auto ptrDiff(T const * const begin, T const * const end)<br />
{<br />
return end - begin;<br />
}<br />
template<typename T><br />
static auto midPtr(T* const begin, long long const & ptr_diff)<br />
{<br />
return begin + ptr_diff / 2u;<br />
}<br />
template<typename T><br />
static auto midPtr(T* const begin, T* const end)<br />
{<br />
return midPtr(begin, ptrDiff(begin, end));<br />
}<br />
static auto continueSplit(long long const & ptr_diff)<br />
{<br />
return ptr_diff > 1u;<br />
}<br />
template<typename T><br />
static auto continueSplit(T const * const begin, T const * const end)<br />
{<br />
return continueSplit(ptrDiff(begin, end));<br />
}<br />
template<typename T><br />
static auto mergeSort(T* const begin, T* const end)<br />
{<br />
mergeSort(begin, end, std::less<T>());<br />
}<br />
template<typename T, typename Compare><br />
static auto mergeSort(T* const begin, T* const end, Compare&& comp)<br />
{<br />
auto ptr_diff = ptrDiff(begin, end);<br />
if (ptr_diff) {<br />
auto* temp = new T[ptr_diff];<br />
mergeSort(begin, end, temp, comp);<br />
delete[] temp;<br />
}<br />
}<br />
template<typename T><br />
static auto copyRange(T const * begin, T const * const end, T* dst)<br />
{<br />
copyRangeOverwrite(begin, end, dst);<br />
}<br />
template<typename T><br />
static auto copyRangeOverwrite(T const * begin, T const * const end, T*& dst)<br />
{<br />
while (begin != end) {<br />
*dst++ = *begin++;<br />
}<br />
}<br />
private:<br />
template<typename T, typename Compare><br />
static void mergeSort(T* const begin, T* const end, T* temp, Compare&& comp)<br />
{<br />
auto ptr_diff = ptrDiff(begin, end);<br />
if (continueSplit(ptr_diff)) {<br />
auto * const mid = midPtr(begin, ptr_diff);<br />
mergeSort(begin, mid, temp, comp);<br />
mergeSort(mid, end, temp, comp);<br />
merge(begin, mid, end, temp, comp);<br />
}<br />
}<br />
template<typename T, typename Compare><br />
static auto merge(T* begin, T const * const mid, T const * const end, T* temp, Compare&& comp)<br />
{<br />
copyRange(begin, end, temp);<br />
auto* right_temp = temp + ptrDiff(begin, mid);<br />
auto const * const left_temp_end = right_temp;<br />
auto const * const right_temp_end = temp + ptrDiff(begin, end);<br />
mergeResults(temp, right_temp, right_temp_end, begin, comp);<br />
copyRangeOverwrite(temp, left_temp_end, begin);<br />
copyRangeOverwrite(right_temp, right_temp_end, begin);<br />
}<br />
template<typename T, typename Compare><br />
static auto mergeResults(T*& begin, T*& mid, T const * const end, T*& dst, Compare&& comp)<br />
{<br />
auto const * const mid_ptr = mid;<br />
while (begin != mid_ptr && mid != end) {<br />
*dst++ = comp(*begin, *mid) ? *begin++ : *mid++;<br />
}<br />
}<br />
};<br />
}<br />
<br />
#endif // !ALGORITHM_H<br />
</syntaxhighlight><br />
<br />
== Komplexität ==<br />
Mergesort ist ein stabiles Sortierverfahren, vorausgesetzt der Merge-Schritt ist entsprechend implementiert. Seine [[Komplexität (Informatik)|Komplexität]] beträgt im Worst-, Best- und Average-Case in [[Landau-Symbole|Landau-Notation]] ausgedrückt stets <math> \mathcal{O}(n \cdot \log (n)) </math>.<br />
<br />
Damit ist Mergesort hinsichtlich der Komplexität Quicksort grundsätzlich überlegen, da Quicksort (ohne besondere Vorkehrungen) ein {{nowrap|[[Worst Case|Worst-Case]]-Verhalten}} von <math> \Theta(n^2) </math> besitzt. Es benötigt jedoch zusätzlichen Speicherplatz (der Größenordnung <math>\mathcal{O}(n)</math>), ist also kein In-place-Verfahren.<br />
<br />
Für die Laufzeit <math>T(n)</math> von Mergesort bei <math>n</math> zu sortierenden Elementen gilt die Rekursionsformel<br />
<br />
:<math>T(n)= \underbrace{T(\lfloor \tfrac{n}{2} \rfloor)}_{\text{Aufwand, den einen Teil zu sortieren}} <br />
+ \underbrace{T(\lceil \tfrac{n}{2} \rceil)}_{\text{Aufwand, den anderen Teil zu sortieren}} <br />
+ \underbrace{\mathcal{O}(n)}_{\text{Aufwand, die beiden Teile zu verschmelzen}} </math><br />
<br />
mit dem Rekursionsanfang <math>T(1)=1</math>.<br />
<br />
Nach dem [[Master-Theorem]] kann die Rekursionsformel durch <math>2 \, T(\lfloor \tfrac{n}{2} \rfloor) + n </math> bzw. <math>2 \, T(\lceil \tfrac{n}{2} \rceil) + n </math> approximiert werden mit jeweils der Lösung (2. Fall des Mastertheorems, s. dort) <math>T(n)=\mathcal{O}(n \cdot \log (n)) </math>.<br />
<br />
{| class="wikitable left mw-collapsible mw-collapsed font-size: 105.3%;"<br />
|style="text-align:left; font-size: 95%;"| '''Durchschnittliche und maximale Anzahl Vergleiche''' &nbsp; &nbsp; &nbsp; &nbsp; <br />
|-<br />
|<br />
Sind <math>l_0,l_1</math> die Längen der zu verschmelzenden und vorsortierten Folgen <math>F_0,F_1 ,</math> dann gilt für die Anzahl <math>M</math> der erforderlichen Vergleiche fürs sortierende Verschmelzen<br />
: <math>\min(l_0,l_1) \le M \le l_0+l_1-1 </math>,<br />
da erstens eine Folge komplett vor der anderen liegen kann, d.&nbsp;h., es ist <math>F_0[l_0] \prec F_1[1]</math> bzw. <math>F_1[l_1] \prec F_0[1] , </math> oder es ist zweitens <math>F_0[l_0-1] \prec F_1[l_1] \prec F_0[l_0]</math> (bzw. umgekehrt), sodass die Elemente bis zum letzten Element in jeder Folge verglichen werden müssen. Dabei ist jeweils angenommen, dass das Vergleichen der zwei Folgen bei den Elementen mit niedrigem Index beginnt. Mit<br />
: <math>V_m(l_0+l_1) := l_0+l_1-1</math><br />
(Subskript <math>m </math> für ''maximal'') sei die maximale Anzahl der Vergleiche fürs ''Verschmelzen'' bezeichnet.<br />
Für die maximale Anzahl <math>S_m(n)</math> an Vergleichen für einen ganzen ''Mergesort''-Lauf von <math>n</math> Elementen errechnet sich daraus<br />
: <math>S_m(n) = n l-2^l+1 ,</math> mit <math>l:=\lceil \log_2(n) \rceil .</math><br />
<math>S_m(n) </math> ist die {{OEIS|A001855}}.<br />
<br />
Für eine Gleichverteilung lässt sich auch die durchschnittliche Anzahl <math>V_\varnothing(l_0,l_1)</math> (Subskript <math>{}_\varnothing </math> für ''durchschnittlich'') der Vergleiche genau berechnen, und zwar ist für <math>l_1 = l_0</math><br />
: <math><br />
\begin{align}<br />
V_\varnothing(l_0,l_0) \; \; \; \; \; & = \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1} k\right) \, \bigg/ \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1}\right) \\<br />
& = 2 \, l_0^2/(l_0+1)<br />
\end{align}</math><br />
und für <math>l_1 = l_0-1</math><br />
: <math><br />
\begin{align}<br />
V_\varnothing(l_0,l_0-1) & = \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1} k \; + \sum_{k=l_0-1}^{2 l_0-2} \binom{k-1}{l_0-2} k\right) \\<br />
& \; \bigg/ \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1} \; \; \; + \sum_{k=l_0-1}^{2 l_0-2} \binom{k-1}{l_0-2} \; \; \right) \\<br />
& = 2 \, l_0^2/(l_0+1)-1 .<br />
\end{align}</math><br />
Dabei ist <math>k</math> die Anzahl der Vergleiche für die Anordnung<br />
: <math>\underbrace{u\,v \dotso w\,0}_{k} \, \underbrace{1\,1 \dotso 1}_{l_1+l_0-k}</math> &nbsp; ,<br />
wobei <math>0</math> für das letzte (das am höchsten sortierende) Element in der Folge <math>F_0</math> steht, die (nicht von einem Element aus <math>F_0</math> unterbrochenen) {{nowrap|<math>1</math>en}} zu <math>F_1</math> gehören und <math>u,v,w</math> (die auch fehlen können) entweder zu <math>F_0</math> oder zu <math>F_1</math> gehören. Der in den Summenformeln beigegebene [[Binomialkoeffizient]] zählt die verschiedenen Möglichkeiten für <math>u\,v \dotso w .</math><br />
<br />
Für die durchschnittliche Anzahl <math>S_\varnothing(n)</math> an Vergleichen für einen ganzen Mergesort-Lauf von <math>n</math> Elementen errechnet man daraus<br />
{| class="wikitable" style="text-align:right;"<br />
|-<br />
! <math>n</math> !! <math>S_\varnothing(n)</math> !! <math>S_m(n)</math> !! <math>(S_m(n)-</math><br><math>S_\varnothing(n))/n</math><br />
|-<br />
| <small>2</small>||<small>1,0000</small>||<small>1</small>||<small>0,00000</small><br />
|-<br />
|<small>3</small>||<small>2,6667</small>||<small>3</small>||<small>0,11111</small><br />
|-<br />
|<small>4</small>||<small>4,6667</small>||<small>5</small>||<small>0,08333</small><br />
|-<br />
|<small>6</small>||<small>9,8333</small>||<small>11</small>||<small>0,19444</small><br />
|-<br />
|<small>8</small>||<small>15,733</small>||<small>17</small>||<small>0,15833</small><br />
|-<br />
|<small>12</small>||<small>29,952</small>||<small>33</small>||<small>0,25397</small><br />
|-<br />
|<small>16</small>||<small>45,689</small>||<small>49</small>||<small>0,20694</small><br />
|-<br />
|<small>24</small>||<small>82,059</small>||<small>89</small>||<small>0,28922</small><br />
|-<br />
|<small>32</small>||<small>121,50</small>||<small>129</small>||<small>0,23452</small><br />
|-<br />
|<small>48</small>||<small>210,20</small>||<small>225</small>||<small>0,30839</small><br />
|-<br />
|<small>64</small>||<small>305,05</small>||<small>321</small>||<small>0,24920</small><br />
|-<br />
|<small>96</small>||<small>514,44</small>||<small>545</small>||<small>0,31838</small><br />
|-<br />
|<small>128</small>||<small>736,13</small>||<small>769</small>||<small>0,25677</small><br />
|-<br />
|<small>192</small>||<small>1218,9</small>||<small>1281</small>||<small>0,32348</small><br />
|-<br />
|<small>256</small>||<small>1726,3</small>||<small>1793</small>||<small>0,26061</small><br />
|-<br />
|<small>384</small>||<small>2819,8</small>||<small>2945</small>||<small>0,32606</small><br />
|-<br />
|<small>512</small>||<small>3962,6</small>||<small>4097</small>||<small>0,26255</small><br />
|-<br />
|<small>768</small>||<small>6405,6</small>||<small>6657</small>||<small>0,32736</small><br />
|-<br />
|<small>1024</small>||<small>8947,2</small>||<small>9217</small>||<small>0,26352</small><br />
|-<br />
|<small>1536</small>||<small>14345,0</small>||<small>14849</small>||<small>0,32801</small><br />
|-<br />
|<small>2048</small>||<small>19940,0</small>||<small>20481</small>||<small>0,26401</small><br />
|-<br />
|<small>3072</small>||<small>31760,0</small>||<small>32769</small>||<small>0,32833</small><br />
|-<br />
|<small>4096</small>||<small>43974,0</small>||<small>45057</small>||<small>0,26426</small><br />
|-<br />
|<small>6144</small>||<small>69662,0</small>||<small>71681</small>||<small>0,32849</small><br />
|-<br />
|<small>8192</small>||<small>96139,0</small>||<small>98305</small>||<small>0,26438</small><br />
|-<br />
|<small>12288</small>||<small>1,5161E5</small>||<small>155649</small>||<small>0,32857</small><br />
|-<br />
|<small>16384</small>||<small>2,0866E5</small>||<small>212993</small>||<small>0,26444</small><br />
|-<br />
|<small>24576</small>||<small>3,278E5</small>||<small>335873</small>||<small>0,32862</small><br />
|-<br />
|<small>32768</small>||<small>4,5009E5</small>||<small>458753</small>||<small>0,26447</small><br />
|-<br />
|<small>49152</small>||<small>7,0474E5</small>||<small>720897</small>||<small>0,32864</small><br />
|-<br />
|<small>65536</small>||<small>9,6571E5</small>||<small>983041</small>||<small>0,26448</small><br />
|-<br />
|<small>98304</small>||<small>1,5078E6</small>||<small>1540097</small>||<small>0,32865</small><br />
|-<br />
|<small>131072</small>||<small>2,0625E6</small>||<small>2097153</small>||<small>0,26449</small><br />
|-<br />
|<small>196608</small>||<small>3,2122E6</small>||<small>3276801</small>||<small>0,32865</small><br />
|-<br />
|<small>262144</small>||<small>4,3871E6</small>||<small>4456449</small>||<small>0,26450</small><br />
|-<br />
|<small>393216</small>||<small>6,8176E6</small>||<small>6946817</small>||<small>0,32865</small><br />
|-<br />
|<small>524288</small>||<small>9,2985E6</small>||<small>9437185</small>||<small>0,26450</small><br />
|-<br />
|<small>786432</small>||<small>1,4422E7</small>||<small>14680065</small>||<small>0,32865</small><br />
|-<br />
|<small>1048576</small>||<small>1,9646E7</small>||<small>19922945</small>||<small>0,26450</small><br />
|-<br />
|<small>1572864</small>||<small>3,0416E7</small>||<small>30932993</small>||<small>0,32866</small><br />
|-<br />
|<small>2097152</small>||<small>4,1388E7</small>||<small>41943041</small>||<small>0,26450</small><br />
|-<br />
|<small>3145728</small>||<small>6,3978E7</small>||<small>65011713</small>||<small>0,32866</small><br />
|-<br />
|<small>4194304</small>||<small>8,6971E7</small>||<small>88080385</small>||<small>0,26450</small><br />
|-<br />
|<small>6291456</small>||<small>1,3425E8</small>||<small>136314881</small>||<small>0,32866</small><br />
|-<br />
|<small>8388608</small>||<small>1,8233E8</small>||<small>184549377</small>||<small>0,26450</small><br />
|-<br />
|<small>12582912</small>||<small>2,8108E8</small>||<small>285212673</small>||<small>0,32866</small><br />
|-<br />
|<small>16777216</small>||<small>3,8144E8</small>||<small>385875969</small>||<small>0,26450</small><br />
|}<br />
und findet <math>S_m(n)-0{,}3286560975 \, n \le S_\varnothing(n) \le S_m(n) .</math><br />
|}<br />
<br />
== Korrektheit und Terminierung ==<br />
Der Rekursionsabbruch stellt die [[Terminiertheit|Terminierung]] von Mergesort offensichtlich sicher, so dass lediglich noch die [[Korrektheit (Informatik)|Korrektheit]] gezeigt werden muss. Dies geschieht, indem wir folgende Behauptung beweisen:<br />
<br />
'''Behauptung''': In Rekursionstiefe <math>i</math> werden die sortierten Teillisten aus Rekursionstiefe <math>i{+}1</math> korrekt sortiert.<br />
<br />
'''Beweis''': Sei [[Ohne Beschränkung der Allgemeinheit|o.&nbsp;B.&nbsp;d.&nbsp;A.]] die <math>(i{+}1)</math>-te Rekursion die tiefste. Dann sind die Teillisten offensichtlich sortiert, da sie einelementig sind. Somit ist ein Teil der Behauptung schon mal gesichert. Nun werden diese sortierten Teillisten eine Rekursionsebene nach oben, also in die <math>i</math>-te Rekursion übergeben. Dort werden diese nach Konstruktion der ''merge''-Prozedur von Mergesort korrekt sortiert. Somit ist unsere Behauptung erfüllt und die totale Korrektheit von Mergesort bewiesen.<br />
<br />
== Natural Mergesort ==<br />
'''Natural Mergesort''' ''(natürliches Mergesort)'' ist eine Erweiterung von Mergesort, die<br />
bereits vorsortierte Teilfolgen, so genannte ''runs'', innerhalb der zu sortierenden Startliste ausnutzt. Die Basis für den Mergevorgang bilden hier nicht die rekursiv oder iterativ gewonnenen Zweiergruppen, sondern die in einem ersten Durchgang zu bestimmenden ''runs'':<br />
<br />
Startliste : 3--4--2--1--7--5--8--9--0--6<br />
Runs bestimmen: 3--4 2 1--7 5--8--9 0--6<br />
Merge : 2--3--4 1--5--7--8--9 0--6<br />
Merge : 1--2--3--4--5--7--8--9 0--6<br />
Merge : 0--1--2--3--4--5--6--7--8--9<br />
<br />
Diese Variante hat den Vorteil, dass sortierte Folgen „erkannt“ werden und die Komplexität im Best-Case <math> \mathcal{O}(n) </math> beträgt. Average- und Worst-Case-Verhalten ändern sich hingegen nicht.<br />
<br />
Außerdem eignet sich Mergesort gut für größere Datenmengen, die nicht mehr im Hauptspeicher gehalten werden können – es müssen jeweils nur beim Verschmelzen in jeder Ebene zwei ''Listen'' vom externen Zwischenspeicher (z.&nbsp;B. Festplatte) gelesen und eine dorthin geschrieben werden. Eine Variante nutzt den verfügbaren Hauptspeicher besser aus (und minimiert Schreib-/Lesezugriffe auf der Festplatte), indem mehr als nur zwei Teil-Listen gleichzeitig vereinigt werden, und damit die Rekursionstiefe abnimmt.<br />
<br />
== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit entwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen beschrieben werden, die Teilen-Phase und die anschließende Misch-Phase. Die erste besteht aus vielen rekursiven Aufrufen, die immer wieder den gleichen Aufteilungsprozess durchführen, bis die Teilsequenzen trivial sortiert sind (mit einem oder keinem Element). Ein intuitiver Ansatz ist es, diese rekursiven Aufrufe zu parallelisieren.<ref name=":2">{{Literatur |Autor=Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford |Titel=Introduction to algorithms |Hrsg= |Sammelwerk= |Band= |Nummer= |Auflage=Third edition |Verlag=MIT Press |Ort=Cambridge, Mass. |Datum=2009 |ISBN=978-0-262-27083-0 |Seiten= |Online=https://www.worldcat.org/oclc/676697295 |Abruf=2020-03-06}}</ref> Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Rekursion unter Verwendung der Schlüsselwörter [[:en:Fork–join_model|fork and join]].<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals der parallelen Ausführung ist.<br />
<br />
=== Mergesort mit paralleler Mischmethode ===<br />
Ein besserer Parallelismus kann durch eine parallele [[Merge-Algorithmen|Mischmethode]] erreicht werden. [[:en:Introduction_to_Algorithms|Cormen et al.]] präsentieren eine binäre Variante, welche zwei sortierte Teilsequenzen in eine sortierte Ausgabesequenz mischt. Eine ausführlichere Beschreibung findet sich [[:en:Merge_algorithm#Parallel_merge|hier]]. <ref name=":2" /><br />
<br />
In der längeren der beiden Sequenzen (falls ungleich lang) wird das Element des mittleren Indexes ausgewählt. Seine Position in der anderen Sequenz wird so bestimmt, dass die Sequenz sortiert bliebe, wenn dieses Element an der bestimmten Stelle eingefügt werden würde. So weiß man, wie viele Elemente insgesamt kleiner sind als das Pivot Element und die finale Position des Pivots kann in der Ausgabesequenz berechnet werden. Für die so erzeugten Teilfolgen der kleineren und größeren Elemente wird die Mischmethode wieder parallel ausgeführt, bis der Basisfall der Rekursion erreicht ist. <br />
<br />
Der folgende Pseudocode illustriert den Mergesort mit modifizierter paralleler Mischmethode (aus Cormen et al.).<br />
<br />
/**<br />
* A: Input array<br />
* B: Output array<br />
* lo: lower bound<br />
* hi: upper bound<br />
* off: offset<br />
*/<br />
'''algorithm''' parallelMergesort(A, lo, hi, B, off) '''is'''<br />
len := hi - lo + 1<br />
'''if''' len == 1 '''then'''<br />
<nowiki> B[off] := A[lo]</nowiki><br />
'''else''' let T[1..len] be a new array<br />
mid := ⌊(lo + hi) / 2⌋ <br />
mid' := mid - lo + 1<br />
'''fork''' parallelMergesort(A, lo, mid, T, 1)<br />
parallelMergesort(A, mid + 1, hi, T, mid' + 1) <br />
'''join''' <br />
parallelMerge(T, 1, mid', mid' + 1, len, B, off)<br />
Um eine Rekurrenzrelation für den Worst Case zu erhalten müssen die rekursiven Aufrufe von parallelMergesort aufgrund der parallelen Ausführung nur einmal aufgeführt werden. Man erhält<br />
<br />
<math display="inline">T_{\infty}^{\text{sort} }(n) = T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + T_{\infty}^{\text{merge}}(n) <br />
= T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + \Theta \left( \log(n)^2\right)</math>.<br />
<br />
Für genauere Informationen über die Komplexität der parallelen Mischmethode, siehe [[:en:Merge_algorithm#Parallel_merge|Merge algorithm]].<br />
<br />
Die Lösung dieser Rekurrenz ist<br />
<br />
<math display="inline">T_{\infty}^{\text{sort}} = \Theta \left ( \log(n)^3 \right)</math>.<br />
<br />
Dieser Algorithmus erreicht eine Parallelisierbarkeit von <math>\Theta \biggr({n \over (\log n)^2}\biggr)</math>, was um einiges besser ist als der Parallelismus des vorherigen Algorithmus. Solch ein Sortieralgorithmus kann, wenn er mit einem schnellen stabilen sequentiellen Sortieralgorithmus und einer sequentiellen Mischmethode als Basisfall für das Mischen von zwei kleinen Sequenzen ausgestattet ist gut in der Praxis funktionieren.<ref>Victor J. Duvanenko "Parallel Merge Sort" Dr. Dobb's Journal & blog[https://duvanenko.tech.blog/2018/01/13/parallel-merge-sort/] and GitHub repo C++ implementation [https://github.com/DragonSpit/ParallelAlgorithms]</ref><br />
<br />
=== Paralleler Mehrwege-Mergesort ===<br />
Es wirkt unnatürlich, Mergesort Algorithmen auf binäre Mischmethoden zu beschränken, da oftmals mehr als zwei Prozessoren zur Verfügung stehen. Ein besserer Ansatz wäre es, ein [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] zu realisieren. Diese Generalisierung mischt im Gegensatz zum binären Mischen <math>k</math> sortierte Sequenzen zu einer sortierten Sequenz. Diese Misch-Variante eignet sich gut zur Beschreibung eines Sortieralgorithmus auf einem [[Parallel Random Access Machine|PRAM]].<ref>Peter Sanders, Johannes Singler. 2008. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg08/singler.pdf</ref><ref name=":02">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
==== Grundidee ====<br />
[[File:Parallel_multiway_mergesort_process.svg|verweis=https://en.wikipedia.org/wiki/File:Parallel_multiway_mergesort_process.svg|alternativtext=|mini|Der parallele Mehrwege-Mergesort Algorithmus auf vier Prozessoren <math>t_0</math> bis <math>t_3</math>.]]<br />
Gegeben sei eine Folge von <math>n</math> Elementen. Ziel ist es, diese Sequenz mit <math>p</math> verfügbaren Prozessoren zu sortieren. Die Elemente sind dabei gleich auf alle Prozessoren aufgeteilt und werden zunächst lokal mit einem sequentiellen [[Sortierverfahren|Sortieralgorithmus]] vorsortiert. Dementsprechend bestehen die Daten nun aus sortierten Folgen <math>S_1, ..., S_p</math> der Länge <math display="inline">\lceil \frac{n}{p} \rceil</math>. Der Einfachheit halber sei <math>n</math> eine Vielfaches von <math>p</math>, so dass für <math>i = 1, ..., p</math> gilt: <math display="inline">\left\vert S_i \right\vert = \frac{n}{p}</math>. Jede dieser Sequenzen wird wiederum in <math>p</math> Teilsequenzen <math>S_{i,1}, ..., S_{i,p}</math> partitioniert, indem für <math>j = 1,..., p</math> die Trennelemente <math>v_j</math> mit globalem Rang <math display="inline">k = j \frac{n}{p}</math> bestimmt werden. Die korrespondierenden Indizes werden in jeder Folge <math>S_i</math> mit binärer Sucher ermittelt, sodass die Folgen anhand der Indizes aufgeteilt werden könne. Formal definiert gilt somit <math>S_{i,j} := \{x \in S_i | rank(v_{j-1}) < rank(x) \le rank(v_j)\}</math>.<br />
<br />
Nun werden die Elemente von <math>S_{1,i}, ..., S_{p,i}</math> dem Prozessor <math>i</math> zugeteilt. Dies sind alle Elemente vom globalen Rang <math display="inline">(i-1) \frac{n}{p}</math> bis zum Rang <math display="inline">i \frac{n}{p}</math>, die über die <math>S_i</math> verteilt sind. So erhält erhält jeder Prozessor eine Folge von sortierten Sequenzen. Aus der Tatsache, dass der Rang <math>k</math> der Trennelemente <math>v_i</math> global gewählt wurde, ergeben sich zwei wichtige Eigenschaften: Zunächst sind die Trennelemente so gewählt, dass jeder Prozessor nach der Zuteilung der neuen Daten immer noch mit <math display="inline">n/p</math> Elementen betraut ist. Der Algorithmus besitzt also eine perfekte [[Lastverteilung (Informatik)|Lastverteilung]]. Außerdem sind alle Elemente des Prozessors <math>i</math> kleiner oder gleich der Elemente des Prozessors <math>i+1</math>. Wenn nun jeder Prozessor ein [[Merge-Algorithmen#k-Wege-Mischen|p-Wege-Mischen]] lokal durchführt, sind aufgrund dieser Eigenschaft die Elemente global sortiert. Somit müssen die Ergebnisse nur in der Reihenfolge der Prozessoren zusammengesetzt werden.<br />
<br />
==== Trennelementbestimmung ====<br />
In der einfachsten Form sind <math>p</math> sortierte Folgen <math>S_1, ..., S_p</math> gleichverteilt auf <math>p</math> Prozessoren sowie ein Rang <math>k</math> gegeben. Gesucht ist nun ein Trennelement <math>x</math> mit globalem Rang <math>k</math> in der Vereinigung der Folgen. Damit kann jede Folge <math>S_i</math> an einem Index <math>l_i</math> in zwei Teile aufgeteilt werden: Der untere Teil besteht nur aus Elementen, die kleiner <math>x</math> sind, während der obere Teil alle Elemente enthält, welche größer oder gleich als <math>x</math> sind. <br />
<br />
Der hier vorgestellte sequentielle Algorithmus gibt die Indizes der Trennungen zurück, also die Indizes <math>l_i</math>in den Folgen <math>S_i</math>, sodass <math>S_i[l_i]</math> einen global kleineren Rang als <math>k</math> hat und <math>\mathrm{rank}\left(S_i[l_i+1]\right) \ge k</math> ist.<ref>Peter Sanders. 2019. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg19/vorlesung.pdf</ref><br />
'''algorithm'''<nowiki> msSelect(S : Array of sorted Sequences [S_1,..,S_p], k : int) </nowiki>'''is'''<br />
'''for''' i = 1 '''to''' p '''do''' <br />
(l_i, r_i) = (0, |S_i|-1)<br />
<br />
'''while''' there exists i: l_i < r_i '''do'''<br />
//pick Pivot Element in S_j[l_j],..,S_j[r_j], chose random j uniformly<br />
v := pickPivot(S, l, r)<br />
'''for''' i = 1 '''to''' p '''do''' <br />
m_i = binarySearch(v, S_i[l_i, r_i]) //sequentially<br />
'''if''' m_1 + ... + m_p >= k '''then''' //m_1+ ... + m_p is the global rank of v<br />
r := m //vector assignment<br />
'''else'''<br />
l := m <br />
<br />
'''return''' l<br />
Für die Komplexitätsanalyse wurde das PRAM-Modell gewählt. Die p-fache Ausführung der ''binarySearch'' Methode hat eine Laufzeit in <math>\mathcal{O}\left(p\log\left(n/p\right)\right)</math>, falls die Daten über alle <math>p</math> Prozessoren gleichverteilt anliegen. Die erwartete Rekursionstiefe beträgt wie im [[Quickselect]] Algorithmus <math>\mathcal{O}\left(\log\left( \textstyle \sum_i |S_i| \right)\right) = \mathcal{O}(\log(n))</math>. Somit ist die gesamte erwartete Laufzeit <math>\mathcal{O}\left(p\log(n/p)\log(n)\right)</math>.<br />
<br />
Angewandt auf den parallelen Mehrwege-Mergesort muss die ''msSelect'' Methode parallel ausgeführt werden, um alle Trennelemente vom Rang <math display="inline"> i \frac n p</math> gleichzeitig zu finden. Dies kann anschließend verwendet werden, um jede Folge in <math>p</math> Teile zu zerschneiden. Es ergibt sich die gleiche Gesamtlaufzeit <math>\mathcal{O}\left(p\, \log(n/p)\log(n)\right)</math>.<br />
<br />
==== Pseudocode ====<br />
Hier ist der komplette Pseudocode für den parallelen Mehrwege-Mergesort. Dabei wird eine Barriere-Synchronisation vor und nach der Trennelementbestimmung angenommen, sodass jeder Prozessor seine Trennelemente und die Partitionierung seiner Sequenz richtig berechnen kann. <br />
/**<br />
* d: Unsorted Array of Elements<br />
* n: Number of Elements<br />
* p: Number of Processors<br />
* return Sorted Array<br />
*/<br />
'''algorithm''' parallelMultiwayMergesort(d : Array, n : int, p : int) '''is'''<br />
o := '''new''' Array[0, n] // the output array<br />
'''for''' i = 1 '''to''' p '''do in parallel''' // each processor in parallel<br />
<nowiki> S_i := d[(i-1) * n/p, i * n/p] // Sequence of length n/p</nowiki><br />
sort(S_i) // sort locally<br />
'''synch'''<br />
v_i := msSelect([S_1,...,S_p], i * n/p) // element with global rank i * n/p<br />
'''synch'''<br />
(S_i,1 ,..., S_i,p) := sequence_partitioning(si, v_1, ..., v_p) // split s_i into subsequences<br />
<br />
o[(i-1) * n/p, i * n/p] := kWayMerge(s_1,i, ..., s_p,i) // merge and assign to output array<br />
<br />
'''return''' o<br />
<br />
==== Analyse ====<br />
Zunächst sortiert jeder Prozessor die zugewiesenen <math>n/p</math> Elemente lokal mit einem vergleichsbasiertem Sortieralgorithmus der Komplexität <math>\mathcal{O}\left( n/p \; \log ( n/p) \right)</math>. Anschließend können die Trennelemente in Zeit <math>\mathcal{O}\left(p \,\log(n/p) \log (n) \right)</math> bestimmt werden. Schließlich müssen jede Gruppe von <math>p</math> Teilstücken gleichzeitig von jedem Prozessor zusammen gemischt werden. Dies hat eine Laufzeit von <math>\mathcal{O}(\log(p)\; n/p )</math>, indem ein sequentieller [[:en:Merge_algorithm|k-Wege Mischalgorithmus]] verwendet wird. Somit ergibt sich eine Gesamtlaufzeit von<br />
<br />
<math>\mathcal{O}\left( \frac n p \log\left(\frac n p\right) + p \log \left( \frac n p\right) \log (n) + \frac n p \log (p) \right)</math>.<br />
<br />
==== Praktische Anpassung und Anwendung ====<br />
Der Mehrwege-Mergesort Algorithmus ist durch seine hohe Parallelität, was den Einsatz vieler Prozessoren ermöglicht, sehr skalierbar. Dies macht den Algorithmus zu einem brauchbaren Kandidaten für das Sortieren großer Datenmengen, wie sie beispielsweise in [[Rechnerverbund|Computer-Clustern]] verarbeitet werden. Da der Speicher in solchen Systemen in der Regel keine limitierende Ressource darstellt, ist der Nachteil der Speicherkomplexität von Mergesort vernachlässigbar. Allerdings werden in solchen Systemen andere Faktoren wichtig, die bei der Modellierung auf einer [[Parallel Random Access Machine|PRAM]] nicht berücksichtigt werden. Hier sind unter anderem die folgenden Aspekte zu berücksichtigen: Die [[Speicherhierarchie]], wenn die Daten nicht in den Cache der Prozessoren passen, oder der Kommunikationsaufwand beim Datenaustausch zwischen den Prozessoren, der zu einem Engpass werden könnte, wenn auf die Daten nicht mehr über den gemeinsamen Speicher zugegriffen werden kann.<br />
<br />
[[Peter Sanders|Sanders]] et al. haben in ihrem Paper einen [[Bulk Synchronous Parallel Computers|bulk synchronous parallel]]-Algorithmus für einen mehrstufigen Mehrwege-Mergesort vorgestellt, der <math>p</math> Prozessoren in <math>r</math> Gruppen der Größe <math>p'</math> unterteilt. Alle Prozessoren sortieren zuerst lokal. Im Gegensatz zu einem einstufigen Mehrwege-Mergesort werden diese Sequenzen dann in <math>r</math> Teile aufgeteilt und den entsprechenden Prozessorgruppen zugeordnet. Diese Schritte werden innerhalb dieser Gruppen rekursiv wiederholt. So wird die Kommunikation reduziert und insbesondere Probleme mit vielen kleinen Nachrichten vermieden. Die hierarchische Struktur des zugrundeliegenden realen Netzwerks (z.B. [[Rack|Racks]], [[Rechnerverbund|Cluster]],...) kann zur Definition der Prozessorgruppen verwendet werden.<ref name=":0">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
=== Weitere Varianten ===<br />
Mergesort war einer der ersten Sortieralgorithmen, bei dem ein optimaler [[Speedup]] erreicht wurde, wobei Richard Cole einen cleveren Subsampling-Algorithmus verwendete, um die O(1)-Zusammenführung sicherzustellen.<ref>{{Literatur |Autor=Richard Cole |Titel=Parallel Merge Sort |Sammelwerk=SIAM Journal on Computing |Band=17 |Nummer=4 |Datum=1988-08 |ISSN=0097-5397 |DOI=10.1137/0217049 |Seiten=770–785 |Online=http://epubs.siam.org/doi/10.1137/0217049 |Abruf=2020-03-06}}</ref> Andere ausgeklügelte parallele Sortieralgorithmen können die gleichen oder bessere Zeitschranken mit einer niedrigeren Konstante erreichen. David Powers beschrieb beispielsweise 1991 einen parallelisierten [[Quicksort]] (und einen verwandten [[Radixsort]]), der durch implizite Partitionierung in <math>O(\log n)</math> Zeit auf einer [[Parallel Random Access Machine|CRCW-Parallel Random Access Machine (PRAM)]] mit <math>n</math> Prozessoren arbeiten kann.<ref>Powers, David M. W. [http://citeseer.ist.psu.edu/327487.html Parallelized Quicksort and Radixsort with Optimal Speedup], ''Proceedings of International Conference on Parallel Computing Technologies''. [[Novosibirsk]]. 1991.</ref> Powers zeigt ferner, dass eine Pipeline-Version von Batchers Bitonic Mergesort in <math>O((\log n)^2)</math> Zeit auf einem Butterfly-Sortiernetzwerk in der Praxis schneller ist als sein <math>O(\log n)</math> Sortieralgorithmus auf einer PRAM, und er bietet eine detaillierte Diskussion der versteckten Overheads beim Vergleich, bei der Radix- und der Parallelsortierung.<ref>David M. W. Powers, [http://david.wardpowers.info/Research/AI/papers/199501-ACAW-PUPC.pdf Parallel Unification: Practical Complexity], Australasian Computer Architecture Workshop, Flinders University, January 1995</ref><br />
<br />
<br />
== Sonstiges ==<br />
Da Mergesort die Startliste sowie alle Zwischenlisten sequenziell abarbeitet, eignet er sich besonders zur Sortierung von [[Liste (Datenstruktur)|verketteten Listen]]. Für [[Feld (Datentyp)|Arrays]] wird normalerweise ein temporäres Array derselben Länge des zu sortierenden Arrays als Zwischenspeicher verwendet (das heißt Mergesort arbeitet normalerweise nicht [[in-place]], s. o.). Quicksort dagegen benötigt kein temporäres Array.<br />
<br />
Die [[Silicon Graphics|SGI]]-Implementierung der [[Standard Template Library|Standard Template Library (STL)]] verwendet den Mergesort als Algorithmus zur stabilen Sortierung.<ref>http://www.sgi.com/tech/stl/stable_sort.html stable_sort</ref><br />
<br />
== Literatur ==<br />
* Robert Sedgewick: ''Algorithmen.'' Pearson Studium, Februar 2002, ISBN 3-8273-7032-9<br />
<br />
== Weblinks ==<br />
* [http://www.hermann-gruber.com/lehre/sorting/Merge/Merge.html Visualisierung und Tutorial für Mergesort, mit Darstellung der Rekursion]<br />
* [http://www.iti.fh-flensburg.de/lang/algorithmen/sortieren/merge/merge.htm Demonstration des Merge-Vorgangs] ([[Java-Applet]])<br />
<br />
== Einzelnachweise ==<br />
<references /><br />
<br />
[[Kategorie:Sortieralgorithmus]]<br />
<br />
[[no:Sorteringsalgoritme#Flettesortering]]</div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Mergesort&diff=197539806Mergesort2020-03-07T14:54:47Z<p>ParAlgMergeSort: /* Paralleler Mehrwege-Mergesort */</p>
<hr />
<div>[[Datei:Merge sort animation2.gif|miniatur|300px|Beispiel, wie Mergesort eine Liste sortiert. Die Listenelemente werden durch Punkte dargestellt. Die waagerechte Achse gibt an, wo sich ein Element in der Liste befindet, die senkrechte Achse gibt an, wie groß ein Element ist.]]<br />
<br />
'''Mergesort''' (von {{enS|''merge''}} ‚verschmelzen‘ und {{lang|en|''sort''}} ‚sortieren‘) ist ein [[Stabiles Sortierverfahren|stabiler]] [[Sortierverfahren|Sortieralgorithmus]], der nach dem Prinzip ''[[Teile und herrsche (Informatik)|teile und herrsche]]'' (divide and conquer) arbeitet. Er wurde erstmals [[1945]] durch [[John von Neumann]] vorgestellt.<ref>{{Literatur | Autor=Donald E. Knuth | Titel=The Art of Computer Programming. Volume 3: Sorting and Searching. | Auflage=2 | Verlag=Addison-Wesley | Ort= | Jahr=1998 | Seiten=158 }}</ref><br />
<br />
== Funktionsweise ==<br />
Mergesort betrachtet die zu sortierenden Daten als Liste und zerlegt sie in kleinere Listen, die jede für sich sortiert werden. Die sortierten kleinen Listen werden dann im Reißverschlussverfahren zu größeren Listen zusammengefügt (engl. {{lang|en|''(to) merge''}}), bis wieder eine sortierte Gesamtliste erreicht ist. Das Verfahren arbeitet bei Arrays in der Regel nicht [[in-place]], es sind dafür aber (trickreiche) Implementierungen bekannt, in welchen die Teil-Arrays üblicherweise rekursiv zusammengeführt werden.<ref>{{Internetquelle|url=https://github.com/h2database/h2database/blob/2cf822269945e25973aa6f1d412f47f7254ce383/h2/src/tools/org/h2/dev/sort/InPlaceStableMergeSort.java|titel=h2database/h2database|werk=GitHub|zugriff=2016-09-01}}</ref> [[Liste (Datenstruktur)|Verkettete Listen]] sind besonders geeignet zur Implementierung von Mergesort, dabei ergibt sich die in-place-Sortierung fast von selbst.<br />
<br />
=== Veranschaulichung der Funktionsweise ===<br />
[[Datei:Mergesort.png|mini|Funktionsweise]]<br />
<br />
Das Bild veranschaulicht die drei wesentlichen Schritte eines [[Teile und herrsche (Informatik)|Teile-und-herrsche]]-Verfahrens, wie sie im Rahmen von Mergesort umgesetzt werden. Der Teile-Schritt ist ersichtlich trivial (die Daten werden einfach in zwei Hälften aufgeteilt). Die wesentliche Arbeit wird beim Verschmelzen (merge) geleistet – daher rührt auch der Name des Algorithmus. Bei [[Quicksort]] ist hingegen der Teile-Schritt aufwendig und der Merge-Schritt einfacher (nämlich eine [[Konkatenation (Listen)|Konkatenierung]]).<br />
<br />
Bei der Betrachtung des in der Grafik dargestellten Verfahrens sollte man sich allerdings bewusst machen, dass es sich hier nur um eine von mehreren [[Rekursion]]sebenen handelt. So könnte etwa die Sortierfunktion, welche die beiden Teile 1 und 2 sortieren soll, zu dem Ergebnis kommen, dass diese Teile immer noch zu groß für die Sortierung sind. Beide Teile würden dann wiederum aufgeteilt und der Sortierfunktion rekursiv übergeben, so dass eine weitere Rekursionsebene geöffnet wird, welche dieselben Schritte abarbeitet. Im Extremfall (der bei Mergesort sogar der Regelfall ist) wird das Aufteilen so weit fortgesetzt, bis die beiden Teile nur noch aus einzelnen Datenelementen bestehen.<br />
<br />
== Implementierung ==<br />
Der folgende [[Pseudocode]] illustriert die Arbeitsweise des [[Algorithmus]], wobei ''liste'' die zu sortierenden Elemente enthält.<br />
<br />
funktion mergesort(liste);<br />
falls (Größe von liste <= 1) dann antworte liste<br />
sonst<br />
halbiere die liste in linkeListe, rechteListe<br />
linkeListe = mergesort(linkeListe)<br />
rechteListe = mergesort(rechteListe)<br />
antworte merge(linkeListe, rechteListe)<br />
<br />
funktion merge(linkeListe, rechteListe);<br />
neueListe<br />
solange (linkeListe und rechteListe nicht leer)<br />
| falls (erstes Element der linkeListe <= erstes Element der rechteListe)<br />
| dann füge erstes Element linkeListe in die neueListe hinten ein und entferne es aus linkeListe<br />
| sonst füge erstes Element rechteListe in die neueListe hinten ein und entferne es aus rechteListe<br />
solange_ende<br />
solange (linkeListe nicht leer)<br />
| füge erstes Element linkeListe in die neueListe hinten ein und entferne es aus linkeListe<br />
solange_ende<br />
solange (rechteListe nicht leer)<br />
| füge erstes Element rechteListe in die neueListe hinten ein und entferne es aus rechteListe<br />
solange_ende<br />
antworte neueListe<br />
<br />
== Beispiel ==<br />
[[Datei:Mergesort example.png|269px|links]]<br />
<br />
Im letzten Verschmelzungsschritt ist das Reißverschlussverfahren beim Verschmelzen (in der Abb. „Mischen:“) angedeutet. Blaue Pfeile verdeutlichen den Aufteilungsschritt, grüne Pfeile die Verschmelzungsschritte.<br />
<br />
Es folgt ein Beispielcode analog zum obigen Abschnitt "Implementierung" für den rekursiven Sortieralgorithmus. Er teilt rekursiv absteigend die Eingabe in 2 kleinere Listen, bis diese trivialerweise sortiert sind, und verschmilzt sie auf dem rekursiven Rückweg, wodurch sie sortiert werden.<br />
'''function''' merge_sort(list ''x'')<br />
<br />
'''if''' length(''x'') ≤ 1 '''then'''<br />
'''return''' ''x'' // Kurzes ''x'' ist trivialerweise sortiert.<br />
<br />
'''var''' ''l'' := empty list<br />
'''var''' ''r'' := empty list<br />
'''var''' ''i'', ''nx'' := length(''x'')−1<br />
// Teile ''x'' in die zwei Hälften ''l'' und ''r'' ...<br />
'''for''' ''i'' := 0 '''to''' floor(''nx''/2) '''do'''<br />
append ''x''[''i''] to ''l''<br />
'''for''' ''i'' := floor(''nx''/2)+1 '''to''' ''nx'' '''do'''<br />
append ''x''[''i''] to ''r''<br />
// ... und sortiere beide (einzeln).<br />
''l'' := merge_sort(''l'')<br />
''r'' := merge_sort(''r'')<br />
// Verschmelze die sortierten Hälften.<br />
'''return''' merge(''l'', ''r'')<br />
Beispielcode zum Verschmelzen zweier sortierter Listen.<br />
'''function''' merge(list ''l'', list ''r'')<br />
'''var''' ''y'' := empty list // Ergebnisliste<br />
<br />
'''var''' ''nl'' := length(''l'')−1<br />
'''var''' ''nr'' := length(''r'')−1<br />
'''var''' ''i'', ''il'' := 0<br />
'''for''' ''i'' := 0 '''to''' ''nl''+''nr+1'' '''do'''<br />
'''if''' ''il'' > ''nl'' '''then'''<br />
append ''r''[''i''−''il''] to ''y''<br />
'''continue'''<br />
'''if''' ''il'' < ''i''−''nr'' '''then'''<br />
append ''l''[''il''] to ''y''<br />
''il'' := ''il''+1<br />
'''continue'''<br />
// Jetzt ist 0 ≤ ''il'' ≤ ''nl'' und 0 ≤ ''i''−''il'' ≤ ''nr''.<br />
'''if''' ''l''[''il''] ≤ ''r''[''i''−''il''] '''then'''<br />
append ''l''[''il''] to ''y''<br />
''il'' := ''il''+1<br />
'''else'''<br />
append ''r''[''i''−''il''] to ''y''<br />
<br />
'''return''' ''y''<br />
<br />
===C++ 11===<br />
Eine Implementierung in der Programmiersprache C++ unter Verwendung von [[Zeigerarithmetik]] könnte folgendermaßen aussehen:<br />
<br />
<syntaxhighlight lang="cpp"><br />
#ifndef ALGORITHM_H<br />
#define ALGORITHM_H<br />
<br />
#include <functional><br />
<br />
namespace ExampleNamespace<br />
{<br />
class Algorithm<br />
{<br />
public:<br />
Algorithm() = delete;<br />
template<typename T><br />
static auto ptrDiff(T const * const begin, T const * const end)<br />
{<br />
return end - begin;<br />
}<br />
template<typename T><br />
static auto midPtr(T* const begin, long long const & ptr_diff)<br />
{<br />
return begin + ptr_diff / 2u;<br />
}<br />
template<typename T><br />
static auto midPtr(T* const begin, T* const end)<br />
{<br />
return midPtr(begin, ptrDiff(begin, end));<br />
}<br />
static auto continueSplit(long long const & ptr_diff)<br />
{<br />
return ptr_diff > 1u;<br />
}<br />
template<typename T><br />
static auto continueSplit(T const * const begin, T const * const end)<br />
{<br />
return continueSplit(ptrDiff(begin, end));<br />
}<br />
template<typename T><br />
static auto mergeSort(T* const begin, T* const end)<br />
{<br />
mergeSort(begin, end, std::less<T>());<br />
}<br />
template<typename T, typename Compare><br />
static auto mergeSort(T* const begin, T* const end, Compare&& comp)<br />
{<br />
auto ptr_diff = ptrDiff(begin, end);<br />
if (ptr_diff) {<br />
auto* temp = new T[ptr_diff];<br />
mergeSort(begin, end, temp, comp);<br />
delete[] temp;<br />
}<br />
}<br />
template<typename T><br />
static auto copyRange(T const * begin, T const * const end, T* dst)<br />
{<br />
copyRangeOverwrite(begin, end, dst);<br />
}<br />
template<typename T><br />
static auto copyRangeOverwrite(T const * begin, T const * const end, T*& dst)<br />
{<br />
while (begin != end) {<br />
*dst++ = *begin++;<br />
}<br />
}<br />
private:<br />
template<typename T, typename Compare><br />
static void mergeSort(T* const begin, T* const end, T* temp, Compare&& comp)<br />
{<br />
auto ptr_diff = ptrDiff(begin, end);<br />
if (continueSplit(ptr_diff)) {<br />
auto * const mid = midPtr(begin, ptr_diff);<br />
mergeSort(begin, mid, temp, comp);<br />
mergeSort(mid, end, temp, comp);<br />
merge(begin, mid, end, temp, comp);<br />
}<br />
}<br />
template<typename T, typename Compare><br />
static auto merge(T* begin, T const * const mid, T const * const end, T* temp, Compare&& comp)<br />
{<br />
copyRange(begin, end, temp);<br />
auto* right_temp = temp + ptrDiff(begin, mid);<br />
auto const * const left_temp_end = right_temp;<br />
auto const * const right_temp_end = temp + ptrDiff(begin, end);<br />
mergeResults(temp, right_temp, right_temp_end, begin, comp);<br />
copyRangeOverwrite(temp, left_temp_end, begin);<br />
copyRangeOverwrite(right_temp, right_temp_end, begin);<br />
}<br />
template<typename T, typename Compare><br />
static auto mergeResults(T*& begin, T*& mid, T const * const end, T*& dst, Compare&& comp)<br />
{<br />
auto const * const mid_ptr = mid;<br />
while (begin != mid_ptr && mid != end) {<br />
*dst++ = comp(*begin, *mid) ? *begin++ : *mid++;<br />
}<br />
}<br />
};<br />
}<br />
<br />
#endif // !ALGORITHM_H<br />
</syntaxhighlight><br />
<br />
== Komplexität ==<br />
Mergesort ist ein stabiles Sortierverfahren, vorausgesetzt der Merge-Schritt ist entsprechend implementiert. Seine [[Komplexität (Informatik)|Komplexität]] beträgt im Worst-, Best- und Average-Case in [[Landau-Symbole|Landau-Notation]] ausgedrückt stets <math> \mathcal{O}(n \cdot \log (n)) </math>.<br />
<br />
Damit ist Mergesort hinsichtlich der Komplexität Quicksort grundsätzlich überlegen, da Quicksort (ohne besondere Vorkehrungen) ein {{nowrap|[[Worst Case|Worst-Case]]-Verhalten}} von <math> \Theta(n^2) </math> besitzt. Es benötigt jedoch zusätzlichen Speicherplatz (der Größenordnung <math>\mathcal{O}(n)</math>), ist also kein In-place-Verfahren.<br />
<br />
Für die Laufzeit <math>T(n)</math> von Mergesort bei <math>n</math> zu sortierenden Elementen gilt die Rekursionsformel<br />
<br />
:<math>T(n)= \underbrace{T(\lfloor \tfrac{n}{2} \rfloor)}_{\text{Aufwand, den einen Teil zu sortieren}} <br />
+ \underbrace{T(\lceil \tfrac{n}{2} \rceil)}_{\text{Aufwand, den anderen Teil zu sortieren}} <br />
+ \underbrace{\mathcal{O}(n)}_{\text{Aufwand, die beiden Teile zu verschmelzen}} </math><br />
<br />
mit dem Rekursionsanfang <math>T(1)=1</math>.<br />
<br />
Nach dem [[Master-Theorem]] kann die Rekursionsformel durch <math>2 \, T(\lfloor \tfrac{n}{2} \rfloor) + n </math> bzw. <math>2 \, T(\lceil \tfrac{n}{2} \rceil) + n </math> approximiert werden mit jeweils der Lösung (2. Fall des Mastertheorems, s. dort) <math>T(n)=\mathcal{O}(n \cdot \log (n)) </math>.<br />
<br />
{| class="wikitable left mw-collapsible mw-collapsed font-size: 105.3%;"<br />
|style="text-align:left; font-size: 95%;"| '''Durchschnittliche und maximale Anzahl Vergleiche''' &nbsp; &nbsp; &nbsp; &nbsp; <br />
|-<br />
|<br />
Sind <math>l_0,l_1</math> die Längen der zu verschmelzenden und vorsortierten Folgen <math>F_0,F_1 ,</math> dann gilt für die Anzahl <math>M</math> der erforderlichen Vergleiche fürs sortierende Verschmelzen<br />
: <math>\min(l_0,l_1) \le M \le l_0+l_1-1 </math>,<br />
da erstens eine Folge komplett vor der anderen liegen kann, d.&nbsp;h., es ist <math>F_0[l_0] \prec F_1[1]</math> bzw. <math>F_1[l_1] \prec F_0[1] , </math> oder es ist zweitens <math>F_0[l_0-1] \prec F_1[l_1] \prec F_0[l_0]</math> (bzw. umgekehrt), sodass die Elemente bis zum letzten Element in jeder Folge verglichen werden müssen. Dabei ist jeweils angenommen, dass das Vergleichen der zwei Folgen bei den Elementen mit niedrigem Index beginnt. Mit<br />
: <math>V_m(l_0+l_1) := l_0+l_1-1</math><br />
(Subskript <math>m </math> für ''maximal'') sei die maximale Anzahl der Vergleiche fürs ''Verschmelzen'' bezeichnet.<br />
Für die maximale Anzahl <math>S_m(n)</math> an Vergleichen für einen ganzen ''Mergesort''-Lauf von <math>n</math> Elementen errechnet sich daraus<br />
: <math>S_m(n) = n l-2^l+1 ,</math> mit <math>l:=\lceil \log_2(n) \rceil .</math><br />
<math>S_m(n) </math> ist die {{OEIS|A001855}}.<br />
<br />
Für eine Gleichverteilung lässt sich auch die durchschnittliche Anzahl <math>V_\varnothing(l_0,l_1)</math> (Subskript <math>{}_\varnothing </math> für ''durchschnittlich'') der Vergleiche genau berechnen, und zwar ist für <math>l_1 = l_0</math><br />
: <math><br />
\begin{align}<br />
V_\varnothing(l_0,l_0) \; \; \; \; \; & = \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1} k\right) \, \bigg/ \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1}\right) \\<br />
& = 2 \, l_0^2/(l_0+1)<br />
\end{align}</math><br />
und für <math>l_1 = l_0-1</math><br />
: <math><br />
\begin{align}<br />
V_\varnothing(l_0,l_0-1) & = \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1} k \; + \sum_{k=l_0-1}^{2 l_0-2} \binom{k-1}{l_0-2} k\right) \\<br />
& \; \bigg/ \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1} \; \; \; + \sum_{k=l_0-1}^{2 l_0-2} \binom{k-1}{l_0-2} \; \; \right) \\<br />
& = 2 \, l_0^2/(l_0+1)-1 .<br />
\end{align}</math><br />
Dabei ist <math>k</math> die Anzahl der Vergleiche für die Anordnung<br />
: <math>\underbrace{u\,v \dotso w\,0}_{k} \, \underbrace{1\,1 \dotso 1}_{l_1+l_0-k}</math> &nbsp; ,<br />
wobei <math>0</math> für das letzte (das am höchsten sortierende) Element in der Folge <math>F_0</math> steht, die (nicht von einem Element aus <math>F_0</math> unterbrochenen) {{nowrap|<math>1</math>en}} zu <math>F_1</math> gehören und <math>u,v,w</math> (die auch fehlen können) entweder zu <math>F_0</math> oder zu <math>F_1</math> gehören. Der in den Summenformeln beigegebene [[Binomialkoeffizient]] zählt die verschiedenen Möglichkeiten für <math>u\,v \dotso w .</math><br />
<br />
Für die durchschnittliche Anzahl <math>S_\varnothing(n)</math> an Vergleichen für einen ganzen Mergesort-Lauf von <math>n</math> Elementen errechnet man daraus<br />
{| class="wikitable" style="text-align:right;"<br />
|-<br />
! <math>n</math> !! <math>S_\varnothing(n)</math> !! <math>S_m(n)</math> !! <math>(S_m(n)-</math><br><math>S_\varnothing(n))/n</math><br />
|-<br />
| <small>2</small>||<small>1,0000</small>||<small>1</small>||<small>0,00000</small><br />
|-<br />
|<small>3</small>||<small>2,6667</small>||<small>3</small>||<small>0,11111</small><br />
|-<br />
|<small>4</small>||<small>4,6667</small>||<small>5</small>||<small>0,08333</small><br />
|-<br />
|<small>6</small>||<small>9,8333</small>||<small>11</small>||<small>0,19444</small><br />
|-<br />
|<small>8</small>||<small>15,733</small>||<small>17</small>||<small>0,15833</small><br />
|-<br />
|<small>12</small>||<small>29,952</small>||<small>33</small>||<small>0,25397</small><br />
|-<br />
|<small>16</small>||<small>45,689</small>||<small>49</small>||<small>0,20694</small><br />
|-<br />
|<small>24</small>||<small>82,059</small>||<small>89</small>||<small>0,28922</small><br />
|-<br />
|<small>32</small>||<small>121,50</small>||<small>129</small>||<small>0,23452</small><br />
|-<br />
|<small>48</small>||<small>210,20</small>||<small>225</small>||<small>0,30839</small><br />
|-<br />
|<small>64</small>||<small>305,05</small>||<small>321</small>||<small>0,24920</small><br />
|-<br />
|<small>96</small>||<small>514,44</small>||<small>545</small>||<small>0,31838</small><br />
|-<br />
|<small>128</small>||<small>736,13</small>||<small>769</small>||<small>0,25677</small><br />
|-<br />
|<small>192</small>||<small>1218,9</small>||<small>1281</small>||<small>0,32348</small><br />
|-<br />
|<small>256</small>||<small>1726,3</small>||<small>1793</small>||<small>0,26061</small><br />
|-<br />
|<small>384</small>||<small>2819,8</small>||<small>2945</small>||<small>0,32606</small><br />
|-<br />
|<small>512</small>||<small>3962,6</small>||<small>4097</small>||<small>0,26255</small><br />
|-<br />
|<small>768</small>||<small>6405,6</small>||<small>6657</small>||<small>0,32736</small><br />
|-<br />
|<small>1024</small>||<small>8947,2</small>||<small>9217</small>||<small>0,26352</small><br />
|-<br />
|<small>1536</small>||<small>14345,0</small>||<small>14849</small>||<small>0,32801</small><br />
|-<br />
|<small>2048</small>||<small>19940,0</small>||<small>20481</small>||<small>0,26401</small><br />
|-<br />
|<small>3072</small>||<small>31760,0</small>||<small>32769</small>||<small>0,32833</small><br />
|-<br />
|<small>4096</small>||<small>43974,0</small>||<small>45057</small>||<small>0,26426</small><br />
|-<br />
|<small>6144</small>||<small>69662,0</small>||<small>71681</small>||<small>0,32849</small><br />
|-<br />
|<small>8192</small>||<small>96139,0</small>||<small>98305</small>||<small>0,26438</small><br />
|-<br />
|<small>12288</small>||<small>1,5161E5</small>||<small>155649</small>||<small>0,32857</small><br />
|-<br />
|<small>16384</small>||<small>2,0866E5</small>||<small>212993</small>||<small>0,26444</small><br />
|-<br />
|<small>24576</small>||<small>3,278E5</small>||<small>335873</small>||<small>0,32862</small><br />
|-<br />
|<small>32768</small>||<small>4,5009E5</small>||<small>458753</small>||<small>0,26447</small><br />
|-<br />
|<small>49152</small>||<small>7,0474E5</small>||<small>720897</small>||<small>0,32864</small><br />
|-<br />
|<small>65536</small>||<small>9,6571E5</small>||<small>983041</small>||<small>0,26448</small><br />
|-<br />
|<small>98304</small>||<small>1,5078E6</small>||<small>1540097</small>||<small>0,32865</small><br />
|-<br />
|<small>131072</small>||<small>2,0625E6</small>||<small>2097153</small>||<small>0,26449</small><br />
|-<br />
|<small>196608</small>||<small>3,2122E6</small>||<small>3276801</small>||<small>0,32865</small><br />
|-<br />
|<small>262144</small>||<small>4,3871E6</small>||<small>4456449</small>||<small>0,26450</small><br />
|-<br />
|<small>393216</small>||<small>6,8176E6</small>||<small>6946817</small>||<small>0,32865</small><br />
|-<br />
|<small>524288</small>||<small>9,2985E6</small>||<small>9437185</small>||<small>0,26450</small><br />
|-<br />
|<small>786432</small>||<small>1,4422E7</small>||<small>14680065</small>||<small>0,32865</small><br />
|-<br />
|<small>1048576</small>||<small>1,9646E7</small>||<small>19922945</small>||<small>0,26450</small><br />
|-<br />
|<small>1572864</small>||<small>3,0416E7</small>||<small>30932993</small>||<small>0,32866</small><br />
|-<br />
|<small>2097152</small>||<small>4,1388E7</small>||<small>41943041</small>||<small>0,26450</small><br />
|-<br />
|<small>3145728</small>||<small>6,3978E7</small>||<small>65011713</small>||<small>0,32866</small><br />
|-<br />
|<small>4194304</small>||<small>8,6971E7</small>||<small>88080385</small>||<small>0,26450</small><br />
|-<br />
|<small>6291456</small>||<small>1,3425E8</small>||<small>136314881</small>||<small>0,32866</small><br />
|-<br />
|<small>8388608</small>||<small>1,8233E8</small>||<small>184549377</small>||<small>0,26450</small><br />
|-<br />
|<small>12582912</small>||<small>2,8108E8</small>||<small>285212673</small>||<small>0,32866</small><br />
|-<br />
|<small>16777216</small>||<small>3,8144E8</small>||<small>385875969</small>||<small>0,26450</small><br />
|}<br />
und findet <math>S_m(n)-0{,}3286560975 \, n \le S_\varnothing(n) \le S_m(n) .</math><br />
|}<br />
<br />
== Korrektheit und Terminierung ==<br />
Der Rekursionsabbruch stellt die [[Terminiertheit|Terminierung]] von Mergesort offensichtlich sicher, so dass lediglich noch die [[Korrektheit (Informatik)|Korrektheit]] gezeigt werden muss. Dies geschieht, indem wir folgende Behauptung beweisen:<br />
<br />
'''Behauptung''': In Rekursionstiefe <math>i</math> werden die sortierten Teillisten aus Rekursionstiefe <math>i{+}1</math> korrekt sortiert.<br />
<br />
'''Beweis''': Sei [[Ohne Beschränkung der Allgemeinheit|o.&nbsp;B.&nbsp;d.&nbsp;A.]] die <math>(i{+}1)</math>-te Rekursion die tiefste. Dann sind die Teillisten offensichtlich sortiert, da sie einelementig sind. Somit ist ein Teil der Behauptung schon mal gesichert. Nun werden diese sortierten Teillisten eine Rekursionsebene nach oben, also in die <math>i</math>-te Rekursion übergeben. Dort werden diese nach Konstruktion der ''merge''-Prozedur von Mergesort korrekt sortiert. Somit ist unsere Behauptung erfüllt und die totale Korrektheit von Mergesort bewiesen.<br />
<br />
== Natural Mergesort ==<br />
'''Natural Mergesort''' ''(natürliches Mergesort)'' ist eine Erweiterung von Mergesort, die<br />
bereits vorsortierte Teilfolgen, so genannte ''runs'', innerhalb der zu sortierenden Startliste ausnutzt. Die Basis für den Mergevorgang bilden hier nicht die rekursiv oder iterativ gewonnenen Zweiergruppen, sondern die in einem ersten Durchgang zu bestimmenden ''runs'':<br />
<br />
Startliste : 3--4--2--1--7--5--8--9--0--6<br />
Runs bestimmen: 3--4 2 1--7 5--8--9 0--6<br />
Merge : 2--3--4 1--5--7--8--9 0--6<br />
Merge : 1--2--3--4--5--7--8--9 0--6<br />
Merge : 0--1--2--3--4--5--6--7--8--9<br />
<br />
Diese Variante hat den Vorteil, dass sortierte Folgen „erkannt“ werden und die Komplexität im Best-Case <math> \mathcal{O}(n) </math> beträgt. Average- und Worst-Case-Verhalten ändern sich hingegen nicht.<br />
<br />
Außerdem eignet sich Mergesort gut für größere Datenmengen, die nicht mehr im Hauptspeicher gehalten werden können – es müssen jeweils nur beim Verschmelzen in jeder Ebene zwei ''Listen'' vom externen Zwischenspeicher (z.&nbsp;B. Festplatte) gelesen und eine dorthin geschrieben werden. Eine Variante nutzt den verfügbaren Hauptspeicher besser aus (und minimiert Schreib-/Lesezugriffe auf der Festplatte), indem mehr als nur zwei Teil-Listen gleichzeitig vereinigt werden, und damit die Rekursionstiefe abnimmt.<br />
<br />
== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit entwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen beschrieben werden, die Teilen-Phase und die anschließende Misch-Phase. Die Erste besteht aus vielen rekursiven Aufrufen, die immer wieder den gleichen Aufteilungsprozess durchführen, bis die Teilsequenzen trivial sortiert sind (mit einem oder keinem Element). Ein intuitiver Ansatz ist es, diese rekursiven Aufrufe zu parallelisieren.<ref name=":2">{{Literatur |Autor=Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford |Titel=Introduction to algorithms |Hrsg= |Sammelwerk= |Band= |Nummer= |Auflage=Third edition |Verlag=MIT Press |Ort=Cambridge, Mass. |Datum=2009 |ISBN=978-0-262-27083-0 |Seiten= |Online=https://www.worldcat.org/oclc/676697295 |Abruf=2020-03-06}}</ref> Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Rekursion unter Verwendung der Schlüsselwörter [[:en:Fork–join_model|fork and join]].<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.<br />
<br />
=== Mergesort mit paralleler Mischmethode ===<br />
Ein besserer Parallelismus kann durch eine parallele [[Merge-Algorithmen|Mischmethode]] erreicht werden. [[:en:Introduction_to_Algorithms|Cormen et al.]] präsentieren eine binäre Variante, welche zwei sortierte Teilsequenzen in eine sortierte Ausgabesequenz mischt. Eine ausführlichere Beschreibung findet sich [[:en:Merge_algorithm#Parallel_merge|hier]]. <ref name=":2" /><br />
<br />
In der längeren der beiden Sequenzen (falls ungleich lang) wird das Element des mittleren Indexes ausgewählt. Seine Position in der anderen Sequenz wird auf die Weise bestimmt, dass die Sequenz sortiert bliebe, wenn dieses Element an bestimmten Stelle eingefügt werden würde. So weiß man wie viele Elemente insgesamt kleiner sind als das Pivot Element und die finale Position des Pivots kann in der Ausgabesequenz berechnet werden. Für die so erzeugten Teilfolgen der kleineren und größeren Elemente wird die Mischmethode wieder parallel ausgeführt, bis der Basisfall der Rekursion erreicht ist. <br />
<br />
Der folgende Pseudocode illustriert die modifizierte parallele Mischmethode (aus Cormen et al.).<br />
<br />
/**<br />
* A: Input array<br />
* B: Output array<br />
* lo: lower bound<br />
* hi: upper bound<br />
* off: offset<br />
*/<br />
'''algorithm''' parallelMergesort(A, lo, hi, B, off) '''is'''<br />
len := hi - lo + 1<br />
'''if''' len == 1 '''then'''<br />
<nowiki> B[off] := A[lo]</nowiki><br />
'''else''' let T[1..len] be a new array<br />
mid := ⌊(lo + hi) / 2⌋ <br />
mid' := mid - lo + 1<br />
'''fork''' parallelMergesort(A, lo, mid, T, 1)<br />
parallelMergesort(A, mid + 1, hi, T, mid' + 1) <br />
'''join''' <br />
parallelMerge(T, 1, mid', mid' + 1, len, B, off)<br />
Um eine Rekurrenzrelation für den Worst Case zu erhalten müssen die rekursiven Aufrufe von parallelMergesort aufgrund der parallelen Ausführung nur einmal aufgeführt werden. Man erhält<br />
<br />
<math display="inline">T_{\infty}^{\text{sort} }(n) = T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + T_{\infty}^{\text{merge}}(n) <br />
= T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + \Theta \left( \log(n)^2\right)</math>.<br />
<br />
Die Lösung dieser Rekurrenz ist<br />
<br />
<math display="inline">T_{\infty}^{\text{sort}} = \Theta \left ( \log(n)^3 \right)</math>.<br />
<br />
Dieser Algorithmus erreicht eine Parallelisierbarkeit von <math>\Theta \biggr({n \over (\log n)^2}\biggr)</math>, was um einiges besser ist als der Parallelismus des vorherigen Algorithmus. Solch ein Sortieralgorithmus kann, wenn er mit einem schnellen stabilen sequentiellen Sortieralgorithmus und einer sequentiellen Mischmethode als Basisfall für das Mischen von zwei kleinen Sequenzen ausgestattet ist gut in der Praxis funktionieren.<ref>Victor J. Duvanenko "Parallel Merge Sort" Dr. Dobb's Journal & blog[https://duvanenko.tech.blog/2018/01/13/parallel-merge-sort/] and GitHub repo C++ implementation [https://github.com/DragonSpit/ParallelAlgorithms]</ref><br />
<br />
=== Paralleler Mehrwege-Mergesort ===<br />
Es wirkt unnatürlich, Mergesort Algorithmen auf binäre Mischmethoden zu beschränken, da oftmals mehr als zwei Prozessoren zur Verfügung stehen. Ein besserer Ansatz wäre es, ein [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] zu realisieren. Diese Generalisierung mischt im Gegensatz zum binären Mischen <math>k</math> sortierte Sequenzen zu einer sortierten Sequenz. Diese Misch-Variante eignet sich gut zur Beschreibung eines Sortieralgorithmus auf einem [[Parallel Random Access Machine|PRAM]].<ref>Peter Sanders, Johannes Singler. 2008. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg08/singler.pdf</ref><ref name=":02">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
==== Grundidee ====<br />
[[File:Parallel_multiway_mergesort_process.svg|verweis=https://en.wikipedia.org/wiki/File:Parallel_multiway_mergesort_process.svg|alternativtext=|mini|Der parallele Mehrwege-Mergesort Algorithmus auf vier Prozessoren <math>t_0</math> bis <math>t_3</math>.]]<br />
Gegeben sei eine Folge von <math>n</math> Elementen. Ziel ist es, diese Sequenz mit <math>p</math> verfügbaren Prozessoren zu sortieren. Die Elemente sind dabei gleich auf alle Prozessoren aufgeteilt und werden zunächst lokal mit einem sequentiellen [[Sortierverfahren|Sortieralgorithmus]] vorsortiert. Dementsprechend bestehen die Daten nun aus sortierten Folgen <math>S_1, ..., S_p</math> der Länge <math display="inline">\lceil \frac{n}{p} \rceil</math>. Der Einfachheit halber sei <math>n</math> eine Vielfaches von <math>p</math>, so dass für <math>i = 1, ..., p</math> gilt: <math display="inline">\left\vert S_i \right\vert = \frac{n}{p}</math>. Jede dieser Sequenzen wird wiederum in <math>p</math> Teilsequenzen <math>S_{i,1}, ..., S_{i,p}</math> partitioniert, indem für <math>i = 1,..., p</math> die Trennelemente <math>v_i</math> mit globalem Rang <math display="inline">k = i \frac{n}{p}</math> bestimmt werden. Die korrespondierenden Indizes werden in jeder Folge <math>S_i</math> mit binärer Sucher ermittelt, sodass die Folgen anhand der Indizes aufgeteilt werden könne. Formal definiert gilt somit <math>S_{i,j} := \{x \in S_i | rank(v_{j-1}) < rank(x) \le rank(v_j)\}</math>.<br />
<br />
Nun werden die Elemente von <math>S_{1,i}, ..., S_{p,i}</math>dem Prozessor <math>i</math> zugeteilt. Dies sind alle Elemente vom globalen Rang <math display="inline">(i-1) \frac{n}{p}</math> bis zum Rang <math display="inline">i \frac{n}{p}</math>, die über die <math>S_i</math> verteilt sind. So erhält erhält jeder Prozessor eine Folge von sortierten Sequenzen. Aus der Tatsache, dass der Rang <math>k</math> der Trennelemente <math>v_i</math> global gewählt wurde, ergeben sich zwei wichtige Eigenschaften: Zunächst sind die Trennelemente so gewählt, dass jeder Prozessor nach der Zuteilung der neuen Daten immer noch mit <math display="inline">n/p</math> Elementen betraut ist. Der Algorithmus besitzt also eine perfekte [[Lastverteilung (Informatik)|Lastverteilung]]. Außerdem sind alle Elemente des Prozessors <math>i</math> kleiner oder gleich der Elemente des Prozessors <math>i+1</math>. Wenn nun jeder Prozessor ein [[Merge-Algorithmen#k-Wege-Mischen|p-Wege-Mischen]] lokal durchführt, sind aufgrund dieser Eigenschaft die Elemente global sortiert. Somit müssen die Ergebnisse nur in der Reihenfolge der Prozessoren zusammengesetzt werden.<br />
<br />
==== Trennelementbestimmung ====<br />
In der einfachsten Form sind <math>p</math> sortierte Folgen <math>S_1, ..., S_p</math> gleichverteilt auf <math>p</math> Prozessoren sowie ein Rang <math>k</math> gegeben. Gesucht ist nun ein Trennelement <math>x</math> mit globalem Rang <math>k</math> in der Vereinigung der Folgen. Damit kann jede Folge <math>S_i</math> an einem Index <math>l_i</math> in zwei Teile aufgeteilt werden: Der untere Teil besteht nur aus Elementen, die kleiner <math>x</math> sind, während der obere Teil alle Elemente enthält, welche größer oder gleich als <math>x</math> sind. <br />
<br />
Der hier vorgestellte sequentielle Algorithmus gibt die Indizes der Trennungen zurück, also die Indizes <math>l_i</math>in den Folgen <math>S_i</math>, sodass <math>S_i[l_i]</math> einen global kleineren Rang als <math>k</math> hat und <math>\mathrm{rank}\left(S_i[l_i+1]\right) \ge k</math> ist.<ref>Peter Sanders. 2019. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg19/vorlesung.pdf</ref><br />
'''algorithm'''<nowiki> msSelect(S : Array of sorted Sequences [S_1,..,S_p], k : int) </nowiki>'''is'''<br />
'''for''' i = 1 '''to''' p '''do''' <br />
(l_i, r_i) = (0, |S_i|-1)<br />
<br />
'''while''' there exists i: l_i < r_i '''do'''<br />
//pick Pivot Element in S_j[l_j],..,S_j[r_j], chose random j uniformly<br />
v := pickPivot(S, l, r)<br />
'''for''' i = 1 '''to''' p '''do''' <br />
m_i = binarySearch(v, S_i[l_i, r_i]) //sequentially<br />
'''if''' m_1 + ... + m_p >= k '''then''' //m_1+ ... + m_p is the global rank of v<br />
r := m //vector assignment<br />
'''else'''<br />
l := m <br />
<br />
'''return''' l<br />
Für die Komplexitätsanalyse wurde das PRAM-Modell gewählt. Die p-fache Ausführung der ''binarySearch'' Methode hat eine Laufzeit in <math>\mathcal{O}\left(p\log\left(n/p\right)\right)</math>, falls die Daten über alle <math>p</math> Prozessoren gleichverteilt anliegen. Die erwartete Rekursionstiefe beträgt wie im [[Quickselect]] Algorithmus <math>\mathcal{O}\left(\log\left( \textstyle \sum_i |S_i| \right)\right) = \mathcal{O}(\log(n))</math>. Somit ist die gesamte erwartete Laufzeit <math>\mathcal{O}\left(p\log(n/p)\log(n)\right)</math>.<br />
<br />
Angewandt auf den parallelen Mehrwege-Mergesort muss die ''msSelect'' Methode parallel ausgeführt werden, um alle Trennelemente vom Rang <math display="inline"> i \frac n p</math> gleichzeitig zu finden. Dies kann anschließend verwendet werden, um jede Folge in <math>p</math> Teile zu zerschneiden. Es ergibt sich die gleiche Gesamtlaufzeit <math>\mathcal{O}\left(p\, \log(n/p)\log(n)\right)</math>.<br />
<br />
==== Pseudocode ====<br />
Hier ist der komplette Pseudocode für den parallelen Mehrwege-Mergesort. Dabei wird eine Barriere-Synchronisation vor und nach der Trennelementbestimmung angenommen, sodass jeder Prozessor seine Trennelemente und die Partitionierung seiner Sequenz richtig berechnen kann. <br />
/**<br />
* d: Unsorted Array of Elements<br />
* n: Number of Elements<br />
* p: Number of Processors<br />
* return Sorted Array<br />
*/<br />
'''algorithm''' parallelMultiwayMergesort(d : Array, n : int, p : int) '''is'''<br />
o := '''new''' Array[0, n] // the output array<br />
'''for''' i = 1 '''to''' p '''do in parallel''' // each processor in parallel<br />
<nowiki> S_i := d[(i-1) * n/p, i * n/p] // Sequence of length n/p</nowiki><br />
sort(S_i) // sort locally<br />
'''synch'''<br />
v_i := msSelect([S_1,...,S_p], i * n/p) // element with global rank i * n/p<br />
'''synch'''<br />
(S_i,1 ,..., S_i,p) := sequence_partitioning(si, v_1, ..., v_p) // split s_i into subsequences<br />
<br />
o[(i-1) * n/p, i * n/p] := kWayMerge(s_1,i, ..., s_p,i) // merge and assign to output array<br />
<br />
'''return''' o<br />
<br />
==== Analyse ====<br />
Zunächst sortiert jeder Prozessor die zugewiesenen <math>n/p</math> Elemente lokal mit einem vergleichsbasiertem Sortieralgorithmus der Komplexität <math>\mathcal{O}\left( n/p \; \log ( n/p) \right)</math>. Anschließend können die Trennelemente in Zeit <math>\mathcal{O}\left(p \,\log(n/p) \log (n) \right)</math> bestimmt werden. Schließlich müssen jede Gruppe von <math>p</math> Teilstücken gleichzeitig von jedem Prozessor zusammen gemischt werden. Dies hat eine Laufzeit von <math>\mathcal{O}(\log(p)\; n/p )</math>, indem ein sequentieller [[:en:Merge_algorithm|k-Wege Mischalgorithmus]] verwendet wird. Somit ergibt sich eine Gesamtlaufzeit von<br />
<br />
<math>\mathcal{O}\left( \frac n p \log\left(\frac n p\right) + p \log \left( \frac n p\right) \log (n) + \frac n p \log (p) \right)</math>.<br />
<br />
==== Praktische Anpassung und Anwendung ====<br />
Der Mehrwege-Mergesort Algorithmus ist durch seine hohe Parallelität, was den Einsatz vieler Prozessoren ermöglicht, sehr skalierbar. Dies macht den Algorithmus zu einem brauchbaren Kandidaten für das Sortieren großer Datenmengen, wie sie beispielsweise in [[Rechnerverbund|Computer-Clustern]] verarbeitet werden. Da der Speicher in solchen Systemen in der Regel keine limitierende Ressource darstellt, ist der Nachteil der Speicherkomplexität von Mergesort vernachlässigbar. Allerdings werden in solchen Systemen andere Faktoren wichtig, die bei der Modellierung auf einer [[Parallel Random Access Machine|PRAM]] nicht berücksichtigt werden. Hier sind unter anderem die folgenden Aspekte zu berücksichtigen: Die [[Speicherhierarchie]], wenn die Daten nicht in den Cache der Prozessoren passen, oder der Kommunikationsaufwand beim Datenaustausch zwischen den Prozessoren, der zu einem Engpass werden könnte, wenn auf die Daten nicht mehr über den gemeinsamen Speicher zugegriffen werden kann.<br />
<br />
[[Peter Sanders|Sanders]] et al. haben in ihrem Paper einen [[Bulk Synchronous Parallel Computers|bulk synchronous parallel]]-Algorithmus für einen mehrstufigen Mehrwege-Mergesort vorgestellt, der <math>p</math> Prozessoren in <math>r</math> Gruppen der Größe <math>p'</math> unterteilt. Alle Prozessoren sortieren zuerst lokal. Im Gegensatz zu einem einstufigen Mehrwege-Mergesort werden diese Sequenzen dann in <math>r</math> Teile aufgeteilt und den entsprechenden Prozessorgruppen zugeordnet. Diese Schritte werden innerhalb dieser Gruppen rekursiv wiederholt. So wird die Kommunikation reduziert und insbesondere Probleme mit vielen kleinen Nachrichten vermieden. Die hierarchische Struktur des zugrundeliegenden realen Netzwerks (z.B. [[Rack|Racks]], [[Rechnerverbund|Cluster]],...) kann zur Definition der Prozessorgruppen verwendet werden.<ref name=":0">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
=== Weitere Varianten ===<br />
Mergesort war einer der ersten Sortieralgorithmen, bei dem ein optimaler [[Speedup]] erreicht wurde, wobei Richard Cole einen cleveren Subsampling-Algorithmus verwendete, um die O(1)-Zusammenführung sicherzustellen.<ref>{{Literatur |Autor=Richard Cole |Titel=Parallel Merge Sort |Sammelwerk=SIAM Journal on Computing |Band=17 |Nummer=4 |Datum=1988-08 |ISSN=0097-5397 |DOI=10.1137/0217049 |Seiten=770–785 |Online=http://epubs.siam.org/doi/10.1137/0217049 |Abruf=2020-03-06}}</ref> Andere ausgeklügelte parallele Sortieralgorithmen können die gleichen oder bessere Zeitschranken mit einer niedrigeren Konstante erreichen. David Powers beschrieb beispielsweise 1991 einen parallelisierten [[Quicksort]] (und einen verwandten [[Radixsort]]), der durch implizite Partitionierung in <math>O(\log n)</math> Zeit auf einer [[Parallel Random Access Machine|CRCW-Parallel Random Access Machine (PRAM)]] mit <math>n</math> Prozessoren arbeiten kann.<ref>Powers, David M. W. [http://citeseer.ist.psu.edu/327487.html Parallelized Quicksort and Radixsort with Optimal Speedup], ''Proceedings of International Conference on Parallel Computing Technologies''. [[Novosibirsk]]. 1991.</ref> Powers zeigt ferner, dass eine Pipeline-Version von Batchers Bitonic Mergesort in <math>O((\log n)^2)</math> Zeit auf einem Butterfly-Sortiernetzwerk in der Praxis schneller ist als sein <math>O(\log n)</math> Sortieralgorithmus auf einer PRAM, und er bietet eine detaillierte Diskussion der versteckten Overheads beim Vergleich, bei der Radix- und der Parallelsortierung.<ref>David M. W. Powers, [http://david.wardpowers.info/Research/AI/papers/199501-ACAW-PUPC.pdf Parallel Unification: Practical Complexity], Australasian Computer Architecture Workshop, Flinders University, January 1995</ref><br />
<br />
<br />
== Sonstiges ==<br />
Da Mergesort die Startliste sowie alle Zwischenlisten sequenziell abarbeitet, eignet er sich besonders zur Sortierung von [[Liste (Datenstruktur)|verketteten Listen]]. Für [[Feld (Datentyp)|Arrays]] wird normalerweise ein temporäres Array derselben Länge des zu sortierenden Arrays als Zwischenspeicher verwendet (das heißt Mergesort arbeitet normalerweise nicht [[in-place]], s. o.). Quicksort dagegen benötigt kein temporäres Array.<br />
<br />
Die [[Silicon Graphics|SGI]]-Implementierung der [[Standard Template Library|Standard Template Library (STL)]] verwendet den Mergesort als Algorithmus zur stabilen Sortierung.<ref>http://www.sgi.com/tech/stl/stable_sort.html stable_sort</ref><br />
<br />
== Literatur ==<br />
* Robert Sedgewick: ''Algorithmen.'' Pearson Studium, Februar 2002, ISBN 3-8273-7032-9<br />
<br />
== Weblinks ==<br />
* [http://www.hermann-gruber.com/lehre/sorting/Merge/Merge.html Visualisierung und Tutorial für Mergesort, mit Darstellung der Rekursion]<br />
* [http://www.iti.fh-flensburg.de/lang/algorithmen/sortieren/merge/merge.htm Demonstration des Merge-Vorgangs] ([[Java-Applet]])<br />
<br />
== Einzelnachweise ==<br />
<references /><br />
<br />
[[Kategorie:Sortieralgorithmus]]<br />
<br />
[[no:Sorteringsalgoritme#Flettesortering]]</div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Mergesort&diff=197539772Mergesort2020-03-07T14:53:37Z<p>ParAlgMergeSort: </p>
<hr />
<div>[[Datei:Merge sort animation2.gif|miniatur|300px|Beispiel, wie Mergesort eine Liste sortiert. Die Listenelemente werden durch Punkte dargestellt. Die waagerechte Achse gibt an, wo sich ein Element in der Liste befindet, die senkrechte Achse gibt an, wie groß ein Element ist.]]<br />
<br />
'''Mergesort''' (von {{enS|''merge''}} ‚verschmelzen‘ und {{lang|en|''sort''}} ‚sortieren‘) ist ein [[Stabiles Sortierverfahren|stabiler]] [[Sortierverfahren|Sortieralgorithmus]], der nach dem Prinzip ''[[Teile und herrsche (Informatik)|teile und herrsche]]'' (divide and conquer) arbeitet. Er wurde erstmals [[1945]] durch [[John von Neumann]] vorgestellt.<ref>{{Literatur | Autor=Donald E. Knuth | Titel=The Art of Computer Programming. Volume 3: Sorting and Searching. | Auflage=2 | Verlag=Addison-Wesley | Ort= | Jahr=1998 | Seiten=158 }}</ref><br />
<br />
== Funktionsweise ==<br />
Mergesort betrachtet die zu sortierenden Daten als Liste und zerlegt sie in kleinere Listen, die jede für sich sortiert werden. Die sortierten kleinen Listen werden dann im Reißverschlussverfahren zu größeren Listen zusammengefügt (engl. {{lang|en|''(to) merge''}}), bis wieder eine sortierte Gesamtliste erreicht ist. Das Verfahren arbeitet bei Arrays in der Regel nicht [[in-place]], es sind dafür aber (trickreiche) Implementierungen bekannt, in welchen die Teil-Arrays üblicherweise rekursiv zusammengeführt werden.<ref>{{Internetquelle|url=https://github.com/h2database/h2database/blob/2cf822269945e25973aa6f1d412f47f7254ce383/h2/src/tools/org/h2/dev/sort/InPlaceStableMergeSort.java|titel=h2database/h2database|werk=GitHub|zugriff=2016-09-01}}</ref> [[Liste (Datenstruktur)|Verkettete Listen]] sind besonders geeignet zur Implementierung von Mergesort, dabei ergibt sich die in-place-Sortierung fast von selbst.<br />
<br />
=== Veranschaulichung der Funktionsweise ===<br />
[[Datei:Mergesort.png|mini|Funktionsweise]]<br />
<br />
Das Bild veranschaulicht die drei wesentlichen Schritte eines [[Teile und herrsche (Informatik)|Teile-und-herrsche]]-Verfahrens, wie sie im Rahmen von Mergesort umgesetzt werden. Der Teile-Schritt ist ersichtlich trivial (die Daten werden einfach in zwei Hälften aufgeteilt). Die wesentliche Arbeit wird beim Verschmelzen (merge) geleistet – daher rührt auch der Name des Algorithmus. Bei [[Quicksort]] ist hingegen der Teile-Schritt aufwendig und der Merge-Schritt einfacher (nämlich eine [[Konkatenation (Listen)|Konkatenierung]]).<br />
<br />
Bei der Betrachtung des in der Grafik dargestellten Verfahrens sollte man sich allerdings bewusst machen, dass es sich hier nur um eine von mehreren [[Rekursion]]sebenen handelt. So könnte etwa die Sortierfunktion, welche die beiden Teile 1 und 2 sortieren soll, zu dem Ergebnis kommen, dass diese Teile immer noch zu groß für die Sortierung sind. Beide Teile würden dann wiederum aufgeteilt und der Sortierfunktion rekursiv übergeben, so dass eine weitere Rekursionsebene geöffnet wird, welche dieselben Schritte abarbeitet. Im Extremfall (der bei Mergesort sogar der Regelfall ist) wird das Aufteilen so weit fortgesetzt, bis die beiden Teile nur noch aus einzelnen Datenelementen bestehen.<br />
<br />
== Implementierung ==<br />
Der folgende [[Pseudocode]] illustriert die Arbeitsweise des [[Algorithmus]], wobei ''liste'' die zu sortierenden Elemente enthält.<br />
<br />
funktion mergesort(liste);<br />
falls (Größe von liste <= 1) dann antworte liste<br />
sonst<br />
halbiere die liste in linkeListe, rechteListe<br />
linkeListe = mergesort(linkeListe)<br />
rechteListe = mergesort(rechteListe)<br />
antworte merge(linkeListe, rechteListe)<br />
<br />
funktion merge(linkeListe, rechteListe);<br />
neueListe<br />
solange (linkeListe und rechteListe nicht leer)<br />
| falls (erstes Element der linkeListe <= erstes Element der rechteListe)<br />
| dann füge erstes Element linkeListe in die neueListe hinten ein und entferne es aus linkeListe<br />
| sonst füge erstes Element rechteListe in die neueListe hinten ein und entferne es aus rechteListe<br />
solange_ende<br />
solange (linkeListe nicht leer)<br />
| füge erstes Element linkeListe in die neueListe hinten ein und entferne es aus linkeListe<br />
solange_ende<br />
solange (rechteListe nicht leer)<br />
| füge erstes Element rechteListe in die neueListe hinten ein und entferne es aus rechteListe<br />
solange_ende<br />
antworte neueListe<br />
<br />
== Beispiel ==<br />
[[Datei:Mergesort example.png|269px|links]]<br />
<br />
Im letzten Verschmelzungsschritt ist das Reißverschlussverfahren beim Verschmelzen (in der Abb. „Mischen:“) angedeutet. Blaue Pfeile verdeutlichen den Aufteilungsschritt, grüne Pfeile die Verschmelzungsschritte.<br />
<br />
Es folgt ein Beispielcode analog zum obigen Abschnitt "Implementierung" für den rekursiven Sortieralgorithmus. Er teilt rekursiv absteigend die Eingabe in 2 kleinere Listen, bis diese trivialerweise sortiert sind, und verschmilzt sie auf dem rekursiven Rückweg, wodurch sie sortiert werden.<br />
'''function''' merge_sort(list ''x'')<br />
<br />
'''if''' length(''x'') ≤ 1 '''then'''<br />
'''return''' ''x'' // Kurzes ''x'' ist trivialerweise sortiert.<br />
<br />
'''var''' ''l'' := empty list<br />
'''var''' ''r'' := empty list<br />
'''var''' ''i'', ''nx'' := length(''x'')−1<br />
// Teile ''x'' in die zwei Hälften ''l'' und ''r'' ...<br />
'''for''' ''i'' := 0 '''to''' floor(''nx''/2) '''do'''<br />
append ''x''[''i''] to ''l''<br />
'''for''' ''i'' := floor(''nx''/2)+1 '''to''' ''nx'' '''do'''<br />
append ''x''[''i''] to ''r''<br />
// ... und sortiere beide (einzeln).<br />
''l'' := merge_sort(''l'')<br />
''r'' := merge_sort(''r'')<br />
// Verschmelze die sortierten Hälften.<br />
'''return''' merge(''l'', ''r'')<br />
Beispielcode zum Verschmelzen zweier sortierter Listen.<br />
'''function''' merge(list ''l'', list ''r'')<br />
'''var''' ''y'' := empty list // Ergebnisliste<br />
<br />
'''var''' ''nl'' := length(''l'')−1<br />
'''var''' ''nr'' := length(''r'')−1<br />
'''var''' ''i'', ''il'' := 0<br />
'''for''' ''i'' := 0 '''to''' ''nl''+''nr+1'' '''do'''<br />
'''if''' ''il'' > ''nl'' '''then'''<br />
append ''r''[''i''−''il''] to ''y''<br />
'''continue'''<br />
'''if''' ''il'' < ''i''−''nr'' '''then'''<br />
append ''l''[''il''] to ''y''<br />
''il'' := ''il''+1<br />
'''continue'''<br />
// Jetzt ist 0 ≤ ''il'' ≤ ''nl'' und 0 ≤ ''i''−''il'' ≤ ''nr''.<br />
'''if''' ''l''[''il''] ≤ ''r''[''i''−''il''] '''then'''<br />
append ''l''[''il''] to ''y''<br />
''il'' := ''il''+1<br />
'''else'''<br />
append ''r''[''i''−''il''] to ''y''<br />
<br />
'''return''' ''y''<br />
<br />
===C++ 11===<br />
Eine Implementierung in der Programmiersprache C++ unter Verwendung von [[Zeigerarithmetik]] könnte folgendermaßen aussehen:<br />
<br />
<syntaxhighlight lang="cpp"><br />
#ifndef ALGORITHM_H<br />
#define ALGORITHM_H<br />
<br />
#include <functional><br />
<br />
namespace ExampleNamespace<br />
{<br />
class Algorithm<br />
{<br />
public:<br />
Algorithm() = delete;<br />
template<typename T><br />
static auto ptrDiff(T const * const begin, T const * const end)<br />
{<br />
return end - begin;<br />
}<br />
template<typename T><br />
static auto midPtr(T* const begin, long long const & ptr_diff)<br />
{<br />
return begin + ptr_diff / 2u;<br />
}<br />
template<typename T><br />
static auto midPtr(T* const begin, T* const end)<br />
{<br />
return midPtr(begin, ptrDiff(begin, end));<br />
}<br />
static auto continueSplit(long long const & ptr_diff)<br />
{<br />
return ptr_diff > 1u;<br />
}<br />
template<typename T><br />
static auto continueSplit(T const * const begin, T const * const end)<br />
{<br />
return continueSplit(ptrDiff(begin, end));<br />
}<br />
template<typename T><br />
static auto mergeSort(T* const begin, T* const end)<br />
{<br />
mergeSort(begin, end, std::less<T>());<br />
}<br />
template<typename T, typename Compare><br />
static auto mergeSort(T* const begin, T* const end, Compare&& comp)<br />
{<br />
auto ptr_diff = ptrDiff(begin, end);<br />
if (ptr_diff) {<br />
auto* temp = new T[ptr_diff];<br />
mergeSort(begin, end, temp, comp);<br />
delete[] temp;<br />
}<br />
}<br />
template<typename T><br />
static auto copyRange(T const * begin, T const * const end, T* dst)<br />
{<br />
copyRangeOverwrite(begin, end, dst);<br />
}<br />
template<typename T><br />
static auto copyRangeOverwrite(T const * begin, T const * const end, T*& dst)<br />
{<br />
while (begin != end) {<br />
*dst++ = *begin++;<br />
}<br />
}<br />
private:<br />
template<typename T, typename Compare><br />
static void mergeSort(T* const begin, T* const end, T* temp, Compare&& comp)<br />
{<br />
auto ptr_diff = ptrDiff(begin, end);<br />
if (continueSplit(ptr_diff)) {<br />
auto * const mid = midPtr(begin, ptr_diff);<br />
mergeSort(begin, mid, temp, comp);<br />
mergeSort(mid, end, temp, comp);<br />
merge(begin, mid, end, temp, comp);<br />
}<br />
}<br />
template<typename T, typename Compare><br />
static auto merge(T* begin, T const * const mid, T const * const end, T* temp, Compare&& comp)<br />
{<br />
copyRange(begin, end, temp);<br />
auto* right_temp = temp + ptrDiff(begin, mid);<br />
auto const * const left_temp_end = right_temp;<br />
auto const * const right_temp_end = temp + ptrDiff(begin, end);<br />
mergeResults(temp, right_temp, right_temp_end, begin, comp);<br />
copyRangeOverwrite(temp, left_temp_end, begin);<br />
copyRangeOverwrite(right_temp, right_temp_end, begin);<br />
}<br />
template<typename T, typename Compare><br />
static auto mergeResults(T*& begin, T*& mid, T const * const end, T*& dst, Compare&& comp)<br />
{<br />
auto const * const mid_ptr = mid;<br />
while (begin != mid_ptr && mid != end) {<br />
*dst++ = comp(*begin, *mid) ? *begin++ : *mid++;<br />
}<br />
}<br />
};<br />
}<br />
<br />
#endif // !ALGORITHM_H<br />
</syntaxhighlight><br />
<br />
== Komplexität ==<br />
Mergesort ist ein stabiles Sortierverfahren, vorausgesetzt der Merge-Schritt ist entsprechend implementiert. Seine [[Komplexität (Informatik)|Komplexität]] beträgt im Worst-, Best- und Average-Case in [[Landau-Symbole|Landau-Notation]] ausgedrückt stets <math> \mathcal{O}(n \cdot \log (n)) </math>.<br />
<br />
Damit ist Mergesort hinsichtlich der Komplexität Quicksort grundsätzlich überlegen, da Quicksort (ohne besondere Vorkehrungen) ein {{nowrap|[[Worst Case|Worst-Case]]-Verhalten}} von <math> \Theta(n^2) </math> besitzt. Es benötigt jedoch zusätzlichen Speicherplatz (der Größenordnung <math>\mathcal{O}(n)</math>), ist also kein In-place-Verfahren.<br />
<br />
Für die Laufzeit <math>T(n)</math> von Mergesort bei <math>n</math> zu sortierenden Elementen gilt die Rekursionsformel<br />
<br />
:<math>T(n)= \underbrace{T(\lfloor \tfrac{n}{2} \rfloor)}_{\text{Aufwand, den einen Teil zu sortieren}} <br />
+ \underbrace{T(\lceil \tfrac{n}{2} \rceil)}_{\text{Aufwand, den anderen Teil zu sortieren}} <br />
+ \underbrace{\mathcal{O}(n)}_{\text{Aufwand, die beiden Teile zu verschmelzen}} </math><br />
<br />
mit dem Rekursionsanfang <math>T(1)=1</math>.<br />
<br />
Nach dem [[Master-Theorem]] kann die Rekursionsformel durch <math>2 \, T(\lfloor \tfrac{n}{2} \rfloor) + n </math> bzw. <math>2 \, T(\lceil \tfrac{n}{2} \rceil) + n </math> approximiert werden mit jeweils der Lösung (2. Fall des Mastertheorems, s. dort) <math>T(n)=\mathcal{O}(n \cdot \log (n)) </math>.<br />
<br />
{| class="wikitable left mw-collapsible mw-collapsed font-size: 105.3%;"<br />
|style="text-align:left; font-size: 95%;"| '''Durchschnittliche und maximale Anzahl Vergleiche''' &nbsp; &nbsp; &nbsp; &nbsp; <br />
|-<br />
|<br />
Sind <math>l_0,l_1</math> die Längen der zu verschmelzenden und vorsortierten Folgen <math>F_0,F_1 ,</math> dann gilt für die Anzahl <math>M</math> der erforderlichen Vergleiche fürs sortierende Verschmelzen<br />
: <math>\min(l_0,l_1) \le M \le l_0+l_1-1 </math>,<br />
da erstens eine Folge komplett vor der anderen liegen kann, d.&nbsp;h., es ist <math>F_0[l_0] \prec F_1[1]</math> bzw. <math>F_1[l_1] \prec F_0[1] , </math> oder es ist zweitens <math>F_0[l_0-1] \prec F_1[l_1] \prec F_0[l_0]</math> (bzw. umgekehrt), sodass die Elemente bis zum letzten Element in jeder Folge verglichen werden müssen. Dabei ist jeweils angenommen, dass das Vergleichen der zwei Folgen bei den Elementen mit niedrigem Index beginnt. Mit<br />
: <math>V_m(l_0+l_1) := l_0+l_1-1</math><br />
(Subskript <math>m </math> für ''maximal'') sei die maximale Anzahl der Vergleiche fürs ''Verschmelzen'' bezeichnet.<br />
Für die maximale Anzahl <math>S_m(n)</math> an Vergleichen für einen ganzen ''Mergesort''-Lauf von <math>n</math> Elementen errechnet sich daraus<br />
: <math>S_m(n) = n l-2^l+1 ,</math> mit <math>l:=\lceil \log_2(n) \rceil .</math><br />
<math>S_m(n) </math> ist die {{OEIS|A001855}}.<br />
<br />
Für eine Gleichverteilung lässt sich auch die durchschnittliche Anzahl <math>V_\varnothing(l_0,l_1)</math> (Subskript <math>{}_\varnothing </math> für ''durchschnittlich'') der Vergleiche genau berechnen, und zwar ist für <math>l_1 = l_0</math><br />
: <math><br />
\begin{align}<br />
V_\varnothing(l_0,l_0) \; \; \; \; \; & = \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1} k\right) \, \bigg/ \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1}\right) \\<br />
& = 2 \, l_0^2/(l_0+1)<br />
\end{align}</math><br />
und für <math>l_1 = l_0-1</math><br />
: <math><br />
\begin{align}<br />
V_\varnothing(l_0,l_0-1) & = \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1} k \; + \sum_{k=l_0-1}^{2 l_0-2} \binom{k-1}{l_0-2} k\right) \\<br />
& \; \bigg/ \left(\sum_{k=l_0}^{2 l_0-1} \binom{k-1}{l_0-1} \; \; \; + \sum_{k=l_0-1}^{2 l_0-2} \binom{k-1}{l_0-2} \; \; \right) \\<br />
& = 2 \, l_0^2/(l_0+1)-1 .<br />
\end{align}</math><br />
Dabei ist <math>k</math> die Anzahl der Vergleiche für die Anordnung<br />
: <math>\underbrace{u\,v \dotso w\,0}_{k} \, \underbrace{1\,1 \dotso 1}_{l_1+l_0-k}</math> &nbsp; ,<br />
wobei <math>0</math> für das letzte (das am höchsten sortierende) Element in der Folge <math>F_0</math> steht, die (nicht von einem Element aus <math>F_0</math> unterbrochenen) {{nowrap|<math>1</math>en}} zu <math>F_1</math> gehören und <math>u,v,w</math> (die auch fehlen können) entweder zu <math>F_0</math> oder zu <math>F_1</math> gehören. Der in den Summenformeln beigegebene [[Binomialkoeffizient]] zählt die verschiedenen Möglichkeiten für <math>u\,v \dotso w .</math><br />
<br />
Für die durchschnittliche Anzahl <math>S_\varnothing(n)</math> an Vergleichen für einen ganzen Mergesort-Lauf von <math>n</math> Elementen errechnet man daraus<br />
{| class="wikitable" style="text-align:right;"<br />
|-<br />
! <math>n</math> !! <math>S_\varnothing(n)</math> !! <math>S_m(n)</math> !! <math>(S_m(n)-</math><br><math>S_\varnothing(n))/n</math><br />
|-<br />
| <small>2</small>||<small>1,0000</small>||<small>1</small>||<small>0,00000</small><br />
|-<br />
|<small>3</small>||<small>2,6667</small>||<small>3</small>||<small>0,11111</small><br />
|-<br />
|<small>4</small>||<small>4,6667</small>||<small>5</small>||<small>0,08333</small><br />
|-<br />
|<small>6</small>||<small>9,8333</small>||<small>11</small>||<small>0,19444</small><br />
|-<br />
|<small>8</small>||<small>15,733</small>||<small>17</small>||<small>0,15833</small><br />
|-<br />
|<small>12</small>||<small>29,952</small>||<small>33</small>||<small>0,25397</small><br />
|-<br />
|<small>16</small>||<small>45,689</small>||<small>49</small>||<small>0,20694</small><br />
|-<br />
|<small>24</small>||<small>82,059</small>||<small>89</small>||<small>0,28922</small><br />
|-<br />
|<small>32</small>||<small>121,50</small>||<small>129</small>||<small>0,23452</small><br />
|-<br />
|<small>48</small>||<small>210,20</small>||<small>225</small>||<small>0,30839</small><br />
|-<br />
|<small>64</small>||<small>305,05</small>||<small>321</small>||<small>0,24920</small><br />
|-<br />
|<small>96</small>||<small>514,44</small>||<small>545</small>||<small>0,31838</small><br />
|-<br />
|<small>128</small>||<small>736,13</small>||<small>769</small>||<small>0,25677</small><br />
|-<br />
|<small>192</small>||<small>1218,9</small>||<small>1281</small>||<small>0,32348</small><br />
|-<br />
|<small>256</small>||<small>1726,3</small>||<small>1793</small>||<small>0,26061</small><br />
|-<br />
|<small>384</small>||<small>2819,8</small>||<small>2945</small>||<small>0,32606</small><br />
|-<br />
|<small>512</small>||<small>3962,6</small>||<small>4097</small>||<small>0,26255</small><br />
|-<br />
|<small>768</small>||<small>6405,6</small>||<small>6657</small>||<small>0,32736</small><br />
|-<br />
|<small>1024</small>||<small>8947,2</small>||<small>9217</small>||<small>0,26352</small><br />
|-<br />
|<small>1536</small>||<small>14345,0</small>||<small>14849</small>||<small>0,32801</small><br />
|-<br />
|<small>2048</small>||<small>19940,0</small>||<small>20481</small>||<small>0,26401</small><br />
|-<br />
|<small>3072</small>||<small>31760,0</small>||<small>32769</small>||<small>0,32833</small><br />
|-<br />
|<small>4096</small>||<small>43974,0</small>||<small>45057</small>||<small>0,26426</small><br />
|-<br />
|<small>6144</small>||<small>69662,0</small>||<small>71681</small>||<small>0,32849</small><br />
|-<br />
|<small>8192</small>||<small>96139,0</small>||<small>98305</small>||<small>0,26438</small><br />
|-<br />
|<small>12288</small>||<small>1,5161E5</small>||<small>155649</small>||<small>0,32857</small><br />
|-<br />
|<small>16384</small>||<small>2,0866E5</small>||<small>212993</small>||<small>0,26444</small><br />
|-<br />
|<small>24576</small>||<small>3,278E5</small>||<small>335873</small>||<small>0,32862</small><br />
|-<br />
|<small>32768</small>||<small>4,5009E5</small>||<small>458753</small>||<small>0,26447</small><br />
|-<br />
|<small>49152</small>||<small>7,0474E5</small>||<small>720897</small>||<small>0,32864</small><br />
|-<br />
|<small>65536</small>||<small>9,6571E5</small>||<small>983041</small>||<small>0,26448</small><br />
|-<br />
|<small>98304</small>||<small>1,5078E6</small>||<small>1540097</small>||<small>0,32865</small><br />
|-<br />
|<small>131072</small>||<small>2,0625E6</small>||<small>2097153</small>||<small>0,26449</small><br />
|-<br />
|<small>196608</small>||<small>3,2122E6</small>||<small>3276801</small>||<small>0,32865</small><br />
|-<br />
|<small>262144</small>||<small>4,3871E6</small>||<small>4456449</small>||<small>0,26450</small><br />
|-<br />
|<small>393216</small>||<small>6,8176E6</small>||<small>6946817</small>||<small>0,32865</small><br />
|-<br />
|<small>524288</small>||<small>9,2985E6</small>||<small>9437185</small>||<small>0,26450</small><br />
|-<br />
|<small>786432</small>||<small>1,4422E7</small>||<small>14680065</small>||<small>0,32865</small><br />
|-<br />
|<small>1048576</small>||<small>1,9646E7</small>||<small>19922945</small>||<small>0,26450</small><br />
|-<br />
|<small>1572864</small>||<small>3,0416E7</small>||<small>30932993</small>||<small>0,32866</small><br />
|-<br />
|<small>2097152</small>||<small>4,1388E7</small>||<small>41943041</small>||<small>0,26450</small><br />
|-<br />
|<small>3145728</small>||<small>6,3978E7</small>||<small>65011713</small>||<small>0,32866</small><br />
|-<br />
|<small>4194304</small>||<small>8,6971E7</small>||<small>88080385</small>||<small>0,26450</small><br />
|-<br />
|<small>6291456</small>||<small>1,3425E8</small>||<small>136314881</small>||<small>0,32866</small><br />
|-<br />
|<small>8388608</small>||<small>1,8233E8</small>||<small>184549377</small>||<small>0,26450</small><br />
|-<br />
|<small>12582912</small>||<small>2,8108E8</small>||<small>285212673</small>||<small>0,32866</small><br />
|-<br />
|<small>16777216</small>||<small>3,8144E8</small>||<small>385875969</small>||<small>0,26450</small><br />
|}<br />
und findet <math>S_m(n)-0{,}3286560975 \, n \le S_\varnothing(n) \le S_m(n) .</math><br />
|}<br />
<br />
== Korrektheit und Terminierung ==<br />
Der Rekursionsabbruch stellt die [[Terminiertheit|Terminierung]] von Mergesort offensichtlich sicher, so dass lediglich noch die [[Korrektheit (Informatik)|Korrektheit]] gezeigt werden muss. Dies geschieht, indem wir folgende Behauptung beweisen:<br />
<br />
'''Behauptung''': In Rekursionstiefe <math>i</math> werden die sortierten Teillisten aus Rekursionstiefe <math>i{+}1</math> korrekt sortiert.<br />
<br />
'''Beweis''': Sei [[Ohne Beschränkung der Allgemeinheit|o.&nbsp;B.&nbsp;d.&nbsp;A.]] die <math>(i{+}1)</math>-te Rekursion die tiefste. Dann sind die Teillisten offensichtlich sortiert, da sie einelementig sind. Somit ist ein Teil der Behauptung schon mal gesichert. Nun werden diese sortierten Teillisten eine Rekursionsebene nach oben, also in die <math>i</math>-te Rekursion übergeben. Dort werden diese nach Konstruktion der ''merge''-Prozedur von Mergesort korrekt sortiert. Somit ist unsere Behauptung erfüllt und die totale Korrektheit von Mergesort bewiesen.<br />
<br />
== Natural Mergesort ==<br />
'''Natural Mergesort''' ''(natürliches Mergesort)'' ist eine Erweiterung von Mergesort, die<br />
bereits vorsortierte Teilfolgen, so genannte ''runs'', innerhalb der zu sortierenden Startliste ausnutzt. Die Basis für den Mergevorgang bilden hier nicht die rekursiv oder iterativ gewonnenen Zweiergruppen, sondern die in einem ersten Durchgang zu bestimmenden ''runs'':<br />
<br />
Startliste : 3--4--2--1--7--5--8--9--0--6<br />
Runs bestimmen: 3--4 2 1--7 5--8--9 0--6<br />
Merge : 2--3--4 1--5--7--8--9 0--6<br />
Merge : 1--2--3--4--5--7--8--9 0--6<br />
Merge : 0--1--2--3--4--5--6--7--8--9<br />
<br />
Diese Variante hat den Vorteil, dass sortierte Folgen „erkannt“ werden und die Komplexität im Best-Case <math> \mathcal{O}(n) </math> beträgt. Average- und Worst-Case-Verhalten ändern sich hingegen nicht.<br />
<br />
Außerdem eignet sich Mergesort gut für größere Datenmengen, die nicht mehr im Hauptspeicher gehalten werden können – es müssen jeweils nur beim Verschmelzen in jeder Ebene zwei ''Listen'' vom externen Zwischenspeicher (z.&nbsp;B. Festplatte) gelesen und eine dorthin geschrieben werden. Eine Variante nutzt den verfügbaren Hauptspeicher besser aus (und minimiert Schreib-/Lesezugriffe auf der Festplatte), indem mehr als nur zwei Teil-Listen gleichzeitig vereinigt werden, und damit die Rekursionstiefe abnimmt.<br />
<br />
== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit entwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen beschrieben werden, die Teilen-Phase und die anschließende Misch-Phase. Die Erste besteht aus vielen rekursiven Aufrufen, die immer wieder den gleichen Aufteilungsprozess durchführen, bis die Teilsequenzen trivial sortiert sind (mit einem oder keinem Element). Ein intuitiver Ansatz ist es, diese rekursiven Aufrufe zu parallelisieren.<ref name=":2">{{Literatur |Autor=Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford |Titel=Introduction to algorithms |Hrsg= |Sammelwerk= |Band= |Nummer= |Auflage=Third edition |Verlag=MIT Press |Ort=Cambridge, Mass. |Datum=2009 |ISBN=978-0-262-27083-0 |Seiten= |Online=https://www.worldcat.org/oclc/676697295 |Abruf=2020-03-06}}</ref> Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Rekursion unter Verwendung der Schlüsselwörter [[:en:Fork–join_model|fork and join]].<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.<br />
<br />
=== Mergesort mit paralleler Mischmethode ===<br />
Ein besserer Parallelismus kann durch eine parallele [[Merge-Algorithmen|Mischmethode]] erreicht werden. [[:en:Introduction_to_Algorithms|Cormen et al.]] präsentieren eine binäre Variante, welche zwei sortierte Teilsequenzen in eine sortierte Ausgabesequenz mischt. Eine ausführlichere Beschreibung findet sich [[:en:Merge_algorithm#Parallel_merge|hier]]. <ref name=":2" /><br />
<br />
In der längeren der beiden Sequenzen (falls ungleich lang) wird das Element des mittleren Indexes ausgewählt. Seine Position in der anderen Sequenz wird auf die Weise bestimmt, dass die Sequenz sortiert bliebe, wenn dieses Element an bestimmten Stelle eingefügt werden würde. So weiß man wie viele Elemente insgesamt kleiner sind als das Pivot Element und die finale Position des Pivots kann in der Ausgabesequenz berechnet werden. Für die so erzeugten Teilfolgen der kleineren und größeren Elemente wird die Mischmethode wieder parallel ausgeführt, bis der Basisfall der Rekursion erreicht ist. <br />
<br />
Der folgende Pseudocode illustriert die modifizierte parallele Mischmethode (aus Cormen et al.).<br />
<br />
/**<br />
* A: Input array<br />
* B: Output array<br />
* lo: lower bound<br />
* hi: upper bound<br />
* off: offset<br />
*/<br />
'''algorithm''' parallelMergesort(A, lo, hi, B, off) '''is'''<br />
len := hi - lo + 1<br />
'''if''' len == 1 '''then'''<br />
<nowiki> B[off] := A[lo]</nowiki><br />
'''else''' let T[1..len] be a new array<br />
mid := ⌊(lo + hi) / 2⌋ <br />
mid' := mid - lo + 1<br />
'''fork''' parallelMergesort(A, lo, mid, T, 1)<br />
parallelMergesort(A, mid + 1, hi, T, mid' + 1) <br />
'''join''' <br />
parallelMerge(T, 1, mid', mid' + 1, len, B, off)<br />
Um eine Rekurrenzrelation für den Worst Case zu erhalten müssen die rekursiven Aufrufe von parallelMergesort aufgrund der parallelen Ausführung nur einmal aufgeführt werden. Man erhält<br />
<br />
<math display="inline">T_{\infty}^{\text{sort} }(n) = T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + T_{\infty}^{\text{merge}}(n) <br />
= T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + \Theta \left( \log(n)^2\right)</math>.<br />
<br />
Die Lösung dieser Rekurrenz ist<br />
<br />
<math display="inline">T_{\infty}^{\text{sort}} = \Theta \left ( \log(n)^3 \right)</math>.<br />
<br />
Dieser Algorithmus erreicht eine Parallelisierbarkeit von <math>\Theta \biggr({n \over (\log n)^2}\biggr)</math>, was um einiges besser ist als der Parallelismus des vorherigen Algorithmus. Solch ein Sortieralgorithmus kann, wenn er mit einem schnellen stabilen sequentiellen Sortieralgorithmus und einer sequentiellen Mischmethode als Basisfall für das Mischen von zwei kleinen Sequenzen ausgestattet ist gut in der Praxis funktionieren.<ref>Victor J. Duvanenko "Parallel Merge Sort" Dr. Dobb's Journal & blog[https://duvanenko.tech.blog/2018/01/13/parallel-merge-sort/] and GitHub repo C++ implementation [https://github.com/DragonSpit/ParallelAlgorithms]</ref><br />
<br />
=== Paralleler Mehrwege-Mergesort ===<br />
Es wirkt unnatürlich, Mergesort Algorithmen auf binäre Mischmethoden zu beschränken, da oftmals mehr als zwei Prozessoren zur Verfügung stehen. Ein besserer Ansatz wäre es, ein [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] zu realisieren. Diese Generalisierung mischt <math>k</math> sortierte Sequenzen zu einer sortierten Sequenz und ist dementsprechend eine Generalisierung des binären Mischens. Diese Misch-Variante eignet sich gut zur Beschreibung eines Sortieralgorithmus auf einem [[Parallel Random Access Machine|PRAM]].<ref>Peter Sanders, Johannes Singler. 2008. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg08/singler.pdf</ref><ref name=":02">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
==== Grundidee ====<br />
[[File:Parallel_multiway_mergesort_process.svg|verweis=https://en.wikipedia.org/wiki/File:Parallel_multiway_mergesort_process.svg|alternativtext=|mini|Der parallele Mehrwege-Mergesort Algorithmus auf vier Prozessoren <math>t_0</math> bis <math>t_3</math>.]]<br />
Gegeben sei eine Folge von <math>n</math> Elementen. Ziel ist es, diese Sequenz mit <math>p</math> verfügbaren Prozessoren zu sortieren. Die Elemente sind dabei gleich auf alle Prozessoren aufgeteilt und werden zunächst lokal mit einem sequentiellen [[Sortierverfahren|Sortieralgorithmus]] vorsortiert. Dementsprechend bestehen die Daten nun aus sortierten Folgen <math>S_1, ..., S_p</math> der Länge <math display="inline">\lceil \frac{n}{p} \rceil</math>. Der Einfachheit halber sei <math>n</math> eine Vielfaches von <math>p</math>, so dass für <math>i = 1, ..., p</math> gilt: <math display="inline">\left\vert S_i \right\vert = \frac{n}{p}</math>. Jede dieser Sequenzen wird wiederum in <math>p</math> Teilsequenzen <math>S_{i,1}, ..., S_{i,p}</math> partitioniert, indem für <math>i = 1,..., p</math> die Trennelemente <math>v_i</math> mit globalem Rang <math display="inline">k = i \frac{n}{p}</math> bestimmt werden. Die korrespondierenden Indizes werden in jeder Folge <math>S_i</math> mit binärer Sucher ermittelt, sodass die Folgen anhand der Indizes aufgeteilt werden könne. Formal definiert gilt somit <math>S_{i,j} := \{x \in S_i | rank(v_{j-1}) < rank(x) \le rank(v_j)\}</math>.<br />
<br />
Nun werden die Elemente von <math>S_{1,i}, ..., S_{p,i}</math>dem Prozessor <math>i</math> zugeteilt. Dies sind alle Elemente vom globalen Rang <math display="inline">(i-1) \frac{n}{p}</math> bis zum Rang <math display="inline">i \frac{n}{p}</math>, die über die <math>S_i</math> verteilt sind. So erhält erhält jeder Prozessor eine Folge von sortierten Sequenzen. Aus der Tatsache, dass der Rang <math>k</math> der Trennelemente <math>v_i</math> global gewählt wurde, ergeben sich zwei wichtige Eigenschaften: Zunächst sind die Trennelemente so gewählt, dass jeder Prozessor nach der Zuteilung der neuen Daten immer noch mit <math display="inline">n/p</math> Elementen betraut ist. Der Algorithmus besitzt also eine perfekte [[Lastverteilung (Informatik)|Lastverteilung]]. Außerdem sind alle Elemente des Prozessors <math>i</math> kleiner oder gleich der Elemente des Prozessors <math>i+1</math>. Wenn nun jeder Prozessor ein [[Merge-Algorithmen#k-Wege-Mischen|p-Wege-Mischen]] lokal durchführt, sind aufgrund dieser Eigenschaft die Elemente global sortiert. Somit müssen die Ergebnisse nur in der Reihenfolge der Prozessoren zusammengesetzt werden.<br />
<br />
==== Trennelementbestimmung ====<br />
In der einfachsten Form sind <math>p</math> sortierte Folgen <math>S_1, ..., S_p</math> gleichverteilt auf <math>p</math> Prozessoren sowie ein Rang <math>k</math> gegeben. Gesucht ist nun ein Trennelement <math>x</math> mit globalem Rang <math>k</math> in der Vereinigung der Folgen. Damit kann jede Folge <math>S_i</math> an einem Index <math>l_i</math> in zwei Teile aufgeteilt werden: Der untere Teil besteht nur aus Elementen, die kleiner <math>x</math> sind, während der obere Teil alle Elemente enthält, welche größer oder gleich als <math>x</math> sind. <br />
<br />
Der hier vorgestellte sequentielle Algorithmus gibt die Indizes der Trennungen zurück, also die Indizes <math>l_i</math>in den Folgen <math>S_i</math>, sodass <math>S_i[l_i]</math> einen global kleineren Rang als <math>k</math> hat und <math>\mathrm{rank}\left(S_i[l_i+1]\right) \ge k</math> ist.<ref>Peter Sanders. 2019. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg19/vorlesung.pdf</ref><br />
'''algorithm'''<nowiki> msSelect(S : Array of sorted Sequences [S_1,..,S_p], k : int) </nowiki>'''is'''<br />
'''for''' i = 1 '''to''' p '''do''' <br />
(l_i, r_i) = (0, |S_i|-1)<br />
<br />
'''while''' there exists i: l_i < r_i '''do'''<br />
//pick Pivot Element in S_j[l_j],..,S_j[r_j], chose random j uniformly<br />
v := pickPivot(S, l, r)<br />
'''for''' i = 1 '''to''' p '''do''' <br />
m_i = binarySearch(v, S_i[l_i, r_i]) //sequentially<br />
'''if''' m_1 + ... + m_p >= k '''then''' //m_1+ ... + m_p is the global rank of v<br />
r := m //vector assignment<br />
'''else'''<br />
l := m <br />
<br />
'''return''' l<br />
Für die Komplexitätsanalyse wurde das PRAM-Modell gewählt. Die p-fache Ausführung der ''binarySearch'' Methode hat eine Laufzeit in <math>\mathcal{O}\left(p\log\left(n/p\right)\right)</math>, falls die Daten über alle <math>p</math> Prozessoren gleichverteilt anliegen. Die erwartete Rekursionstiefe beträgt wie im [[Quickselect]] Algorithmus <math>\mathcal{O}\left(\log\left( \textstyle \sum_i |S_i| \right)\right) = \mathcal{O}(\log(n))</math>. Somit ist die gesamte erwartete Laufzeit <math>\mathcal{O}\left(p\log(n/p)\log(n)\right)</math>.<br />
<br />
Angewandt auf den parallelen Mehrwege-Mergesort muss die ''msSelect'' Methode parallel ausgeführt werden, um alle Trennelemente vom Rang <math display="inline"> i \frac n p</math> gleichzeitig zu finden. Dies kann anschließend verwendet werden, um jede Folge in <math>p</math> Teile zu zerschneiden. Es ergibt sich die gleiche Gesamtlaufzeit <math>\mathcal{O}\left(p\, \log(n/p)\log(n)\right)</math>.<br />
<br />
==== Pseudocode ====<br />
Hier ist der komplette Pseudocode für den parallelen Mehrwege-Mergesort. Dabei wird eine Barriere-Synchronisation vor und nach der Trennelementbestimmung angenommen, sodass jeder Prozessor seine Trennelemente und die Partitionierung seiner Sequenz richtig berechnen kann. <br />
/**<br />
* d: Unsorted Array of Elements<br />
* n: Number of Elements<br />
* p: Number of Processors<br />
* return Sorted Array<br />
*/<br />
'''algorithm''' parallelMultiwayMergesort(d : Array, n : int, p : int) '''is'''<br />
o := '''new''' Array[0, n] // the output array<br />
'''for''' i = 1 '''to''' p '''do in parallel''' // each processor in parallel<br />
<nowiki> S_i := d[(i-1) * n/p, i * n/p] // Sequence of length n/p</nowiki><br />
sort(S_i) // sort locally<br />
'''synch'''<br />
v_i := msSelect([S_1,...,S_p], i * n/p) // element with global rank i * n/p<br />
'''synch'''<br />
(S_i,1 ,..., S_i,p) := sequence_partitioning(si, v_1, ..., v_p) // split s_i into subsequences<br />
<br />
o[(i-1) * n/p, i * n/p] := kWayMerge(s_1,i, ..., s_p,i) // merge and assign to output array<br />
<br />
'''return''' o<br />
<br />
==== Analyse ====<br />
Zunächst sortiert jeder Prozessor die zugewiesenen <math>n/p</math> Elemente lokal mit einem vergleichsbasiertem Sortieralgorithmus der Komplexität <math>\mathcal{O}\left( n/p \; \log ( n/p) \right)</math>. Anschließend können die Trennelemente in Zeit <math>\mathcal{O}\left(p \,\log(n/p) \log (n) \right)</math> bestimmt werden. Schließlich müssen jede Gruppe von <math>p</math> Teilstücken gleichzeitig von jedem Prozessor zusammen gemischt werden. Dies hat eine Laufzeit von <math>\mathcal{O}(\log(p)\; n/p )</math>, indem ein sequentieller [[:en:Merge_algorithm|k-Wege Mischalgorithmus]] verwendet wird. Somit ergibt sich eine Gesamtlaufzeit von<br />
<br />
<math>\mathcal{O}\left( \frac n p \log\left(\frac n p\right) + p \log \left( \frac n p\right) \log (n) + \frac n p \log (p) \right)</math>.<br />
<br />
==== Praktische Anpassung und Anwendung ====<br />
Der Mehrwege-Mergesort Algorithmus ist durch seine hohe Parallelität, was den Einsatz vieler Prozessoren ermöglicht, sehr skalierbar. Dies macht den Algorithmus zu einem brauchbaren Kandidaten für das Sortieren großer Datenmengen, wie sie beispielsweise in [[Rechnerverbund|Computer-Clustern]] verarbeitet werden. Da der Speicher in solchen Systemen in der Regel keine limitierende Ressource darstellt, ist der Nachteil der Speicherkomplexität von Mergesort vernachlässigbar. Allerdings werden in solchen Systemen andere Faktoren wichtig, die bei der Modellierung auf einer [[Parallel Random Access Machine|PRAM]] nicht berücksichtigt werden. Hier sind unter anderem die folgenden Aspekte zu berücksichtigen: Die [[Speicherhierarchie]], wenn die Daten nicht in den Cache der Prozessoren passen, oder der Kommunikationsaufwand beim Datenaustausch zwischen den Prozessoren, der zu einem Engpass werden könnte, wenn auf die Daten nicht mehr über den gemeinsamen Speicher zugegriffen werden kann.<br />
<br />
[[Peter Sanders|Sanders]] et al. haben in ihrem Paper einen [[Bulk Synchronous Parallel Computers|bulk synchronous parallel]]-Algorithmus für einen mehrstufigen Mehrwege-Mergesort vorgestellt, der <math>p</math> Prozessoren in <math>r</math> Gruppen der Größe <math>p'</math> unterteilt. Alle Prozessoren sortieren zuerst lokal. Im Gegensatz zu einem einstufigen Mehrwege-Mergesort werden diese Sequenzen dann in <math>r</math> Teile aufgeteilt und den entsprechenden Prozessorgruppen zugeordnet. Diese Schritte werden innerhalb dieser Gruppen rekursiv wiederholt. So wird die Kommunikation reduziert und insbesondere Probleme mit vielen kleinen Nachrichten vermieden. Die hierarchische Struktur des zugrundeliegenden realen Netzwerks (z.B. [[Rack|Racks]], [[Rechnerverbund|Cluster]],...) kann zur Definition der Prozessorgruppen verwendet werden.<ref name=":0">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
=== Weitere Varianten ===<br />
Mergesort war einer der ersten Sortieralgorithmen, bei dem ein optimaler [[Speedup]] erreicht wurde, wobei Richard Cole einen cleveren Subsampling-Algorithmus verwendete, um die O(1)-Zusammenführung sicherzustellen.<ref>{{Literatur |Autor=Richard Cole |Titel=Parallel Merge Sort |Sammelwerk=SIAM Journal on Computing |Band=17 |Nummer=4 |Datum=1988-08 |ISSN=0097-5397 |DOI=10.1137/0217049 |Seiten=770–785 |Online=http://epubs.siam.org/doi/10.1137/0217049 |Abruf=2020-03-06}}</ref> Andere ausgeklügelte parallele Sortieralgorithmen können die gleichen oder bessere Zeitschranken mit einer niedrigeren Konstante erreichen. David Powers beschrieb beispielsweise 1991 einen parallelisierten [[Quicksort]] (und einen verwandten [[Radixsort]]), der durch implizite Partitionierung in <math>O(\log n)</math> Zeit auf einer [[Parallel Random Access Machine|CRCW-Parallel Random Access Machine (PRAM)]] mit <math>n</math> Prozessoren arbeiten kann.<ref>Powers, David M. W. [http://citeseer.ist.psu.edu/327487.html Parallelized Quicksort and Radixsort with Optimal Speedup], ''Proceedings of International Conference on Parallel Computing Technologies''. [[Novosibirsk]]. 1991.</ref> Powers zeigt ferner, dass eine Pipeline-Version von Batchers Bitonic Mergesort in <math>O((\log n)^2)</math> Zeit auf einem Butterfly-Sortiernetzwerk in der Praxis schneller ist als sein <math>O(\log n)</math> Sortieralgorithmus auf einer PRAM, und er bietet eine detaillierte Diskussion der versteckten Overheads beim Vergleich, bei der Radix- und der Parallelsortierung.<ref>David M. W. Powers, [http://david.wardpowers.info/Research/AI/papers/199501-ACAW-PUPC.pdf Parallel Unification: Practical Complexity], Australasian Computer Architecture Workshop, Flinders University, January 1995</ref><br />
<br />
<br />
== Sonstiges ==<br />
Da Mergesort die Startliste sowie alle Zwischenlisten sequenziell abarbeitet, eignet er sich besonders zur Sortierung von [[Liste (Datenstruktur)|verketteten Listen]]. Für [[Feld (Datentyp)|Arrays]] wird normalerweise ein temporäres Array derselben Länge des zu sortierenden Arrays als Zwischenspeicher verwendet (das heißt Mergesort arbeitet normalerweise nicht [[in-place]], s. o.). Quicksort dagegen benötigt kein temporäres Array.<br />
<br />
Die [[Silicon Graphics|SGI]]-Implementierung der [[Standard Template Library|Standard Template Library (STL)]] verwendet den Mergesort als Algorithmus zur stabilen Sortierung.<ref>http://www.sgi.com/tech/stl/stable_sort.html stable_sort</ref><br />
<br />
== Literatur ==<br />
* Robert Sedgewick: ''Algorithmen.'' Pearson Studium, Februar 2002, ISBN 3-8273-7032-9<br />
<br />
== Weblinks ==<br />
* [http://www.hermann-gruber.com/lehre/sorting/Merge/Merge.html Visualisierung und Tutorial für Mergesort, mit Darstellung der Rekursion]<br />
* [http://www.iti.fh-flensburg.de/lang/algorithmen/sortieren/merge/merge.htm Demonstration des Merge-Vorgangs] ([[Java-Applet]])<br />
<br />
== Einzelnachweise ==<br />
<references /><br />
<br />
[[Kategorie:Sortieralgorithmus]]<br />
<br />
[[no:Sorteringsalgoritme#Flettesortering]]</div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Parallel_merge_sort_deutsch&diff=197539686Benutzer:ParAlgMergeSort/sandbox/Parallel merge sort deutsch2020-03-07T14:50:39Z<p>ParAlgMergeSort: /* Paralleler Mergesort */</p>
<hr />
<div>== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit entwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen beschrieben werden, die Teilen-Phase und die anschließende Misch-Phase. Die Erste besteht aus vielen rekursiven Aufrufen, die immer wieder den gleichen Aufteilungsprozess durchführen, bis die Teilsequenzen trivial sortiert sind (mit einem oder keinem Element). Ein intuitiver Ansatz ist es, diese rekursiven Aufrufe zu parallelisieren.<ref name=":2">{{Literatur |Autor=Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford |Titel=Introduction to algorithms |Hrsg= |Sammelwerk= |Band= |Nummer= |Auflage=Third edition |Verlag=MIT Press |Ort=Cambridge, Mass. |Datum=2009 |ISBN=978-0-262-27083-0 |Seiten= |Online=https://www.worldcat.org/oclc/676697295 |Abruf=2020-03-06}}</ref> Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Rekursion unter Verwendung der Schlüsselwörter [[:en:Fork–join_model|fork and join]].<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.<br />
<br />
=== Mergesort mit paralleler Mischmethode ===<br />
Ein besserer Parallelismus kann durch eine parallele [[Merge-Algorithmen|Mischmethode]] erreicht werden. [[:en:Introduction_to_Algorithms|Cormen et al.]] präsentieren eine binäre Variante, welche zwei sortierte Teilsequenzen in eine sortierte Ausgabesequenz mischt. Eine ausführlichere Beschreibung findet sich [[:en:Merge_algorithm#Parallel_merge|hier]]. <ref name=":2" /><br />
<br />
In der längeren der beiden Sequenzen (falls ungleich lang) wird das Element des mittleren Indexes ausgewählt. Seine Position in der anderen Sequenz wird auf die Weise bestimmt, dass die Sequenz sortiert bliebe, wenn dieses Element an bestimmten Stelle eingefügt werden würde. So weiß man wie viele Elemente insgesamt kleiner sind als das Pivot Element und die finale Position des Pivots kann in der Ausgabesequenz berechnet werden. Für die so erzeugten Teilfolgen der kleineren und größeren Elemente wird die Mischmethode wieder parallel ausgeführt, bis der Basisfall der Rekursion erreicht ist. <br />
<br />
Der folgende Pseudocode illustriert die modifizierte parallele Mischmethode (aus Cormen et al.).<br />
<br />
/**<br />
* A: Input array<br />
* B: Output array<br />
* lo: lower bound<br />
* hi: upper bound<br />
* off: offset<br />
*/<br />
'''algorithm''' parallelMergesort(A, lo, hi, B, off) '''is'''<br />
len := hi - lo + 1<br />
'''if''' len == 1 '''then'''<br />
<nowiki> B[off] := A[lo]</nowiki><br />
'''else''' let T[1..len] be a new array<br />
mid := ⌊(lo + hi) / 2⌋ <br />
mid' := mid - lo + 1<br />
'''fork''' parallelMergesort(A, lo, mid, T, 1)<br />
parallelMergesort(A, mid + 1, hi, T, mid' + 1) <br />
'''join''' <br />
parallelMerge(T, 1, mid', mid' + 1, len, B, off)<br />
Um eine Rekurrenzrelation für den Worst Case zu erhalten müssen die rekursiven Aufrufe von parallelMergesort aufgrund der parallelen Ausführung nur einmal aufgeführt werden. Man erhält<br />
<br />
<math display="inline">T_{\infty}^{\text{sort} }(n) = T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + T_{\infty}^{\text{merge}}(n) <br />
= T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + \Theta \left( \log(n)^2\right)</math>.<br />
<br />
Die Lösung dieser Rekurrenz ist<br />
<br />
<math display="inline">T_{\infty}^{\text{sort}} = \Theta \left ( \log(n)^3 \right)</math>.<br />
<br />
Dieser Algorithmus erreicht eine Parallelisierbarkeit von <math>\Theta \biggr({n \over (\log n)^2}\biggr)</math>, was um einiges besser ist als der Parallelismus des vorherigen Algorithmus. Solch ein Sortieralgorithmus kann, wenn er mit einem schnellen stabilen sequentiellen Sortieralgorithmus und einer sequentiellen Mischmethode als Basisfall für das Mischen von zwei kleinen Sequenzen ausgestattet ist gut in der Praxis funktionieren.<ref>Victor J. Duvanenko "Parallel Merge Sort" Dr. Dobb's Journal & blog[https://duvanenko.tech.blog/2018/01/13/parallel-merge-sort/] and GitHub repo C++ implementation [https://github.com/DragonSpit/ParallelAlgorithms]</ref><br />
<br />
=== Paralleler Mehrwege-Mergesort ===<br />
Es wirkt unnatürlich, Mergesort Algorithmen auf binäre Mischmethoden zu beschränken, da oftmals mehr als zwei Prozessoren zur Verfügung stehen. Ein besserer Ansatz wäre es, ein [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] zu realisieren. Diese Generalisierung mischt <math>k</math> sortierte Sequenzen zu einer sortierten Sequenz und ist dementsprechend eine Generalisierung des binären Mischens. Diese Misch-Variante eignet sich gut zur Beschreibung eines Sortieralgorithmus auf einem [[Parallel Random Access Machine|PRAM]].<ref>Peter Sanders, Johannes Singler. 2008. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg08/singler.pdf</ref><ref name=":02">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
==== Grundidee ====<br />
[[File:Parallel_multiway_mergesort_process.svg|verweis=https://en.wikipedia.org/wiki/File:Parallel_multiway_mergesort_process.svg|alternativtext=|mini|Der parallele Mehrwege-Mergesort Algorithmus auf vier Prozessoren <math>t_0</math> bis <math>t_3</math>.]]<br />
Gegeben sei eine Folge von <math>n</math> Elementen. Ziel ist es, diese Sequenz mit <math>p</math> verfügbaren Prozessoren zu sortieren. Die Elemente sind dabei gleich auf alle Prozessoren aufgeteilt und werden zunächst lokal mit einem sequentiellen [[Sortierverfahren|Sortieralgorithmus]] vorsortiert. Dementsprechend bestehen die Daten nun aus sortierten Folgen <math>S_1, ..., S_p</math> der Länge <math display="inline">\lceil \frac{n}{p} \rceil</math>. Der Einfachheit halber sei <math>n</math> eine Vielfaches von <math>p</math>, so dass für <math>i = 1, ..., p</math> gilt: <math display="inline">\left\vert S_i \right\vert = \frac{n}{p}</math>. Jede dieser Sequenzen wird wiederum in <math>p</math> Teilsequenzen <math>S_{i,1}, ..., S_{i,p}</math> partitioniert, indem für <math>i = 1,..., p</math> die Trennelemente <math>v_i</math> mit globalem Rang <math display="inline">k = i \frac{n}{p}</math> bestimmt werden. Die korrespondierenden Indizes werden in jeder Folge <math>S_i</math> mit binärer Sucher ermittelt, sodass die Folgen anhand der Indizes aufgeteilt werden könne. Formal definiert gilt somit <math>S_{i,j} := \{x \in S_i | rank(v_{j-1}) < rank(x) \le rank(v_j)\}</math>.<br />
<br />
Nun werden die Elemente von <math>S_{1,i}, ..., S_{p,i}</math>dem Prozessor <math>i</math> zugeteilt. Dies sind alle Elemente vom globalen Rang <math display="inline">(i-1) \frac{n}{p}</math> bis zum Rang <math display="inline">i \frac{n}{p}</math>, die über die <math>S_i</math> verteilt sind. So erhält erhält jeder Prozessor eine Folge von sortierten Sequenzen. Aus der Tatsache, dass der Rang <math>k</math> der Trennelemente <math>v_i</math> global gewählt wurde, ergeben sich zwei wichtige Eigenschaften: Zunächst sind die Trennelemente so gewählt, dass jeder Prozessor nach der Zuteilung der neuen Daten immer noch mit <math display="inline">n/p</math> Elementen betraut ist. Der Algorithmus besitzt also eine perfekte [[Lastverteilung (Informatik)|Lastverteilung]]. Außerdem sind alle Elemente des Prozessors <math>i</math> kleiner oder gleich der Elemente des Prozessors <math>i+1</math>. Wenn nun jeder Prozessor ein [[Merge-Algorithmen#k-Wege-Mischen|p-Wege-Mischen]] lokal durchführt, sind aufgrund dieser Eigenschaft die Elemente global sortiert. Somit müssen die Ergebnisse nur in der Reihenfolge der Prozessoren zusammengesetzt werden.<br />
<br />
==== Trennelementbestimmung ====<br />
In der einfachsten Form sind <math>p</math> sortierte Folgen <math>S_1, ..., S_p</math> gleichverteilt auf <math>p</math> Prozessoren sowie ein Rang <math>k</math> gegeben. Gesucht ist nun ein Trennelement <math>x</math> mit globalem Rang <math>k</math> in der Vereinigung der Folgen. Damit kann jede Folge <math>S_i</math> an einem Index <math>l_i</math> in zwei Teile aufgeteilt werden: Der untere Teil besteht nur aus Elementen, die kleiner <math>x</math> sind, während der obere Teil alle Elemente enthält, welche größer oder gleich als <math>x</math> sind. <br />
<br />
Der hier vorgestellte sequentielle Algorithmus gibt die Indizes der Trennungen zurück, also die Indizes <math>l_i</math>in den Folgen <math>S_i</math>, sodass <math>S_i[l_i]</math> einen global kleineren Rang als <math>k</math> hat und <math>\mathrm{rank}\left(S_i[l_i+1]\right) \ge k</math> ist.<ref>Peter Sanders. 2019. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg19/vorlesung.pdf</ref><br />
'''algorithm'''<nowiki> msSelect(S : Array of sorted Sequences [S_1,..,S_p], k : int) </nowiki>'''is'''<br />
'''for''' i = 1 '''to''' p '''do''' <br />
(l_i, r_i) = (0, |S_i|-1)<br />
<br />
'''while''' there exists i: l_i < r_i '''do'''<br />
//pick Pivot Element in S_j[l_j],..,S_j[r_j], chose random j uniformly<br />
v := pickPivot(S, l, r)<br />
'''for''' i = 1 '''to''' p '''do''' <br />
m_i = binarySearch(v, S_i[l_i, r_i]) //sequentially<br />
'''if''' m_1 + ... + m_p >= k '''then''' //m_1+ ... + m_p is the global rank of v<br />
r := m //vector assignment<br />
'''else'''<br />
l := m <br />
<br />
'''return''' l<br />
Für die Komplexitätsanalyse wurde das PRAM-Modell gewählt. Die p-fache Ausführung der ''binarySearch'' Methode hat eine Laufzeit in <math>\mathcal{O}\left(p\log\left(n/p\right)\right)</math>, falls die Daten über alle <math>p</math> Prozessoren gleichverteilt anliegen. Die erwartete Rekursionstiefe beträgt wie im [[Quickselect]] Algorithmus <math>\mathcal{O}\left(\log\left( \textstyle \sum_i |S_i| \right)\right) = \mathcal{O}(\log(n))</math>. Somit ist die gesamte erwartete Laufzeit <math>\mathcal{O}\left(p\log(n/p)\log(n)\right)</math>.<br />
<br />
Angewandt auf den parallelen Mehrwege-Mergesort muss die ''msSelect'' Methode parallel ausgeführt werden, um alle Trennelemente vom Rang <math display="inline"> i \frac n p</math> gleichzeitig zu finden. Dies kann anschließend verwendet werden, um jede Folge in <math>p</math> Teile zu zerschneiden. Es ergibt sich die gleiche Gesamtlaufzeit <math>\mathcal{O}\left(p\, \log(n/p)\log(n)\right)</math>.<br />
<br />
==== Pseudocode ====<br />
Hier ist der komplette Pseudocode für den parallelen Mehrwege-Mergesort. Dabei wird eine Barriere-Synchronisation vor und nach der Trennelementbestimmung angenommen, sodass jeder Prozessor seine Trennelemente und die Partitionierung seiner Sequenz richtig berechnen kann. <br />
/**<br />
* d: Unsorted Array of Elements<br />
* n: Number of Elements<br />
* p: Number of Processors<br />
* return Sorted Array<br />
*/<br />
'''algorithm''' parallelMultiwayMergesort(d : Array, n : int, p : int) '''is'''<br />
o := '''new''' Array[0, n] // the output array<br />
'''for''' i = 1 '''to''' p '''do in parallel''' // each processor in parallel<br />
<nowiki> S_i := d[(i-1) * n/p, i * n/p] // Sequence of length n/p</nowiki><br />
sort(S_i) // sort locally<br />
'''synch'''<br />
v_i := msSelect([S_1,...,S_p], i * n/p) // element with global rank i * n/p<br />
'''synch'''<br />
(S_i,1 ,..., S_i,p) := sequence_partitioning(si, v_1, ..., v_p) // split s_i into subsequences<br />
<br />
o[(i-1) * n/p, i * n/p] := kWayMerge(s_1,i, ..., s_p,i) // merge and assign to output array<br />
<br />
'''return''' o<br />
<br />
==== Analyse ====<br />
Zunächst sortiert jeder Prozessor die zugewiesenen <math>n/p</math> Elemente lokal mit einem vergleichsbasiertem Sortieralgorithmus der Komplexität <math>\mathcal{O}\left( n/p \; \log ( n/p) \right)</math>. Anschließend können die Trennelemente in Zeit <math>\mathcal{O}\left(p \,\log(n/p) \log (n) \right)</math> bestimmt werden. Schließlich müssen jede Gruppe von <math>p</math> Teilstücken gleichzeitig von jedem Prozessor zusammen gemischt werden. Dies hat eine Laufzeit von <math>\mathcal{O}(\log(p)\; n/p )</math>, indem ein sequentieller [[:en:Merge_algorithm|k-Wege Mischalgorithmus]] verwendet wird. Somit ergibt sich eine Gesamtlaufzeit von<br />
<br />
<math>\mathcal{O}\left( \frac n p \log\left(\frac n p\right) + p \log \left( \frac n p\right) \log (n) + \frac n p \log (p) \right)</math>.<br />
<br />
==== Praktische Anpassung und Anwendung ====<br />
Der Mehrwege-Mergesort Algorithmus ist durch seine hohe Parallelität, was den Einsatz vieler Prozessoren ermöglicht, sehr skalierbar. Dies macht den Algorithmus zu einem brauchbaren Kandidaten für das Sortieren großer Datenmengen, wie sie beispielsweise in [[Rechnerverbund|Computer-Clustern]] verarbeitet werden. Da der Speicher in solchen Systemen in der Regel keine limitierende Ressource darstellt, ist der Nachteil der Speicherkomplexität von Mergesort vernachlässigbar. Allerdings werden in solchen Systemen andere Faktoren wichtig, die bei der Modellierung auf einer [[Parallel Random Access Machine|PRAM]] nicht berücksichtigt werden. Hier sind unter anderem die folgenden Aspekte zu berücksichtigen: Die [[Speicherhierarchie]], wenn die Daten nicht in den Cache der Prozessoren passen, oder der Kommunikationsaufwand beim Datenaustausch zwischen den Prozessoren, der zu einem Engpass werden könnte, wenn auf die Daten nicht mehr über den gemeinsamen Speicher zugegriffen werden kann.<br />
<br />
[[Peter Sanders|Sanders]] et al. haben in ihrem Paper einen [[Bulk Synchronous Parallel Computers|bulk synchronous parallel]]-Algorithmus für einen mehrstufigen Mehrwege-Mergesort vorgestellt, der <math>p</math> Prozessoren in <math>r</math> Gruppen der Größe <math>p'</math> unterteilt. Alle Prozessoren sortieren zuerst lokal. Im Gegensatz zu einem einstufigen Mehrwege-Mergesort werden diese Sequenzen dann in <math>r</math> Teile aufgeteilt und den entsprechenden Prozessorgruppen zugeordnet. Diese Schritte werden innerhalb dieser Gruppen rekursiv wiederholt. So wird die Kommunikation reduziert und insbesondere Probleme mit vielen kleinen Nachrichten vermieden. Die hierarchische Struktur des zugrundeliegenden realen Netzwerks (z.B. [[Rack|Racks]], [[Rechnerverbund|Cluster]],...) kann zur Definition der Prozessorgruppen verwendet werden.<ref name=":0">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
=== Weitere Varianten ===<br />
Mergesort war einer der ersten Sortieralgorithmen, bei dem ein optimaler [[Speedup]] erreicht wurde, wobei Richard Cole einen cleveren Subsampling-Algorithmus verwendete, um die O(1)-Zusammenführung sicherzustellen.<ref>{{Literatur |Autor=Richard Cole |Titel=Parallel Merge Sort |Sammelwerk=SIAM Journal on Computing |Band=17 |Nummer=4 |Datum=1988-08 |ISSN=0097-5397 |DOI=10.1137/0217049 |Seiten=770–785 |Online=http://epubs.siam.org/doi/10.1137/0217049 |Abruf=2020-03-06}}</ref> Andere ausgeklügelte parallele Sortieralgorithmen können die gleichen oder bessere Zeitschranken mit einer niedrigeren Konstante erreichen. David Powers beschrieb beispielsweise 1991 einen parallelisierten [[Quicksort]] (und einen verwandten [[Radixsort]]), der durch implizite Partitionierung in <math>O(\log n)</math> Zeit auf einer [[Parallel Random Access Machine|CRCW-Parallel Random Access Machine (PRAM)]] mit <math>n</math> Prozessoren arbeiten kann.<ref>Powers, David M. W. [http://citeseer.ist.psu.edu/327487.html Parallelized Quicksort and Radixsort with Optimal Speedup], ''Proceedings of International Conference on Parallel Computing Technologies''. [[Novosibirsk]]. 1991.</ref> Powers zeigt ferner, dass eine Pipeline-Version von Batchers Bitonic Mergesort in <math>O((\log n)^2)</math> Zeit auf einem Butterfly-Sortiernetzwerk in der Praxis schneller ist als sein <math>O(\log n)</math> Sortieralgorithmus auf einer PRAM, und er bietet eine detaillierte Diskussion der versteckten Overheads beim Vergleich, bei der Radix- und der Parallelsortierung.<ref>David M. W. Powers, [http://david.wardpowers.info/Research/AI/papers/199501-ACAW-PUPC.pdf Parallel Unification: Practical Complexity], Australasian Computer Architecture Workshop, Flinders University, January 1995</ref></div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Parallel_merge_sort_deutsch&diff=197523669Benutzer:ParAlgMergeSort/sandbox/Parallel merge sort deutsch2020-03-06T23:25:13Z<p>ParAlgMergeSort: /* Paralleler Mergesort */</p>
<hr />
<div>== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit entwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen beschrieben werden, die Teilen-Phase und die anschließende Misch-Phase. Die Erste besteht aus vielen rekursiven Aufrufen, die immer wieder den gleichen Aufteilungsprozess durchführen, bis die Teilsequenzen trivial sortiert sind (mit einem oder keinem Element). Ein intuitiver Ansatz ist es, diese rekursiven Aufrufe zu parallelisieren.<ref name=":2">{{Literatur |Autor=Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford |Titel=Introduction to algorithms |Hrsg= |Sammelwerk= |Band= |Nummer= |Auflage=Third edition |Verlag=MIT Press |Ort=Cambridge, Mass. |Datum=2009 |ISBN=978-0-262-27083-0 |Seiten= |Online=https://www.worldcat.org/oclc/676697295 |Abruf=2020-03-06}}</ref> Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Rekursion unter Verwendung der Schlüsselwörter [[:en:Fork–join_model|fork and join]].<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.<br />
<br />
=== Mergesort mit paralleler Mischmethode ===<br />
Ein besserer Parallelismus kann durch eine parallele [[Merge-Algorithmen|Mischmethode]] erreicht werden. [[:en:Introduction_to_Algorithms|Cormen et al.]] präsentieren eine binäre Variante, welche zwei sortierte Teilsequenzen in eine sortierte Ausgabesequenz mischt.<ref name=":2" /><br />
<br />
In der längeren der beiden Sequenzen (falls ungleich lang) wird das Element des mittleren Indexes ausgewählt. Seine Position in der anderen Sequenz wird auf die Weise bestimmt, dass die Sequenz sortiert bliebe, wenn dieses Element an bestimmten Stelle eingefügt werden würde. So weiß man wie viele Elemente insgesamt kleiner sind als das Pivot Element und die finale Position des Pivots kann in der Ausgabesequenz berechnet werden. Für die so erzeugten Teilfolgen der kleineren und größeren Elemente wird die Mischmethode wieder parallel ausgeführt, bis der Basisfall der Rekursion erreicht ist. <br />
<br />
Der folgende Pseudocode illustriert die modifizierte parallele Mischmethode (aus Cormen et al.).<br />
<br />
/**<br />
* A: Input array<br />
* B: Output array<br />
* lo: lower bound<br />
* hi: upper bound<br />
* off: offset<br />
*/<br />
'''algorithm''' parallelMergesort(A, lo, hi, B, off) '''is'''<br />
len := hi - lo + 1<br />
'''if''' len == 1 '''then'''<br />
<nowiki> B[off] := A[lo]</nowiki><br />
'''else''' let T[1..len] be a new array<br />
mid := ⌊(lo + hi) / 2⌋ <br />
mid' := mid - lo + 1<br />
'''fork''' parallelMergesort(A, lo, mid, T, 1)<br />
parallelMergesort(A, mid + 1, hi, T, mid' + 1) <br />
'''join''' <br />
parallelMerge(T, 1, mid', mid' + 1, len, B, off)<br />
Um eine Rekurrenzrelation für den Worst Case zu erhalten müssen die rekursiven Aufrufe von parallelMergesort aufgrund der parallelen Ausführung nur einmal aufgeführt werden. Man erhält<br />
<br />
<math display="inline">T_{\infty}^{\text{sort} }(n) = T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + T_{\infty}^{\text{merge}}(n) <br />
= T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + \Theta \left( \log(n)^2\right)</math>.<br />
<br />
Die Lösung dieser Rekurrenz ist<br />
<br />
<math display="inline">T_{\infty}^{\text{sort}} = \Theta \left ( \log(n)^3 \right)</math>.<br />
<br />
Dieser Algorithmus erreicht eine Parallelisierbarkeit von <math>\Theta \biggr({n \over (\log n)^2}\biggr)</math>, was um einiges besser ist als der Parallelismus des vorherigen Algorithmus. Solch ein Sortieralgorithmus kann, wenn er mit einem schnellen stabilen sequentiellen Sortieralgorithmus und einer sequentiellen Mischmethode als Basisfall für das Mischen von zwei kleinen Sequenzen ausgestattet ist gut in der Praxis funktionieren.<ref>Victor J. Duvanenko "Parallel Merge Sort" Dr. Dobb's Journal & blog[https://duvanenko.tech.blog/2018/01/13/parallel-merge-sort/] and GitHub repo C++ implementation [https://github.com/DragonSpit/ParallelAlgorithms]</ref><br />
<br />
=== Paralleler Mehrwege-Mergesort ===<br />
Es wirkt unnatürlich, Mergesort Algorithmen auf binäre Mischmethoden zu beschränken, da oftmals mehr als zwei Prozessoren zur Verfügung stehen. Ein besserer Ansatz wäre es, ein [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] zu realisieren. Diese Generalisierung mischt <math>k</math> sortierte Sequenzen zu einer sortierten Sequenz und ist dementsprechend eine Generalisierung des binären Mischens. Diese Misch-Variante eignet sich gut zur Beschreibung eines Sortieralgorithmus auf einem [[Parallel Random Access Machine|PRAM]].<ref>Peter Sanders, Johannes Singler. 2008. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg08/singler.pdf</ref><ref name=":02">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
==== Grundidee ====<br />
[[File:Parallel_multiway_mergesort_process.svg|verweis=https://en.wikipedia.org/wiki/File:Parallel_multiway_mergesort_process.svg|alternativtext=|mini|Der parallele Mehrwege-Mergesort Algorithmus auf vier Prozessoren <math>t_0</math> bis <math>t_3</math>.]]<br />
Gegeben sei eine Folge von <math>n</math> Elementen. Ziel ist es, diese Sequenz mit <math>p</math> verfügbaren Prozessoren zu sortieren. Die Elemente sind dabei gleich auf alle Prozessoren aufgeteilt und werden zunächst lokal mit einem sequentiellen [[Sortierverfahren|Sortieralgorithmus]] vorsortiert. Dementsprechend bestehen die Daten nun aus sortierten Folgen <math>S_1, ..., S_p</math> der Länge <math display="inline">\lceil \frac{n}{p} \rceil</math>. Der Einfachheit halber sei <math>n</math> eine Vielfaches von <math>p</math>, so dass für <math>i = 1, ..., p</math> gilt: <math display="inline">\left\vert S_i \right\vert = \frac{n}{p}</math>. Jede dieser Sequenzen wird wiederum in <math>p</math> Teilsequenzen <math>S_{i,1}, ..., S_{i,p}</math> partitioniert, indem für <math>i = 1,..., p</math> die Trennelemente <math>v_i</math> mit globalem Rang <math display="inline">k = i \frac{n}{p}</math> bestimmt werden. Die korrespondierenden Indizes werden in jeder Folge <math>S_i</math> mit binärer Sucher ermittelt, sodass die Folgen anhand der Indizes aufgeteilt werden könne. Formal definiert gilt somit <math>S_{i,j} := \{x \in S_i | rank(v_{j-1}) < rank(x) \le rank(v_j)\}</math>.<br />
<br />
Nun werden die Elemente von <math>S_{1,i}, ..., S_{p,i}</math>dem Prozessor <math>i</math> zugeteilt. Dies sind alle Elemente vom globalen Rang <math display="inline">(i-1) \frac{n}{p}</math> bis zum Rang <math display="inline">i \frac{n}{p}</math>, die über die <math>S_i</math> verteilt sind. So erhält erhält jeder Prozessor eine Folge von sortierten Sequenzen. Aus der Tatsache, dass der Rang <math>k</math> der Trennelemente <math>v_i</math> global gewählt wurde, ergeben sich zwei wichtige Eigenschaften: Zunächst sind die Trennelemente so gewählt, dass jeder Prozessor nach der Zuteilung der neuen Daten immer noch mit <math display="inline">n/p</math> Elementen betraut ist. Der Algorithmus besitzt also eine perfekte [[Lastverteilung (Informatik)|Lastverteilung]]. Außerdem sind alle Elemente des Prozessors <math>i</math> kleiner oder gleich der Elemente des Prozessors <math>i+1</math>. Wenn nun jeder Prozessor ein [[Merge-Algorithmen#k-Wege-Mischen|p-Wege-Mischen]] lokal durchführt, sind aufgrund dieser Eigenschaft die Elemente global sortiert. Somit müssen die Ergebnisse nur in der Reihenfolge der Prozessoren zusammengesetzt werden.<br />
<br />
==== Trennelementbestimmung ====<br />
In der einfachsten Form sind <math>p</math> sortierte Folgen <math>S_1, ..., S_p</math> gleichverteilt auf <math>p</math> Prozessoren sowie ein Rang <math>k</math> gegeben. Gesucht ist nun ein Trennelement <math>x</math> mit globalem Rang <math>k</math> in der Vereinigung der Folgen. Damit kann jede Folge <math>S_i</math> an einem Index <math>l_i</math> in zwei Teile aufgeteilt werden: Der untere Teil besteht nur aus Elementen, die kleiner <math>x</math> sind, während der obere Teil alle Elemente enthält, welche größer oder gleich als <math>x</math> sind. <br />
<br />
Der hier vorgestellte sequentielle Algorithmus gibt die Indizes der Trennungen zurück, also die Indizes <math>l_i</math>in den Folgen <math>S_i</math>, sodass <math>S_i[l_i]</math> einen global kleineren Rang als <math>k</math> hat und <math>\mathrm{rank}\left(S_i[l_i+1]\right) \ge k</math> ist.<ref>Peter Sanders. 2019. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg19/vorlesung.pdf</ref><br />
'''algorithm'''<nowiki> msSelect(S : Array of sorted Sequences [S_1,..,S_p], k : int) </nowiki>'''is'''<br />
'''for''' i = 1 '''to''' p '''do''' <br />
(l_i, r_i) = (0, |S_i|-1)<br />
<br />
'''while''' there exists i: l_i < r_i '''do'''<br />
//pick Pivot Element in S_j[l_j],..,S_j[r_j], chose random j uniformly<br />
v := pickPivot(S, l, r)<br />
'''for''' i = 1 '''to''' p '''do''' <br />
m_i = binarySearch(v, S_i[l_i, r_i]) //sequentially<br />
'''if''' m_1 + ... + m_p >= k '''then''' //m_1+ ... + m_p is the global rank of v<br />
r := m //vector assignment<br />
'''else'''<br />
l := m <br />
<br />
'''return''' l<br />
Für die Komplexitätsanalyse wurde das PRAM-Modell gewählt. Die p-fache Ausführung der ''binarySearch'' Methode hat eine Laufzeit in <math>\mathcal{O}\left(p\log\left(n/p\right)\right)</math>, falls die Daten über alle <math>p</math> Prozessoren gleichverteilt anliegen. Die erwartete Rekursionstiefe beträgt wie im [[Quickselect]] Algorithmus <math>\mathcal{O}\left(\log\left( \textstyle \sum_i |S_i| \right)\right) = \mathcal{O}(\log(n))</math>. Somit ist die gesamte erwartete Laufzeit <math>\mathcal{O}\left(p\log(n/p)\log(n)\right)</math>.<br />
<br />
Angewandt auf den parallelen Mehrwege-Mergesort muss die ''msSelect'' Methode parallel ausgeführt werden, um alle Trennelemente vom Rang <math display="inline"> i \frac n p</math> gleichzeitig zu finden. Dies kann anschließend verwendet werden, um jede Folge in <math>p</math> Teile zu zerschneiden. Es ergibt sich die gleiche Gesamtlaufzeit <math>\mathcal{O}\left(p\, \log(n/p)\log(n)\right)</math>.<br />
<br />
==== Pseudocode ====<br />
Hier ist der komplette Pseudocode für den parallelen Mehrwege-Mergesort. Dabei wird eine Barriere-Synchronisation vor und nach der Trennelementbestimmung angenommen, sodass jeder Prozessor seine Trennelemente und die Partitionierung seiner Sequenz richtig berechnen kann. <br />
/**<br />
* d: Unsorted Array of Elements<br />
* n: Number of Elements<br />
* p: Number of Processors<br />
* return Sorted Array<br />
*/<br />
'''algorithm''' parallelMultiwayMergesort(d : Array, n : int, p : int) '''is'''<br />
o := '''new''' Array[0, n] // the output array<br />
'''for''' i = 1 '''to''' p '''do in parallel''' // each processor in parallel<br />
<nowiki> S_i := d[(i-1) * n/p, i * n/p] // Sequence of length n/p</nowiki><br />
sort(S_i) // sort locally<br />
'''synch'''<br />
v_i := msSelect([S_1,...,S_p], i * n/p) // element with global rank i * n/p<br />
'''synch'''<br />
(S_i,1 ,..., S_i,p) := sequence_partitioning(si, v_1, ..., v_p) // split s_i into subsequences<br />
<br />
o[(i-1) * n/p, i * n/p] := kWayMerge(s_1,i, ..., s_p,i) // merge and assign to output array<br />
<br />
'''return''' o<br />
<br />
==== Analyse ====<br />
Zunächst sortiert jeder Prozessor die zugewiesenen <math>n/p</math> Elemente lokal mit einem vergleichsbasiertem Sortieralgorithmus der Komplexität <math>\mathcal{O}\left( n/p \; \log ( n/p) \right)</math>. Anschließend können die Trennelemente in Zeit <math>\mathcal{O}\left(p \,\log(n/p) \log (n) \right)</math> bestimmt werden. Schließlich müssen jede Gruppe von <math>p</math> Teilstücken gleichzeitig von jedem Prozessor zusammen gemischt werden. Dies hat eine Laufzeit von <math>\mathcal{O}(\log(p)\; n/p )</math>, indem ein sequentieller [[:en:Merge_algorithm|k-Wege Mischalgorithmus]] verwendet wird. Somit ergibt sich eine Gesamtlaufzeit von<br />
<br />
<math>\mathcal{O}\left( \frac n p \log\left(\frac n p\right) + p \log \left( \frac n p\right) \log (n) + \frac n p \log (p) \right)</math>.<br />
<br />
==== Praktische Anpassung und Anwendung ====<br />
Der Mehrwege-Mergesort Algorithmus ist durch seine hohe Parallelität, was den Einsatz vieler Prozessoren ermöglicht, sehr skalierbar. Dies macht den Algorithmus zu einem brauchbaren Kandidaten für das Sortieren großer Datenmengen, wie sie beispielsweise in [[Rechnerverbund|Computer-Clustern]] verarbeitet werden. Da der Speicher in solchen Systemen in der Regel keine limitierende Ressource darstellt, ist der Nachteil der Speicherkomplexität von Mergesort vernachlässigbar. Allerdings werden in solchen Systemen andere Faktoren wichtig, die bei der Modellierung auf einer [[Parallel Random Access Machine|PRAM]] nicht berücksichtigt werden. Hier sind unter anderem die folgenden Aspekte zu berücksichtigen: Die [[Speicherhierarchie]], wenn die Daten nicht in den Cache der Prozessoren passen, oder der Kommunikationsaufwand beim Datenaustausch zwischen den Prozessoren, der zu einem Engpass werden könnte, wenn auf die Daten nicht mehr über den gemeinsamen Speicher zugegriffen werden kann.<ref name=":0">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
[[Peter Sanders|Sanders]] et al. haben in ihrem Paper einen [[Bulk Synchronous Parallel Computers|bulk synchronous parallel]]-Algorithmus für einen mehrstufigen Mehrwege-Mergesort vorgestellt, der <math>p</math> Prozessoren in <math>r</math> Gruppen der Größe <math>p'</math> unterteilt. Alle Prozessoren sortieren zuerst lokal. Im Gegensatz zu einem einstufigen Mehrwege-Mergesort werden diese Sequenzen dann in <math>r</math> Teile aufgeteilt und den entsprechenden Prozessorgruppen zugeordnet. Diese Schritte werden innerhalb dieser Gruppen rekursiv wiederholt. So wird die Kommunikation reduziert und insbesondere Probleme mit vielen kleinen Nachrichten vermieden. Die hierarchische Struktur des zugrundeliegenden realen Netzwerks (z.B. [[Rack|Racks]], [[Rechnerverbund|Cluster]],...) kann zur Definition der Prozessorgruppen verwendet werden.<br />
<br />
=== Weitere Varianten ===<br />
Mergesort war einer der ersten Sortieralgorithmen, bei dem ein optimaler [[Speedup]] erreicht wurde, wobei Richard Cole einen cleveren Subsampling-Algorithmus verwendete, um die O(1)-Zusammenführung sicherzustellen.<ref>{{Literatur |Autor=Richard Cole |Titel=Parallel Merge Sort |Sammelwerk=SIAM Journal on Computing |Band=17 |Nummer=4 |Datum=1988-08 |ISSN=0097-5397 |DOI=10.1137/0217049 |Seiten=770–785 |Online=http://epubs.siam.org/doi/10.1137/0217049 |Abruf=2020-03-06}}</ref> Andere ausgeklügelte parallele Sortieralgorithmen können die gleichen oder bessere Zeitschranken mit einer niedrigeren Konstante erreichen. David Powers beschrieb beispielsweise 1991 einen parallelisierten [[Quicksort]] (und einen verwandten [[Radixsort]]), der durch implizite Partitionierung in <math>O(\log n)</math> Zeit auf einer [[Parallel Random Access Machine|CRCW-Parallel Random Access Machine (PRAM)]] mit <math>n</math> Prozessoren arbeiten kann.<ref>Powers, David M. W. [http://citeseer.ist.psu.edu/327487.html Parallelized Quicksort and Radixsort with Optimal Speedup], ''Proceedings of International Conference on Parallel Computing Technologies''. [[Novosibirsk]]. 1991.</ref> Powers zeigt ferner, dass eine Pipeline-Version von Batchers Bitonic Mergesort in <math>O((\log n)^2)</math> Zeit auf einem Butterfly-Sortiernetzwerk in der Praxis schneller ist als sein <math>O(\log n)</math> Sortieralgorithmus auf einer PRAM, und er bietet eine detaillierte Diskussion der versteckten Overheads beim Vergleich, bei der Radix- und der Parallelsortierung.<ref>David M. W. Powers, [http://david.wardpowers.info/Research/AI/papers/199501-ACAW-PUPC.pdf Parallel Unification: Practical Complexity], Australasian Computer Architecture Workshop, Flinders University, January 1995</ref></div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Parallel_merge_sort_deutsch&diff=197523480Benutzer:ParAlgMergeSort/sandbox/Parallel merge sort deutsch2020-03-06T23:11:25Z<p>ParAlgMergeSort: /* Mergesort mit paralleler Mischmethode */ kurz überflogen</p>
<hr />
<div>== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit entwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen beschrieben werden, die Teilen-Phase und die anschließende Misch-Phase. Die Erste besteht aus vielen rekursiven Aufrufen, die immer wieder den gleichen Aufteilungsprozess durchführen, bis die Teilsequenzen trivial sortiert sind (mit einem oder keinem Element). Ein intuitiver Ansatz ist es, diese rekursiven Aufrufe zu parallelisieren.<ref name="clrs2">{{Harvnb|Cormen|Leiserson|Rivest|Stein|2009|pp=797–805}}</ref> Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Rekursion unter Verwendung der Schlüsselwörter [[:en:Fork–join_model|fork and join]].<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.<br />
<br />
=== Mergesort mit paralleler Mischmethode ===<br />
Ein besserer Parallelismus kann durch eine parallele [[Merge-Algorithmen|Mischmethode]] erreicht werden. [[:en:Introduction_to_Algorithms|Cormen et al.]] präsentieren eine binäre Variante, welche zwei sortierte Teilsequenzen in eine sortierte Ausgabesequenz mischt.<ref name="clrs">{{Harvnb|Cormen|Leiserson|Rivest|Stein|2009|pp=797–805}}</ref><br />
<br />
In der längeren der beiden Sequenzen (falls ungleich lang) wird das Element des mittleren Indexes ausgewählt. Seine Position in der anderen Sequenz wird auf die Weise bestimmt, dass die Sequenz sortiert bliebe, wenn dieses Element an bestimmten Stelle eingefügt werden würde. So weiß man wie viele Elemente insgesamt kleiner sind als das Pivot Element und die finale Position des Pivots kann in der Ausgabesequenz berechnet werden. Für die so erzeugten Teilfolgen der kleineren und größeren Elemente wird die Mischmethode wieder parallel ausgeführt, bis der Basisfall der Rekursion erreicht ist. <br />
<br />
Der folgende Pseudocode illustriert die modifizierte parallele Mischmethode (aus Cormen et al.).<br />
<br />
/**<br />
* A: Input array<br />
* B: Output array<br />
* lo: lower bound<br />
* hi: upper bound<br />
* off: offset<br />
*/<br />
'''algorithm''' parallelMergesort(A, lo, hi, B, off) '''is'''<br />
len := hi - lo + 1<br />
'''if''' len == 1 '''then'''<br />
<nowiki> B[off] := A[lo]</nowiki><br />
'''else''' let T[1..len] be a new array<br />
mid := ⌊(lo + hi) / 2⌋ <br />
mid' := mid - lo + 1<br />
'''fork''' parallelMergesort(A, lo, mid, T, 1)<br />
parallelMergesort(A, mid + 1, hi, T, mid' + 1) <br />
'''join''' <br />
parallelMerge(T, 1, mid', mid' + 1, len, B, off)<br />
Um eine Rekurrenzrelation für den Worst Case zu erhalten müssen die rekursiven Aufrufe von parallelMergesort aufgrund der parallelen Ausführung nur einmal aufgeführt werden. Man erhält<br />
<br />
<math display="inline">T_{\infty}^{\text{sort} }(n) = T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + T_{\infty}^{\text{merge}}(n) <br />
= T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + \Theta \left( \log(n)^2\right)</math>.<br />
<br />
Die Lösung dieser Rekurrenz ist<br />
<br />
<math display="inline">T_{\infty}^{\text{sort}} = \Theta \left ( \log(n)^3 \right)</math>.<br />
<br />
Dieser Algorithmus erreicht eine Parallelisierbarkeit von <math>\Theta \biggr({n \over (\log n)^2}\biggr)</math>, was um einiges besser ist als der Parallelismus des vorherigen Algorithmus. Solch ein Sortieralgorithmus kann, wenn er mit einem schnellen stabilen sequentiellen Sortieralgorithmus und einer sequentiellen Mischmethode als Basisfall für das Mischen von zwei kleinen Sequenzen ausgestattet ist gut in der Praxis funktionieren.<ref>Victor J. Duvanenko "Parallel Merge Sort" Dr. Dobb's Journal & blog[https://duvanenko.tech.blog/2018/01/13/parallel-merge-sort/] and GitHub repo C++ implementation [https://github.com/DragonSpit/ParallelAlgorithms]</ref><br />
<br />
=== Paralleler Mehrwege-Mergesort ===<br />
Es wirkt unnatürlich, Mergesort Algorithmen auf binäre Mischmethoden zu beschränken, da oftmals mehr als zwei Prozessoren zur Verfügung stehen. Ein besserer Ansatz wäre es, ein [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] zu realisieren. Diese Generalisierung mischt <math>k</math> sortierte Sequenzen zu einer sortierten Sequenz und ist dementsprechend eine Generalisierung des binären Mischens. Diese Misch-Variante eignet sich gut zur Beschreibung eines Sortieralgorithmus auf einem [[Parallel Random Access Machine|PRAM]].<ref>Peter Sanders, Johannes Singler. 2008. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg08/singler.pdf</ref><ref name=":02">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
==== Grundidee ====<br />
[[File:Parallel_multiway_mergesort_process.svg|verweis=https://en.wikipedia.org/wiki/File:Parallel_multiway_mergesort_process.svg|alternativtext=|mini|Der parallele Mehrwege-Mergesort Algorithmus auf vier Prozessoren <math>t_0</math> bis <math>t_3</math>.]]<br />
Gegeben sei eine Folge von <math>n</math> Elementen. Ziel ist es, diese Sequenz mit <math>p</math> verfügbaren Prozessoren zu sortieren. Die Elemente sind dabei gleich auf alle Prozessoren aufgeteilt und werden zunächst lokal mit einem sequentiellen [[Sortierverfahren|Sortieralgorithmus]] vorsortiert. Dementsprechend bestehen die Daten nun aus sortierten Folgen <math>S_1, ..., S_p</math> der Länge <math display="inline">\lceil \frac{n}{p} \rceil</math>. Der Einfachheit halber sei <math>n</math> eine Vielfaches von <math>p</math>, so dass für <math>i = 1, ..., p</math> gilt: <math display="inline">\left\vert S_i \right\vert = \frac{n}{p}</math>. Jede dieser Sequenzen wird wiederum in <math>p</math> Teilsequenzen <math>S_{i,1}, ..., S_{i,p}</math> partitioniert, indem für <math>i = 1,..., p</math> die Trennelemente <math>v_i</math> mit globalem Rang <math display="inline">k = i \frac{n}{p}</math> bestimmt werden. Die korrespondierenden Indizes werden in jeder Folge <math>S_i</math> mit binärer Sucher ermittelt, sodass die Folgen anhand der Indizes aufgeteilt werden könne. Formal definiert gilt somit <math>S_{i,j} := \{x \in S_i | rank(v_{j-1}) < rank(x) \le rank(v_j)\}</math>.<br />
<br />
Nun werden die Elemente von <math>S_{1,i}, ..., S_{p,i}</math>dem Prozessor <math>i</math> zugeteilt. Dies sind alle Elemente vom globalen Rang <math display="inline">(i-1) \frac{n}{p}</math> bis zum Rang <math display="inline">i \frac{n}{p}</math>, die über die <math>S_i</math> verteilt sind. So erhält erhält jeder Prozessor eine Folge von sortierten Sequenzen. Aus der Tatsache, dass der Rang <math>k</math> der Trennelemente <math>v_i</math> global gewählt wurde, ergeben sich zwei wichtige Eigenschaften: Zunächst sind die Trennelemente so gewählt, dass jeder Prozessor nach der Zuteilung der neuen Daten immer noch mit <math display="inline">n/p</math> Elementen betraut ist. Der Algorithmus besitzt also eine perfekte [[Lastverteilung (Informatik)|Lastverteilung]]. Außerdem sind alle Elemente des Prozessors <math>i</math> kleiner oder gleich der Elemente des Prozessors <math>i+1</math>. Wenn nun jeder Prozessor ein [[Merge-Algorithmen#k-Wege-Mischen|p-Wege-Mischen]] lokal durchführt, sind aufgrund dieser Eigenschaft die Elemente global sortiert. Somit müssen die Ergebnisse nur in der Reihenfolge der Prozessoren zusammengesetzt werden.<br />
<br />
==== Trennelementbestimmung ====<br />
In der einfachsten Form sind <math>p</math> sortierte Folgen <math>S_1, ..., S_p</math> gleichverteilt auf <math>p</math> Prozessoren sowie ein Rang <math>k</math> gegeben. Gesucht ist nun ein Trennelement <math>x</math> mit globalem Rang <math>k</math> in der Vereinigung der Folgen. Damit kann jede Folge <math>S_i</math> an einem Index <math>l_i</math> in zwei Teile aufgeteilt werden: Der untere Teil besteht nur aus Elementen, die kleiner <math>x</math> sind, während der obere Teil alle Elemente enthält, welche größer oder gleich als <math>x</math> sind. <br />
<br />
Der hier vorgestellte sequentielle Algorithmus gibt die Indizes der Trennungen zurück, also die Indizes <math>l_i</math>in den Folgen <math>S_i</math>, sodass <math>S_i[l_i]</math> einen global kleineren Rang als <math>k</math> hat und <math>\mathrm{rank}\left(S_i[l_i+1]\right) \ge k</math> ist.<ref>Peter Sanders. 2019. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg19/vorlesung.pdf</ref><br />
'''algorithm'''<nowiki> msSelect(S : Array of sorted Sequences [S_1,..,S_p], k : int) </nowiki>'''is'''<br />
'''for''' i = 1 '''to''' p '''do''' <br />
(l_i, r_i) = (0, |S_i|-1)<br />
<br />
'''while''' there exists i: l_i < r_i '''do'''<br />
//pick Pivot Element in S_j[l_j],..,S_j[r_j], chose random j uniformly<br />
v := pickPivot(S, l, r)<br />
'''for''' i = 1 '''to''' p '''do''' <br />
m_i = binarySearch(v, S_i[l_i, r_i]) //sequentially<br />
'''if''' m_1 + ... + m_p >= k '''then''' //m_1+ ... + m_p is the global rank of v<br />
r := m //vector assignment<br />
'''else'''<br />
l := m <br />
<br />
'''return''' l<br />
Für die Komplexitätsanalyse wurde das PRAM-Modell gewählt. Die p-fache Ausführung der ''binarySearch'' Methode hat eine Laufzeit in <math>\mathcal{O}\left(p\log\left(n/p\right)\right)</math>, falls die Daten über alle <math>p</math> Prozessoren gleichverteilt anliegen. Die erwartete Rekursionstiefe beträgt wie im [[Quickselect]] Algorithmus <math>\mathcal{O}\left(\log\left( \textstyle \sum_i |S_i| \right)\right) = \mathcal{O}(\log(n))</math>. Somit ist die gesamte erwartete Laufzeit <math>\mathcal{O}\left(p\log(n/p)\log(n)\right)</math>.<br />
<br />
Angewandt auf den parallelen Mehrwege-Mergesort muss die ''msSelect'' Methode parallel ausgeführt werden, um alle Trennelemente vom Rang <math display="inline"> i \frac n p</math> gleichzeitig zu finden. Dies kann anschließend verwendet werden, um jede Folge in <math>p</math> Teile zu zerschneiden. Es ergibt sich die gleiche Gesamtlaufzeit <math>\mathcal{O}\left(p\, \log(n/p)\log(n)\right)</math>.<br />
<br />
==== Pseudocode ====<br />
Hier ist der komplette Pseudocode für den parallelen Mehrwege-Mergesort. Dabei wird eine Barriere-Synchronisation vor und nach der Trennelementbestimmung angenommen, sodass jeder Prozessor seine Trennelemente und die Partitionierung seiner Sequenz richtig berechnen kann. <br />
/**<br />
* d: Unsorted Array of Elements<br />
* n: Number of Elements<br />
* p: Number of Processors<br />
* return Sorted Array<br />
*/<br />
'''algorithm''' parallelMultiwayMergesort(d : Array, n : int, p : int) '''is'''<br />
o := '''new''' Array[0, n] // the output array<br />
'''for''' i = 1 '''to''' p '''do in parallel''' // each processor in parallel<br />
<nowiki> S_i := d[(i-1) * n/p, i * n/p] // Sequence of length n/p</nowiki><br />
sort(S_i) // sort locally<br />
'''synch'''<br />
v_i := msSelect([S_1,...,S_p], i * n/p) // element with global rank i * n/p<br />
'''synch'''<br />
(S_i,1 ,..., S_i,p) := sequence_partitioning(si, v_1, ..., v_p) // split s_i into subsequences<br />
<br />
o[(i-1) * n/p, i * n/p] := kWayMerge(s_1,i, ..., s_p,i) // merge and assign to output array<br />
<br />
'''return''' o<br />
<br />
==== Analyse ====<br />
Zunächst sortiert jeder Prozessor die zugewiesenen <math>n/p</math> Elemente lokal mit einem vergleichsbasiertem Sortieralgorithmus der Komplexität <math>\mathcal{O}\left( n/p \; \log ( n/p) \right)</math>. Anschließend können die Trennelemente in Zeit <math>\mathcal{O}\left(p \,\log(n/p) \log (n) \right)</math> bestimmt werden. Schließlich müssen jede Gruppe von <math>p</math> Teilstücken gleichzeitig von jedem Prozessor zusammen gemischt werden. Dies hat eine Laufzeit von <math>\mathcal{O}(\log(p)\; n/p )</math>, indem ein sequentieller [[:en:Merge_algorithm|k-Wege Mischalgorithmus]] verwendet wird. Somit ergibt sich eine Gesamtlaufzeit von<br />
<br />
<math>\mathcal{O}\left( \frac n p \log\left(\frac n p\right) + p \log \left( \frac n p\right) \log (n) + \frac n p \log (p) \right)</math>.<br />
<br />
==== Praktische Anpassung und Anwendung ====<br />
Der Mehrwege-Mergesort Algorithmus ist durch seine hohe Parallelität, was den Einsatz vieler Prozessoren ermöglicht, sehr skalierbar. Dies macht den Algorithmus zu einem brauchbaren Kandidaten für das Sortieren großer Datenmengen, wie sie beispielsweise in [[Rechnerverbund|Computer-Clustern]] verarbeitet werden. Da der Speicher in solchen Systemen in der Regel keine limitierende Ressource darstellt, ist der Nachteil der Speicherkomplexität von Mergesort vernachlässigbar. Allerdings werden in solchen Systemen andere Faktoren wichtig, die bei der Modellierung auf einer [[Parallel Random Access Machine|PRAM]] nicht berücksichtigt werden. Hier sind unter anderem die folgenden Aspekte zu berücksichtigen: Die [[Speicherhierarchie]], wenn die Daten nicht in den Cache der Prozessoren passen, oder der Kommunikationsaufwand beim Datenaustausch zwischen den Prozessoren, der zu einem Engpass werden könnte, wenn auf die Daten nicht mehr über den gemeinsamen Speicher zugegriffen werden kann.<ref name=":0">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
[[Peter Sanders|Sanders]] et al. haben in ihrem Paper einen [[Bulk Synchronous Parallel Computers|bulk synchronous parallel]]-Algorithmus für einen mehrstufigen Mehrwege-Mergesort vorgestellt, der <math>p</math> Prozessoren in <math>r</math> Gruppen der Größe <math>p'</math> unterteilt. Alle Prozessoren sortieren zuerst lokal. Im Gegensatz zu einem einstufigen Mehrwege-Mergesort werden diese Sequenzen dann in <math>r</math> Teile aufgeteilt und den entsprechenden Prozessorgruppen zugeordnet. Diese Schritte werden innerhalb dieser Gruppen rekursiv wiederholt. So wird die Kommunikation reduziert und insbesondere Probleme mit vielen kleinen Nachrichten vermieden. Die hierarchische Struktur des zugrundeliegenden realen Netzwerks (z.B. [[Rack|Racks]], [[Rechnerverbund|Cluster]],...) kann zur Definition der Prozessorgruppen verwendet werden.<br />
<br />
=== Weitere Varianten ===<br />
Mergesort war einer der ersten Sortieralgorithmen, bei dem ein optimaler [[Speedup]] erreicht wurde, wobei Richard Cole einen cleveren Subsampling-Algorithmus verwendete, um die O(1)-Zusammenführung sicherzustellen.<ref>{{Literatur |Autor=Richard Cole |Titel=Parallel Merge Sort |Sammelwerk=SIAM Journal on Computing |Band=17 |Nummer=4 |Datum=1988-08 |ISSN=0097-5397 |DOI=10.1137/0217049 |Seiten=770–785 |Online=http://epubs.siam.org/doi/10.1137/0217049 |Abruf=2020-03-06}}</ref> Andere ausgeklügelte parallele Sortieralgorithmen können die gleichen oder bessere Zeitschranken mit einer niedrigeren Konstante erreichen. David Powers beschrieb beispielsweise 1991 einen parallelisierten [[Quicksort]] (und einen verwandten [[Radixsort]]), der durch implizite Partitionierung in <math>O(\log n)</math> Zeit auf einer [[Parallel Random Access Machine|CRCW-Parallel Random Access Machine (PRAM)]] mit <math>n</math> Prozessoren arbeiten kann.<ref>Powers, David M. W. [http://citeseer.ist.psu.edu/327487.html Parallelized Quicksort and Radixsort with Optimal Speedup], ''Proceedings of International Conference on Parallel Computing Technologies''. [[Novosibirsk]]. 1991.</ref> Powers zeigt ferner, dass eine Pipeline-Version von Batchers Bitonic Mergesort in <math>O((\log n)^2)</math> Zeit auf einem Butterfly-Sortiernetzwerk in der Praxis schneller ist als sein <math>O(\log n)</math> Sortieralgorithmus auf einer PRAM, und er bietet eine detaillierte Diskussion der versteckten Overheads beim Vergleich, bei der Radix- und der Parallelsortierung.<ref>David M. W. Powers, [http://david.wardpowers.info/Research/AI/papers/199501-ACAW-PUPC.pdf Parallel Unification: Practical Complexity], Australasian Computer Architecture Workshop, Flinders University, January 1995</ref></div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Parallel_merge_sort_deutsch&diff=197522184Benutzer:ParAlgMergeSort/sandbox/Parallel merge sort deutsch2020-03-06T22:19:11Z<p>ParAlgMergeSort: /* Mergesort mit parallelen Rekursionsaufrufen */ kurz überflogen</p>
<hr />
<div>== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit entwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen beschrieben werden, die Teilen-Phase und die anschließende Misch-Phase. Die Erste besteht aus vielen rekursiven Aufrufen, die immer wieder den gleichen Aufteilungsprozess durchführen, bis die Teilsequenzen trivial sortiert sind (mit einem oder keinem Element). Ein intuitiver Ansatz ist es, diese rekursiven Aufrufe zu parallelisieren.<ref name="clrs2">{{Harvnb|Cormen|Leiserson|Rivest|Stein|2009|pp=797–805}}</ref> Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Rekursion unter Verwendung der Schlüsselwörter [[:en:Fork–join_model|fork and join]]:<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.<br />
<br />
=== Mergesort mit paralleler Mischmethode ===<br />
Ein besserer Parallelismus kann durch eine parallele [[Merge-Algorithmen|Mischmethode]] erreicht werden. [[:en:Introduction_to_Algorithms|Cormen et al.]] präsentieren eine binäre Variante, welche zwei sortierte Teilsequenzen in eine sortierte Ausgabesequenz mischt.<ref name="clrs">{{Harvnb|Cormen|Leiserson|Rivest|Stein|2009|pp=797–805}}</ref><br />
<br />
In der längeren der beiden Sequenzen (falls ungleich lang) wird das Element mit dem mittleren Index ausgewählt. Seien Position wird in der anderen Sequenz in der Weise gesucht, dass die Sequenz sortiert bleibt falls man dieses Element an dieser Position einfügen würde. So weiß man wie viele Elemente insgesamt kleiner sind als das Pivot Element und die finale Position des Pivots kann in der Ausgabesequenz bestimmt werden. Diese Mischmethode wird nun rekursiv auf die Teilsequenzen der kleineren und der größeren Elemente aufgerufen, bis der Basisfall der Rekursion erreicht ist. <br />
<br />
Der folgende Pseudocode illustriert die modifizierte parallele Mischmethode (aus Cormen et al.).<br />
<br />
/**<br />
* A: Input array<br />
* B: Output array<br />
* lo: lower bound<br />
* hi: upper bound<br />
* off: offset<br />
*/<br />
'''algorithm''' parallelMergesort(A, lo, hi, B, off) '''is'''<br />
len := hi - lo + 1<br />
'''if''' len == 1 '''then'''<br />
<nowiki> B[off] := A[lo]</nowiki><br />
'''else''' let T[1..len] be a new array<br />
mid := ⌊(lo + hi) / 2⌋ <br />
mid' := mid - lo + 1<br />
'''fork''' parallelMergesort(A, lo, mid, T, 1)<br />
parallelMergesort(A, mid + 1, hi, T, mid' + 1) <br />
'''join''' <br />
parallelMerge(T, 1, mid', mid' + 1, len, B, off)<br />
Um eine Rekurrenzrelation für den Worst Case zu erhalten müssen die rekursiven Aufrufe von parallelMergesort aufgrund der parallelen Ausführung nur einmal aufgeführt werden. Man erhält<br />
<br />
<math display="inline">T_{\infty}^{\text{sort} }(n) = T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + T_{\infty}^{\text{merge}}(n) <br />
= T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + \Theta \left( \log(n)^2\right)</math>.<br />
<br />
Die Lösung dieser Rekurrenz ist<br />
<br />
<math display="inline">T_{\infty}^{\text{sort}} = \Theta \left ( \log(n)^3 \right)</math>.<br />
<br />
Dieser Algorithmus erreicht eine Parallelisierbarkeit von <math>\Theta \biggr({n \over (\log n)^2}\biggr)</math>, was um einiges besser ist als der Parallelismus des vorherigen Algorithmus. Solch ein Sortieralgorithmus kann, wenn er mit einem schnellen stabilen sequentiellen Sortieralgorithmus und einer sequentiellen Mischmethode als Basisfall für das Mischen von zwei kleinen Sequenzen ausgestattet ist gut in der Praxis funktionieren.<ref>Victor J. Duvanenko "Parallel Merge Sort" Dr. Dobb's Journal & blog[https://duvanenko.tech.blog/2018/01/13/parallel-merge-sort/] and GitHub repo C++ implementation [https://github.com/DragonSpit/ParallelAlgorithms]</ref><br />
<br />
=== Paralleler Mehrwege-Mergesort ===<br />
Es wirkt unnatürlich, Mergesort Algorithmen auf binäre Mischmethoden zu beschränken, da oftmals mehr als zwei Prozessoren zur Verfügung stehen. Ein besserer Ansatz wäre es, ein [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] zu realisieren. Diese Generalisierung mischt <math>k</math> sortierte Sequenzen zu einer sortierten Sequenz und ist dementsprechend eine Generalisierung des binären Mischens. Diese Mischvariante ermöglicht es, einen Sortieralgorithmus im [[Parallel Random Access Machine|PRAM]]-Modell zu beschreiben.<ref>Peter Sanders, Johannes Singler. 2008. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg08/singler.pdf</ref><ref name=":02">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
==== Grundidee ====<br />
[[File:Parallel_multiway_mergesort_process.svg|verweis=https://en.wikipedia.org/wiki/File:Parallel_multiway_mergesort_process.svg|alternativtext=|mini|Der parallele Mehrwege-Mergesort Algorithmus auf vier Prozessoren <math>t_0</math> bis <math>t_3</math>.]]<br />
Gegeben sei eine Folge von <math>n</math> Elementen. Ziel ist es, diese Sequenz mit <math>p</math> freien Prozessoren zu sortieren. Die Elemente sind dabei gleich auf alle Prozessoren aufgeteilt und werden zunächst lokal mit einem sequentiellen [[Sortierverfahren|Sortieralgorithmus]] vorsortiert. Dementsprechend bestehen die Daten nun aus sortierten Folgen <math>S_1, ..., S_p</math> der Länge <math display="inline">\lceil \frac{n}{p} \rceil</math>. Der Einfachheit halber sei <math>n</math> eine Vielfaches von <math>p</math>, so dass für <math>i = 1, ..., p</math> gilt: <math display="inline">\left\vert S_i \right\vert = \frac{n}{p}</math>. Jede dieser Sequenzen wird wiederum in <math>p</math> Teilsequenzen <math>S_{i,1}, ..., S_{i,p}</math> zerteilt. Dazu werden für <math>i = 1,..., p</math> die Trennelemente <math>v_i</math> mit globalem Rang <math display="inline">k = i \frac{n}{p}</math> bestimmt. Die korrespondierenden Indizes werden in jeder Folge <math>S_i</math> gesucht, sodass die Folge an diesem Index aufgeteilt werden kann. Dadurch ergibt sich <math>S_{i,j} := \{x \in S_i | rank(v_{j-1}) < rank(x) \le rank(v_j)\}</math>.<br />
<br />
Nun werden die Elemente von <math>S_{1,i}, ..., S_{p,i}</math>dem Prozessor <math>i</math> zugeteilt. Dies sind alle Elemente vom globalen Rang <math display="inline">(i-1) \frac{n}{p}</math> bis zum Rang <math display="inline">i \frac{n}{p}</math>. So erhält erhält jeder Prozessor eine Folge von sortierten Sequenzen. Aus der Tatsache, dass der Rang <math>k</math> der Trennelemente <math>v_i</math> global gewählt wurde, ergeben sich zwei wichtige Eigenschaften: Zunächst waren die Trennelemente so gewählt, dass jeder Prozessor nach der Zuteilung der neuen Daten immer noch mit <math display="inline">n/p</math> Elementen betraut ist Der Algorithmus besitzt also eine perfekte [[Lastverteilung (Informatik)|Lastverteilung]]. Außerdem sind alle Elemente des Prozessors <math>i</math> kleiner oder gleich der Elemente des Prozessors <math>i+1</math>. Wenn nun jeder Prozessor ein [[Merge-Algorithmen#k-Wege-Mischen|p-Wege-Mischen]] durchführt, sind aufgrund dieser Eigenschaft die Elemente global sortiert, das Ergebnis muss nur in der Reihenfolge der Prozessoren zusammengesetzt werden.<br />
<br />
==== Trennelementbestimmung ====<br />
In der einfachsten Form sind <math>p</math> sortierte Folgen <math>S_1, ..., S_p</math> gleichverteilt auf <math>p</math> Prozessoren sowie ein Rang <math>k</math> gegeben. Gesucht ist nun ein Trennelement <math>x</math> mit globalem Rang <math>k</math> in der Vereinigung der Folgen. Damit kann jede Folge <math>S_i</math> an einem Index <math>l_i</math> in zwei Teile aufgeteilt werden: Der untere Teil besteht nur aus Elementen, die kleiner <math>x</math> sind, während der obere Teil alle Elemente enthält, welche größer oder gleich als <math>x</math> sind. <br />
<br />
Der hier vorgestellte sequentielle Algorithmus gibt die Indizes der Trennungen zurück, also die Indizes <math>l_i</math>in den Folgen <math>S_i</math>, sodass <math>S_i[l_i]</math> einen global kleineren Rang als <math>k</math> hat und <math>\mathrm{rank}\left(S_i[l_i+1]\right) \ge k</math> ist.<ref>Peter Sanders. 2019. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg19/vorlesung.pdf</ref><br />
'''algorithm'''<nowiki> msSelect(S : Array of sorted Sequences [S_1,..,S_p], k : int) </nowiki>'''is'''<br />
'''for''' i = 1 '''to''' p '''do''' <br />
(l_i, r_i) = (0, |S_i|-1)<br />
<br />
'''while''' there exists i: l_i < r_i '''do'''<br />
//pick Pivot Element in S_j[l_j],..,S_j[r_j], chose random j uniformly<br />
v := pickPivot(S, l, r)<br />
'''for''' i = 1 '''to''' p '''do''' <br />
m_i = binarySearch(v, S_i[l_i, r_i]) //sequentially<br />
'''if''' m_1 + ... + m_p >= k '''then''' //m_1+ ... + m_p is the global rank of v<br />
r := m //vector assignment<br />
'''else'''<br />
l := m <br />
<br />
'''return''' l<br />
Für die Komplexitätsanalyse wurde das PRAM-Modell gewählt. Die p-fache Ausführung der ''binarySearch'' Methode hat eine Laufzeit in <math>\mathcal{O}\left(p\log\left(n/p\right)\right)</math>, falls die Daten über alle <math>p</math> Prozessoren gleichverteilt anliegen. Die erwartete Rekursionstiefe beträgt wie im [[Quickselect]] Algorithmus <math>\mathcal{O}\left(\log\left( \textstyle \sum_i |S_i| \right)\right) = \mathcal{O}(\log(n))</math>. Somit ist die gesamte erwartete Laufzeit <math>\mathcal{O}\left(p\log(n/p)\log(n)\right)</math>.<br />
<br />
Angewandt auf den parallelen Mehrwege-Mergesort muss die ''msSelect'' Methode parallel ausgeführt werden, um alle Trennelemente vom Rang <math display="inline"> i \frac n p</math> gleichzeitig zu finden. Dies kann anschließend verwendet werden, um jede Folge in <math>p</math> Teile zu zerschneiden. Es ergibt sich die gleiche Gesamtlaufzeit <math>\mathcal{O}\left(p\, \log(n/p)\log(n)\right)</math>.<br />
<br />
==== Pseudocode ====<br />
Hier ist der komplette Pseudocode für den parallelen Mehrwege-Mergesort. Dabei wird eine Barriere-Synchronisation vor und nach der Trennelementbestimmung angenommen, sodass jeder Prozessor seine Trennelemente und die Partitionierung seiner Sequenz richtig berechnen kann. <br />
/**<br />
* d: Unsorted Array of Elements<br />
* n: Number of Elements<br />
* p: Number of Processors<br />
* return Sorted Array<br />
*/<br />
'''algorithm''' parallelMultiwayMergesort(d : Array, n : int, p : int) '''is'''<br />
o := '''new''' Array[0, n] // the output array<br />
'''for''' i = 1 '''to''' p '''do in parallel''' // each processor in parallel<br />
<nowiki> S_i := d[(i-1) * n/p, i * n/p] // Sequence of length n/p</nowiki><br />
sort(S_i) // sort locally<br />
'''synch'''<br />
v_i := msSelect([S_1,...,S_p], i * n/p) // element with global rank i * n/p<br />
'''synch'''<br />
(S_i,1 ,..., S_i,p) := sequence_partitioning(si, v_1, ..., v_p) // split s_i into subsequences<br />
<br />
o[(i-1) * n/p, i * n/p] := kWayMerge(s_1,i, ..., s_p,i) // merge and assign to output array<br />
<br />
'''return''' o<br />
<br />
==== Analyse ====<br />
Zunächst sortiert jeder Prozessor die zugewiesenen <math>n/p</math> Elemente lokal mit einem vergleichsbasiertem Sortieralgorithmus der Komplexität <math>\mathcal{O}\left( n/p \; \log ( n/p) \right)</math>. Anschließend können die Trennelemente in Zeit <math>\mathcal{O}\left(p \,\log(n/p) \log (n) \right)</math> bestimmt werden. Schließlich müssen jede Gruppe von <math>p</math> Teilstücken gleichzeitig von jedem Prozessor zusammen gemischt werden. Dies hat eine Laufzeit von <math>\mathcal{O}(\log(p)\; n/p )</math>, indem ein sequentieller [[:en:Merge_algorithm|k-Wege Mischalgorithmus]] verwendet wird. Somit ergibt sich eine Gesamtlaufzeit von<br />
<br />
<math>\mathcal{O}\left( \frac n p \log\left(\frac n p\right) + p \log \left( \frac n p\right) \log (n) + \frac n p \log (p) \right)</math>.<br />
<br />
==== Praktische Anpassung und Anwendung ====<br />
Der Mehrwege-Mergesort Algorithmus ist durch seine hohe Parallelität, was den Einsatz vieler Prozessoren ermöglicht, sehr skalierbar. Dies macht den Algorithmus zu einem brauchbaren Kandidaten für das Sortieren großer Datenmengen, wie sie beispielsweise in [[Rechnerverbund|Computer-Clustern]] verarbeitet werden. Da der Speicher in solchen Systemen in der Regel keine limitierende Ressource darstellt, ist der Nachteil der Speicherkomplexität von Mergesort vernachlässigbar. Allerdings werden in solchen Systemen andere Faktoren wichtig, die bei der Modellierung auf einer [[Parallel Random Access Machine|PRAM]] nicht berücksichtigt werden. Hier sind unter anderem die folgenden Aspekte zu berücksichtigen: Die [[Speicherhierarchie]], wenn die Daten nicht in den Cache der Prozessoren passen, oder der Kommunikationsaufwand beim Datenaustausch zwischen den Prozessoren, der zu einem Engpass werden könnte, wenn auf die Daten nicht mehr über den gemeinsamen Speicher zugegriffen werden kann.<ref name=":0">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
[[Peter Sanders|Sanders]] et al. haben in ihrem Paper einen [[Bulk Synchronous Parallel Computers|bulk synchronous parallel]]-Algorithmus für einen mehrstufigen Mehrwege-Mergesort vorgestellt, der <math>p</math> Prozessoren in <math>r</math> Gruppen der Größe <math>p'</math> unterteilt. Alle Prozessoren sortieren zuerst lokal. Im Gegensatz zu einem einstufigen Mehrwege-Mergesort werden diese Sequenzen dann in <math>r</math> Teile aufgeteilt und den entsprechenden Prozessorgruppen zugeordnet. Diese Schritte werden innerhalb dieser Gruppen rekursiv wiederholt. So wird die Kommunikation reduziert und insbesondere Probleme mit vielen kleinen Nachrichten vermieden. Die hierarchische Struktur des zugrundeliegenden realen Netzwerks (z.B. [[Rack|Racks]], [[Rechnerverbund|Cluster]],...) kann zur Definition der Prozessorgruppen verwendet werden.<br />
<br />
=== Weitere Varianten ===<br />
Mergesort war einer der ersten Sortieralgorithmen, bei dem ein optimaler [[Speedup]] erreicht wurde, wobei Richard Cole einen cleveren Subsampling-Algorithmus verwendete, um die O(1)-Zusammenführung sicherzustellen.<ref>{{Literatur |Autor=Richard Cole |Titel=Parallel Merge Sort |Sammelwerk=SIAM Journal on Computing |Band=17 |Nummer=4 |Datum=1988-08 |ISSN=0097-5397 |DOI=10.1137/0217049 |Seiten=770–785 |Online=http://epubs.siam.org/doi/10.1137/0217049 |Abruf=2020-03-06}}</ref> Andere ausgeklügelte parallele Sortieralgorithmen können die gleichen oder bessere Zeitschranken mit einer niedrigeren Konstante erreichen. David Powers beschrieb beispielsweise 1991 einen parallelisierten [[Quicksort]] (und einen verwandten [[Radixsort]]), der durch implizite Partitionierung in <math>O(\log n)</math> Zeit auf einer [[Parallel Random Access Machine|CRCW-Parallel Random Access Machine (PRAM)]] mit <math>n</math> Prozessoren arbeiten kann.<ref>Powers, David M. W. [http://citeseer.ist.psu.edu/327487.html Parallelized Quicksort and Radixsort with Optimal Speedup], ''Proceedings of International Conference on Parallel Computing Technologies''. [[Novosibirsk]]. 1991.</ref> Powers zeigt ferner, dass eine Pipeline-Version von Batchers Bitonic Mergesort in <math>O((\log n)^2)</math> Zeit auf einem Butterfly-Sortiernetzwerk in der Praxis schneller ist als sein <math>O(\log n)</math> Sortieralgorithmus auf einer PRAM, und er bietet eine detaillierte Diskussion der versteckten Overheads beim Vergleich, bei der Radix- und der Parallelsortierung.<ref>David M. W. Powers, [http://david.wardpowers.info/Research/AI/papers/199501-ACAW-PUPC.pdf Parallel Unification: Practical Complexity], Australasian Computer Architecture Workshop, Flinders University, January 1995</ref></div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Parallel_merge_sort_deutsch&diff=197521977Benutzer:ParAlgMergeSort/sandbox/Parallel merge sort deutsch2020-03-06T22:07:50Z<p>ParAlgMergeSort: Übersetzung und Belege</p>
<hr />
<div>== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit entwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen erklärt werden, die Teile-phase und anschließend die Mischphase. Die erste besteht aus vielen rekursiven Aufrufen, in denen alle Teilungsprozesse aufrufen bis die Teilsequenzen trivial sortiert sind (das heißt nur ein oder kein Element enthalten). Ein intuitiver Ansatz wäre es, diese rekursiven Aufrufe zu parallelisieren.<ref name="clrs2">{{Harvnb|Cormen|Leiserson|Rivest|Stein|2009|pp=797–805}}</ref> Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Ausführung der Rekursion indem die [[:en:Fork–join_model|fork and join]] Schlüsselwörter verwendet werden.<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.<br />
<br />
=== Mergesort mit paralleler Mischmethode ===<br />
Ein besserer Parallelismus kann durch eine parallele [[Merge-Algorithmen|Mischmethode]] erreicht werden. [[:en:Introduction_to_Algorithms|Cormen et al.]] präsentieren eine binäre Variante, welche zwei sortierte Teilsequenzen in eine sortierte Ausgabesequenz mischt.<ref name="clrs">{{Harvnb|Cormen|Leiserson|Rivest|Stein|2009|pp=797–805}}</ref><br />
<br />
In der längeren der beiden Sequenzen (falls ungleich lang) wird das Element mit dem mittleren Index ausgewählt. Seien Position wird in der anderen Sequenz in der Weise gesucht, dass die Sequenz sortiert bleibt falls man dieses Element an dieser Position einfügen würde. So weiß man wie viele Elemente insgesamt kleiner sind als das Pivot Element und die finale Position des Pivots kann in der Ausgabesequenz bestimmt werden. Diese Mischmethode wird nun rekursiv auf die Teilsequenzen der kleineren und der größeren Elemente aufgerufen, bis der Basisfall der Rekursion erreicht ist. <br />
<br />
Der folgende Pseudocode illustriert die modifizierte parallele Mischmethode (aus Cormen et al.).<br />
<br />
/**<br />
* A: Input array<br />
* B: Output array<br />
* lo: lower bound<br />
* hi: upper bound<br />
* off: offset<br />
*/<br />
'''algorithm''' parallelMergesort(A, lo, hi, B, off) '''is'''<br />
len := hi - lo + 1<br />
'''if''' len == 1 '''then'''<br />
<nowiki> B[off] := A[lo]</nowiki><br />
'''else''' let T[1..len] be a new array<br />
mid := ⌊(lo + hi) / 2⌋ <br />
mid' := mid - lo + 1<br />
'''fork''' parallelMergesort(A, lo, mid, T, 1)<br />
parallelMergesort(A, mid + 1, hi, T, mid' + 1) <br />
'''join''' <br />
parallelMerge(T, 1, mid', mid' + 1, len, B, off)<br />
Um eine Rekurrenzrelation für den Worst Case zu erhalten müssen die rekursiven Aufrufe von parallelMergesort aufgrund der parallelen Ausführung nur einmal aufgeführt werden. Man erhält<br />
<br />
<math display="inline">T_{\infty}^{\text{sort} }(n) = T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + T_{\infty}^{\text{merge}}(n) <br />
= T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + \Theta \left( \log(n)^2\right)</math>.<br />
<br />
Die Lösung dieser Rekurrenz ist<br />
<br />
<math display="inline">T_{\infty}^{\text{sort}} = \Theta \left ( \log(n)^3 \right)</math>.<br />
<br />
Dieser Algorithmus erreicht eine Parallelisierbarkeit von <math>\Theta \biggr({n \over (\log n)^2}\biggr)</math>, was um einiges besser ist als der Parallelismus des vorherigen Algorithmus. Solch ein Sortieralgorithmus kann, wenn er mit einem schnellen stabilen sequentiellen Sortieralgorithmus und einer sequentiellen Mischmethode als Basisfall für das Mischen von zwei kleinen Sequenzen ausgestattet ist gut in der Praxis funktionieren.<ref>Victor J. Duvanenko "Parallel Merge Sort" Dr. Dobb's Journal & blog[https://duvanenko.tech.blog/2018/01/13/parallel-merge-sort/] and GitHub repo C++ implementation [https://github.com/DragonSpit/ParallelAlgorithms]</ref><br />
<br />
=== Paralleler Mehrwege-Mergesort ===<br />
Es wirkt unnatürlich, Mergesort Algorithmen auf binäre Mischmethoden zu beschränken, da oftmals mehr als zwei Prozessoren zur Verfügung stehen. Ein besserer Ansatz wäre es, ein [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] zu realisieren. Diese Generalisierung mischt <math>k</math> sortierte Sequenzen zu einer sortierten Sequenz und ist dementsprechend eine Generalisierung des binären Mischens. Diese Mischvariante ermöglicht es, einen Sortieralgorithmus im [[Parallel Random Access Machine|PRAM]]-Modell zu beschreiben.<ref>Peter Sanders, Johannes Singler. 2008. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg08/singler.pdf</ref><ref name=":02">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
==== Grundidee ====<br />
[[File:Parallel_multiway_mergesort_process.svg|verweis=https://en.wikipedia.org/wiki/File:Parallel_multiway_mergesort_process.svg|alternativtext=|mini|Der parallele Mehrwege-Mergesort Algorithmus auf vier Prozessoren <math>t_0</math> bis <math>t_3</math>.]]<br />
Gegeben sei eine Folge von <math>n</math> Elementen. Ziel ist es, diese Sequenz mit <math>p</math> freien Prozessoren zu sortieren. Die Elemente sind dabei gleich auf alle Prozessoren aufgeteilt und werden zunächst lokal mit einem sequentiellen [[Sortierverfahren|Sortieralgorithmus]] vorsortiert. Dementsprechend bestehen die Daten nun aus sortierten Folgen <math>S_1, ..., S_p</math> der Länge <math display="inline">\lceil \frac{n}{p} \rceil</math>. Der Einfachheit halber sei <math>n</math> eine Vielfaches von <math>p</math>, so dass für <math>i = 1, ..., p</math> gilt: <math display="inline">\left\vert S_i \right\vert = \frac{n}{p}</math>. Jede dieser Sequenzen wird wiederum in <math>p</math> Teilsequenzen <math>S_{i,1}, ..., S_{i,p}</math> zerteilt. Dazu werden für <math>i = 1,..., p</math> die Trennelemente <math>v_i</math> mit globalem Rang <math display="inline">k = i \frac{n}{p}</math> bestimmt. Die korrespondierenden Indizes werden in jeder Folge <math>S_i</math> gesucht, sodass die Folge an diesem Index aufgeteilt werden kann. Dadurch ergibt sich <math>S_{i,j} := \{x \in S_i | rank(v_{j-1}) < rank(x) \le rank(v_j)\}</math>.<br />
<br />
Nun werden die Elemente von <math>S_{1,i}, ..., S_{p,i}</math>dem Prozessor <math>i</math> zugeteilt. Dies sind alle Elemente vom globalen Rang <math display="inline">(i-1) \frac{n}{p}</math> bis zum Rang <math display="inline">i \frac{n}{p}</math>. So erhält erhält jeder Prozessor eine Folge von sortierten Sequenzen. Aus der Tatsache, dass der Rang <math>k</math> der Trennelemente <math>v_i</math> global gewählt wurde, ergeben sich zwei wichtige Eigenschaften: Zunächst waren die Trennelemente so gewählt, dass jeder Prozessor nach der Zuteilung der neuen Daten immer noch mit <math display="inline">n/p</math> Elementen betraut ist Der Algorithmus besitzt also eine perfekte [[Lastverteilung (Informatik)|Lastverteilung]]. Außerdem sind alle Elemente des Prozessors <math>i</math> kleiner oder gleich der Elemente des Prozessors <math>i+1</math>. Wenn nun jeder Prozessor ein [[Merge-Algorithmen#k-Wege-Mischen|p-Wege-Mischen]] durchführt, sind aufgrund dieser Eigenschaft die Elemente global sortiert, das Ergebnis muss nur in der Reihenfolge der Prozessoren zusammengesetzt werden.<br />
<br />
==== Trennelementbestimmung ====<br />
In der einfachsten Form sind <math>p</math> sortierte Folgen <math>S_1, ..., S_p</math> gleichverteilt auf <math>p</math> Prozessoren sowie ein Rang <math>k</math> gegeben. Gesucht ist nun ein Trennelement <math>x</math> mit globalem Rang <math>k</math> in der Vereinigung der Folgen. Damit kann jede Folge <math>S_i</math> an einem Index <math>l_i</math> in zwei Teile aufgeteilt werden: Der untere Teil besteht nur aus Elementen, die kleiner <math>x</math> sind, während der obere Teil alle Elemente enthält, welche größer oder gleich als <math>x</math> sind. <br />
<br />
Der hier vorgestellte sequentielle Algorithmus gibt die Indizes der Trennungen zurück, also die Indizes <math>l_i</math>in den Folgen <math>S_i</math>, sodass <math>S_i[l_i]</math> einen global kleineren Rang als <math>k</math> hat und <math>\mathrm{rank}\left(S_i[l_i+1]\right) \ge k</math> ist.<ref>Peter Sanders. 2019. Lecture ''Parallel algorithms'' Last visited 05.02.2020. http://algo2.iti.kit.edu/sanders/courses/paralg19/vorlesung.pdf</ref><br />
'''algorithm'''<nowiki> msSelect(S : Array of sorted Sequences [S_1,..,S_p], k : int) </nowiki>'''is'''<br />
'''for''' i = 1 '''to''' p '''do''' <br />
(l_i, r_i) = (0, |S_i|-1)<br />
<br />
'''while''' there exists i: l_i < r_i '''do'''<br />
//pick Pivot Element in S_j[l_j],..,S_j[r_j], chose random j uniformly<br />
v := pickPivot(S, l, r)<br />
'''for''' i = 1 '''to''' p '''do''' <br />
m_i = binarySearch(v, S_i[l_i, r_i]) //sequentially<br />
'''if''' m_1 + ... + m_p >= k '''then''' //m_1+ ... + m_p is the global rank of v<br />
r := m //vector assignment<br />
'''else'''<br />
l := m <br />
<br />
'''return''' l<br />
Für die Komplexitätsanalyse wurde das PRAM-Modell gewählt. Die p-fache Ausführung der ''binarySearch'' Methode hat eine Laufzeit in <math>\mathcal{O}\left(p\log\left(n/p\right)\right)</math>, falls die Daten über alle <math>p</math> Prozessoren gleichverteilt anliegen. Die erwartete Rekursionstiefe beträgt wie im [[Quickselect]] Algorithmus <math>\mathcal{O}\left(\log\left( \textstyle \sum_i |S_i| \right)\right) = \mathcal{O}(\log(n))</math>. Somit ist die gesamte erwartete Laufzeit <math>\mathcal{O}\left(p\log(n/p)\log(n)\right)</math>.<br />
<br />
Angewandt auf den parallelen Mehrwege-Mergesort muss die ''msSelect'' Methode parallel ausgeführt werden, um alle Trennelemente vom Rang <math display="inline"> i \frac n p</math> gleichzeitig zu finden. Dies kann anschließend verwendet werden, um jede Folge in <math>p</math> Teile zu zerschneiden. Es ergibt sich die gleiche Gesamtlaufzeit <math>\mathcal{O}\left(p\, \log(n/p)\log(n)\right)</math>.<br />
<br />
==== Pseudocode ====<br />
Hier ist der komplette Pseudocode für den parallelen Mehrwege-Mergesort. Dabei wird eine Barriere-Synchronisation vor und nach der Trennelementbestimmung angenommen, sodass jeder Prozessor seine Trennelemente und die Partitionierung seiner Sequenz richtig berechnen kann. <br />
/**<br />
* d: Unsorted Array of Elements<br />
* n: Number of Elements<br />
* p: Number of Processors<br />
* return Sorted Array<br />
*/<br />
'''algorithm''' parallelMultiwayMergesort(d : Array, n : int, p : int) '''is'''<br />
o := '''new''' Array[0, n] // the output array<br />
'''for''' i = 1 '''to''' p '''do in parallel''' // each processor in parallel<br />
<nowiki> S_i := d[(i-1) * n/p, i * n/p] // Sequence of length n/p</nowiki><br />
sort(S_i) // sort locally<br />
'''synch'''<br />
v_i := msSelect([S_1,...,S_p], i * n/p) // element with global rank i * n/p<br />
'''synch'''<br />
(S_i,1 ,..., S_i,p) := sequence_partitioning(si, v_1, ..., v_p) // split s_i into subsequences<br />
<br />
o[(i-1) * n/p, i * n/p] := kWayMerge(s_1,i, ..., s_p,i) // merge and assign to output array<br />
<br />
'''return''' o<br />
<br />
==== Analyse ====<br />
Zunächst sortiert jeder Prozessor die zugewiesenen <math>n/p</math> Elemente lokal mit einem vergleichsbasiertem Sortieralgorithmus der Komplexität <math>\mathcal{O}\left( n/p \; \log ( n/p) \right)</math>. Anschließend können die Trennelemente in Zeit <math>\mathcal{O}\left(p \,\log(n/p) \log (n) \right)</math> bestimmt werden. Schließlich müssen jede Gruppe von <math>p</math> Teilstücken gleichzeitig von jedem Prozessor zusammen gemischt werden. Dies hat eine Laufzeit von <math>\mathcal{O}(\log(p)\; n/p )</math>, indem ein sequentieller [[:en:Merge_algorithm|k-Wege Mischalgorithmus]] verwendet wird. Somit ergibt sich eine Gesamtlaufzeit von<br />
<br />
<math>\mathcal{O}\left( \frac n p \log\left(\frac n p\right) + p \log \left( \frac n p\right) \log (n) + \frac n p \log (p) \right)</math>.<br />
<br />
==== Praktische Anpassung und Anwendung ====<br />
Der Mehrwege-Mergesort Algorithmus ist durch seine hohe Parallelität, was den Einsatz vieler Prozessoren ermöglicht, sehr skalierbar. Dies macht den Algorithmus zu einem brauchbaren Kandidaten für das Sortieren großer Datenmengen, wie sie beispielsweise in [[Rechnerverbund|Computer-Clustern]] verarbeitet werden. Da der Speicher in solchen Systemen in der Regel keine limitierende Ressource darstellt, ist der Nachteil der Speicherkomplexität von Mergesort vernachlässigbar. Allerdings werden in solchen Systemen andere Faktoren wichtig, die bei der Modellierung auf einer [[Parallel Random Access Machine|PRAM]] nicht berücksichtigt werden. Hier sind unter anderem die folgenden Aspekte zu berücksichtigen: Die [[Speicherhierarchie]], wenn die Daten nicht in den Cache der Prozessoren passen, oder der Kommunikationsaufwand beim Datenaustausch zwischen den Prozessoren, der zu einem Engpass werden könnte, wenn auf die Daten nicht mehr über den gemeinsamen Speicher zugegriffen werden kann.<ref name=":0">{{Cite web|url=https://dl.acm.org/doi/abs/10.1145/2755573.2755595|title=Practical Massively Parallel Sorting {{!}} Proceedings of the 27th ACM symposium on Parallelism in Algorithms and Architectures|language=EN|website=dl.acm.org|doi=10.1145/2755573.2755595|access-date=2020-02-28}}</ref><br />
<br />
[[Peter Sanders|Sanders]] et al. haben in ihrem Paper einen [[Bulk Synchronous Parallel Computers|bulk synchronous parallel]]-Algorithmus für einen mehrstufigen Mehrwege-Mergesort vorgestellt, der <math>p</math> Prozessoren in <math>r</math> Gruppen der Größe <math>p'</math> unterteilt. Alle Prozessoren sortieren zuerst lokal. Im Gegensatz zu einem einstufigen Mehrwege-Mergesort werden diese Sequenzen dann in <math>r</math> Teile aufgeteilt und den entsprechenden Prozessorgruppen zugeordnet. Diese Schritte werden innerhalb dieser Gruppen rekursiv wiederholt. So wird die Kommunikation reduziert und insbesondere Probleme mit vielen kleinen Nachrichten vermieden. Die hierarchische Struktur des zugrundeliegenden realen Netzwerks (z.B. [[Rack|Racks]], [[Rechnerverbund|Cluster]],...) kann zur Definition der Prozessorgruppen verwendet werden.<br />
<br />
=== Weitere Varianten ===<br />
Mergesort war einer der ersten Sortieralgorithmen, bei dem ein optimaler [[Speedup]] erreicht wurde, wobei Richard Cole einen cleveren Subsampling-Algorithmus verwendete, um die O(1)-Zusammenführung sicherzustellen.<ref>{{Literatur |Autor=Richard Cole |Titel=Parallel Merge Sort |Sammelwerk=SIAM Journal on Computing |Band=17 |Nummer=4 |Datum=1988-08 |ISSN=0097-5397 |DOI=10.1137/0217049 |Seiten=770–785 |Online=http://epubs.siam.org/doi/10.1137/0217049 |Abruf=2020-03-06}}</ref> Andere ausgeklügelte parallele Sortieralgorithmen können die gleichen oder bessere Zeitschranken mit einer niedrigeren Konstante erreichen. David Powers beschrieb beispielsweise 1991 einen parallelisierten [[Quicksort]] (und einen verwandten [[Radixsort]]), der durch implizite Partitionierung in <math>O(\log n)</math> Zeit auf einer [[Parallel Random Access Machine|CRCW-Parallel Random Access Machine (PRAM)]] mit <math>n</math> Prozessoren arbeiten kann.<ref>Powers, David M. W. [http://citeseer.ist.psu.edu/327487.html Parallelized Quicksort and Radixsort with Optimal Speedup], ''Proceedings of International Conference on Parallel Computing Technologies''. [[Novosibirsk]]. 1991.</ref> Powers zeigt ferner, dass eine Pipeline-Version von Batchers Bitonic Mergesort in <math>O((\log n)^2)</math> Zeit auf einem Butterfly-Sortiernetzwerk in der Praxis schneller ist als sein <math>O(\log n)</math> Sortieralgorithmus auf einer PRAM, und er bietet eine detaillierte Diskussion der versteckten Overheads beim Vergleich, bei der Radix- und der Parallelsortierung.<ref>David M. W. Powers, [http://david.wardpowers.info/Research/AI/papers/199501-ACAW-PUPC.pdf Parallel Unification: Practical Complexity], Australasian Computer Architecture Workshop, Flinders University, January 1995</ref></div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Merge_algorithm_deutsch&diff=197520440Benutzer:ParAlgMergeSort/sandbox/Merge algorithm deutsch2020-03-06T20:55:08Z<p>ParAlgMergeSort: </p>
<hr />
<div><br /><br />
<br />
== Parallel merge ==<br />
A [[Task parallelism|parallel]] version of the binary merge algorithm can serve as a building block of a [[Merge sort#Parallel merge sort|parallel merge sort]]. The following pseudocode demonstrates this algorithm in a [[Fork-join model|parallel divide-and-conquer]] style (adapted from Cormen ''et al.''<ref name="clrs">{{Introduction to Algorithms|3}}</ref>{{rp|800}}). It operates on two sorted arrays {{mvar|A}} and {{mvar|B}} and writes the sorted output to array {{mvar|C}}. The notation {{mono|A[i...j]}} denotes the part of {{mvar|A}} from index {{mvar|i}} through {{mvar|j}}, exclusive.<br />
'''algorithm''' merge(A[i...j], B[k...ℓ], C[p...q]) '''is'''<br />
'''inputs''' A, B, C : array<br />
i, j, k, ℓ, p, q : indices<br />
<br />
'''let''' m = j - i,<br />
n = ℓ - k<br />
<br />
'''if''' m < n '''then'''<br />
swap A and B ''// ensure that A is the larger array: i, j still belong to A; k, ℓ to B''<br />
swap m and n<br />
<br />
'''if''' m ≤ 0 '''then'''<br />
'''return''' ''// base case, nothing to merge''<br />
<br />
'''let''' r = ⌊(i + j)/2⌋<br />
'''let''' s = binary-search(A[r], B[k...ℓ])<br />
'''let''' t = p + (r - i) + (s - k)<br />
C[t] = A[r]<br />
<br />
'''in parallel do'''<br />
merge(A[i...r], B[k...s], C[p...t])<br />
merge(A[r+1...j], B[s...ℓ], C[t+1...q])<br />
The algorithm operates by splitting either {{mvar|A}} or {{mvar|B}}, whichever is larger, into (nearly) equal halves. It then splits the other array into a part with values smaller than the midpoint of the first, and a part with larger or equal values. (The [[binary search]] subroutine returns the index in {{mvar|B}} where {{math|''A''[''r'']}} would be, if it were in {{mvar|B}}; that this always a number between {{mvar|k}} and {{mvar|ℓ}}.) Finally, each pair of halves is merged [[Divide and conquer algorithm|recursively]], and since the recursive calls are independent of each other, they can be done in parallel. Hybrid approach, where serial algorithm is used for recursion base case has been shown to perform well in practice <ref name="vjd">{{cite|author=Victor J. Duvanenko|title=Parallel Merge|journal=Dr. Dobb's Journal|date=2011|url=http://www.drdobbs.com/parallel/parallel-merge/229204454}}</ref><br />
<br />
The [[Analysis of parallel algorithms#Overview|work]] performed by the algorithm for two arrays holding a total of {{mvar|n}} elements, i.e., the running time of a serial version of it, is {{math|''O''(''n'')}}. This is optimal since {{mvar|n}} elements need to be copied into {{mvar|C}}. To calculate the [[Analysis of parallel algorithms#Overview|span]] of the algorithm, it is necessary to derive a [[Recurrence relation]]. Since the two recursive calls of P-Merge are in parallel, only the costlier of the two calls needs to be considered. In the worst case, the maximum number of elements in one of the recursive calls is at most <math display="inline">\frac 3 4 n</math> since the array with more elements is perfectly split in half. Adding the <math>\Theta\left( \log(n)\right)</math> cost of the Binary Search, we obtain this recurrence as an upper bound:<br />
<br />
<math>T_{\infty}^\text{merge}(n) = T_{\infty}^\text{merge}\left(\frac {3} {4} n\right) + \Theta\left( \log(n)\right)</math><br />
<br />
The solution is <math>T_{\infty}^\text{merge}(n) = \Theta\left(\log(n)^2\right)</math>, meaning that it takes that much time on an ideal machine with an unbounded number of processors.{{r|clrs}}{{rp|801–802}}<br />
<br />
'''Note:''' The routine is not [[Sorting algorithm#Stability|stable]]: if equal items are separated by splitting {{mvar|A}} and {{mvar|B}}, they will become interleaved in {{mvar|C}}; also swapping {{mvar|A}} and {{mvar|B}} will destroy the order, if equal items are spread among both input arrays. As a result, when used for sorting, this algorithm produces a sort that is not stable.</div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Merge_algorithm_deutsch&diff=197520417Benutzer:ParAlgMergeSort/sandbox/Merge algorithm deutsch2020-03-06T20:54:03Z<p>ParAlgMergeSort: AZ: Die Seite wurde neu angelegt: Parallel merge A parallel version of the binary merge algorithm can serve as a building block of a pa…</p>
<hr />
<div>Parallel merge<br />
<br />
A parallel version of the binary merge algorithm can serve as a building block of a parallel merge sort. The following pseudocode demonstrates this algorithm in a parallel divide-and-conquer style (adapted from Cormen et al.:800). It operates on two sorted arrays A and B and writes the sorted output to array C. The notation A[i...j] denotes the part of A from index i through j, exclusive.<br />
<br />
algorithm merge(A[i...j], B[k...ℓ], C[p...q]) is<br />
inputs A, B, C : array<br />
i, j, k, ℓ, p, q : indices<br />
<br />
let m = j - i,<br />
n = ℓ - k<br />
<br />
if m < n then<br />
swap A and B // ensure that A is the larger array: i, j still belong to A; k, ℓ to B<br />
swap m and n<br />
<br />
if m ≤ 0 then<br />
return // base case, nothing to merge<br />
<br />
let r = ⌊(i + j)/2⌋<br />
let s = binary-search(A[r], B[k...ℓ])<br />
let t = p + (r - i) + (s - k)<br />
C[t] = A[r]<br />
<br />
in parallel do<br />
merge(A[i...r], B[k...s], C[p...t])<br />
merge(A[r+1...j], B[s...ℓ], C[t+1...q])<br />
<br />
The algorithm operates by splitting either A or B, whichever is larger, into (nearly) equal halves. It then splits the other array into a part with values smaller than the midpoint of the first, and a part with larger or equal values. (The binary search subroutine returns the index in B where A[r] would be, if it were in B; that this always a number between k and ℓ.) Finally, each pair of halves is merged recursively, and since the recursive calls are independent of each other, they can be done in parallel. Hybrid approach, where serial algorithm is used for recursion base case has been shown to perform well in practice <br />
<br />
The work performed by the algorithm for two arrays holding a total of n elements, i.e., the running time of a serial version of it, is O(n). This is optimal since n elements need to be copied into C. To calculate the span of the algorithm, it is necessary to derive a Recurrence relation. Since the two recursive calls of P-Merge are in parallel, only the costlier of the two calls needs to be considered. In the worst case, the maximum number of elements in one of the recursive calls is at most <br />
{\textstyle {\frac {3}{4}}n}<br />
since the array with more elements is perfectly split in half. Adding the <br />
{\displaystyle \Theta \left(\log(n)\right)}<br />
cost of the Binary Search, we obtain this recurrence as an upper bound:<br />
<br />
{\displaystyle T_{\infty }^{\text{merge}}(n)=T_{\infty }^{\text{merge}}\left({\frac {3}{4}}n\right)+\Theta \left(\log(n)\right)}<br />
<br />
The solution is <br />
{\displaystyle T_{\infty }^{\text{merge}}(n)=\Theta \left(\log(n)^{2}\right)}<br />
, meaning that it takes that much time on an ideal machine with an unbounded number of processors.:801–802<br />
<br />
Note: The routine is not stable: if equal items are separated by splitting A and B, they will become interleaved in C; also swapping A and B will destroy the order, if equal items are spread among both input arrays. As a result, when used for sorting, this algorithm produces a sort that is not stable.</div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Parallel_merge_sort_deutsch&diff=197513399Benutzer:ParAlgMergeSort/sandbox/Parallel merge sort deutsch2020-03-06T16:46:52Z<p>ParAlgMergeSort: /* Paralleler Mehrwege-Mergesort */</p>
<hr />
<div>== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit enwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen erklärt werden, die Teile-phase und anschließend die Mischphase. Die erste besteht aus vielen rekursiven Aufrufen, in denen alle Teilungsprozesse aufrufen bis die Teilsequenzen trivial sortiert sind (das heißt nur ein oder kein Element enthalten). Ein intuitiver Ansatz wäre es, diese rekursiven Aufrufe zu parallelisieren. Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Ausführung der Rekursion indem die [[:en:Fork–join_model|fork and join]] Schlüsselwörter verwendet werden.<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.<br />
<br />
=== Mergesort mit paralleler Mischmethode ===<br />
Ein besserer Parallelismus kann durch eine parallele [[Merge-Algorithmen|Mischmethode]] erreicht werden. [[:en:Introduction_to_Algorithms|Cormen et al.]] präsentieren eine binäre Variante, welche zwei sortierte Teilsequenzen in eine sortierte Ausgabesequenz mischt.<br />
<br />
In der längeren der beiden Sequenzen (falls ungleich lang) wird das Element mit dem mittleren Index ausgewählt. Seien Position wird in der anderen Sequenz in der Weise gesucht, dass die Sequenz sortiert bleibt falls man dieses Element an dieser Position einfügen würde. So weiß man wie viele Elemente insgesamt kleiner sind als das Pivot Element und die finale Position des Pivots kann in der Ausgabesequenz bestimmt werden. Diese Mischmethode wird nun rekursiv auf die Teilsequenzen der kleineren und der größeren Elemente aufgerufen, bis der Basisfall der Rekursion erreicht ist. <br />
<br />
Der folgende Pseudocode illustriert die modifizierte parallele Mischmethode (aus Cormen et al.).<br />
<br />
/**<br />
* A: Input array<br />
* B: Output array<br />
* lo: lower bound<br />
* hi: upper bound<br />
* off: offset<br />
*/<br />
'''algorithm''' parallelMergesort(A, lo, hi, B, off) '''is'''<br />
len := hi - lo + 1<br />
'''if''' len == 1 '''then'''<br />
<nowiki> B[off] := A[lo]</nowiki><br />
'''else''' let T[1..len] be a new array<br />
mid := ⌊(lo + hi) / 2⌋ <br />
mid' := mid - lo + 1<br />
'''fork''' parallelMergesort(A, lo, mid, T, 1)<br />
parallelMergesort(A, mid + 1, hi, T, mid' + 1) <br />
'''join''' <br />
parallelMerge(T, 1, mid', mid' + 1, len, B, off)<br />
Um eine Rekurrenzrelation für den Worst Case zu erhalten müssen die rekursiven Aufrufe von parallelMergesort aufgrund der parallelen Ausführung nur einmal aufgeführt werden. Man erhält<br />
<br />
<math display="inline">T_{\infty}^{\text{sort} }(n) = T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + T_{\infty}^{\text{merge}}(n) <br />
= T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + \Theta \left( \log(n)^2\right)</math>.<br />
<br />
Die Lösung dieser Rekurrenz ist<br />
<br />
<math display="inline">T_{\infty}^{\text{sort}} = \Theta \left ( \log(n)^3 \right)</math>.<br />
<br />
Dieser Algorithmus erreicht eine Parallelisierbarkeit von <math>\Theta \biggr({n \over (\log n)^2}\biggr)</math>, was um einiges besser ist als der Parallelismus des vorherigen Algorithmus. Solch ein Sortieralgorithmus kann, wenn er mit einem schnellen stabilen sequentiellen Sortieralgorithmus und einer sequentiellen Mischmethode als Basisfall für das Mischen von zwei kleinen Sequenzen ausgestattet ist gut in der Praxis funktionieren.<br />
<br />
=== Paralleler Mehrwege-Mergesort ===<br />
Es wirkt unnatürlich, Mergesort Algorithmen auf binäre Mischmethoden zu beschränken, da oftmals mehr als zwei Prozessoren zur Verfügung stehen. Ein besserer Ansatz wäre es, ein [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] zu realisieren. Diese Generalisierung mischt <math>k</math> sortierte Sequenzen zu einer sortierten Sequenz und ist dementsprechend eine Generalisierung des binären Mischens. Diese Mischvariante ermöglicht es, einen Sortieralgorithmus im [[Parallel Random Access Machine|PRAM]]-Modell zu beschreiben.<br />
<br />
==== Grundidee ====<br />
[[File:Parallel_multiway_mergesort_process.svg|verweis=https://en.wikipedia.org/wiki/File:Parallel_multiway_mergesort_process.svg|alternativtext=|mini|Der parallele Mehrwege-Mergesort Algorithmus auf vier Prozessoren <math>t_0</math> bis <math>t_3</math>.]]<br />
Gegeben sei eine Folge von <math>n</math> Elementen. Ziel ist es, diese Sequenz mit <math>p</math> freien Prozessoren zu sortieren. Die Elemente sind dabei gleich auf alle Prozessoren aufgeteilt und werden zunächst lokal mit einem sequentiellen [[Sortierverfahren|Sortieralgorithmus]] vorsortiert. Dementsprechend bestehen die Daten nun aus sortierten Folgen <math>S_1, ..., S_p</math> der Länge <math display="inline">\lceil \frac{n}{p} \rceil</math>. Der Einfachheit halber sei <math>n</math> eine Vielfaches von <math>p</math>, so dass für <math>i = 1, ..., p</math> gilt: <math display="inline">\left\vert S_i \right\vert = \frac{n}{p}</math>. Jede dieser Sequenzen wird wiederum in <math>p</math> Teilsequenzen <math>S_{i,1}, ..., S_{i,p}</math> zerteilt. Dazu werden für <math>i = 1,..., p</math> die Trennelemente <math>v_i</math> mit globalem Rang <math display="inline">k = i \frac{n}{p}</math> bestimmt. Die korrespondierenden Indizes werden in jeder Folge <math>S_i</math> gesucht, sodass die Folge an diesem Index aufgeteilt werden kann. Dadurch ergibt sich <math>S_{i,j} := \{x \in S_i | rank(v_{j-1}) < rank(x) \le rank(v_j)\}</math>.<br />
<br />
Nun werden die Elemente von <math>S_{1,i}, ..., S_{p,i}</math>dem Prozessor <math>i</math> zugeteilt. Dies sind alle Elemente vom globalen Rang <math display="inline">(i-1) \frac{n}{p}</math> bis zum Rang <math display="inline">i \frac{n}{p}</math>. So erhält erhält jeder Prozessor eine Folge von sortierten Sequenzen. Aus der Tatsache, dass der Rang <math>k</math> der Trennelemente <math>v_i</math> global gewählt wurde, ergeben sich zwei wichtige Eigenschaften: Zunächst waren die Trennelemente so gewählt, dass jeder Prozessor nach der Zuteilung der neuen Daten immer noch mit <math display="inline">n/p</math> Elementen betraut ist Der Algorithmus besitzt also eine perfekte [[Lastverteilung (Informatik)|Lastverteilung]]. Außerdem sind alle Elemente des Prozessors <math>i</math> kleiner oder gleich der Elemente des Prozessors <math>i+1</math>. Wenn nun jeder Prozessor ein [[Merge-Algorithmen#k-Wege-Mischen|p-Wege-Mischen]] durchführt, sind aufgrund dieser Eigenschaft die Elemente global sortiert, das Ergebnis muss nur in der Reihenfolge der Prozessoren zusammengesetzt werden.<br />
<br />
==== Trennelementbestimmung ====<br />
In der einfachsten Form sind <math>p</math> sortierte Folgen <math>S_1, ..., S_p</math> gleichverteilt auf <math>p</math> Prozessoren sowie ein Rang <math>k</math> gegeben. Gesucht ist nun ein Trennelement <math>x</math> mit globalem Rang <math>k</math> in der Vereinigung der Folgen. Damit kann jede Folge <math>S_i</math> an einem Index <math>l_i</math> in zwei Teile aufgeteilt werden: Der untere Teil besteht nur aus Elementen, die kleiner <math>x</math> sind, während der obere Teil alle Elemente enthält, welche größer oder gleich als <math>x</math> sind. <br />
<br />
Der hier vorgestellte sequentielle Algorithmus gibt die Indizes der Trennungen zurück, also die Indizes <math>l_i</math>in den Folgen <math>S_i</math>, sodass <math>S_i[l_i]</math> einen global kleineren Rang als <math>k</math> hat und <math>\mathrm{rank}\left(S_i[l_i+1]\right) \ge k</math> ist.<br />
'''algorithm'''<nowiki> msSelect(S : Array of sorted Sequences [S_1,..,S_p], k : int) </nowiki>'''is'''<br />
'''for''' i = 1 '''to''' p '''do''' <br />
(l_i, r_i) = (0, |S_i|-1)<br />
<br />
'''while''' there exists i: l_i < r_i '''do'''<br />
//pick Pivot Element in S_j[l_j],..,S_j[r_j], chose random j uniformly<br />
v := pickPivot(S, l, r)<br />
'''for''' i = 1 '''to''' p '''do''' <br />
m_i = binarySearch(v, S_i[l_i, r_i]) //sequentially<br />
'''if''' m_1 + ... + m_p >= k '''then''' //m_1+ ... + m_p is the global rank of v<br />
r := m //vector assignment<br />
'''else'''<br />
l := m <br />
<br />
'''return''' l<br />
Für die Komplexitätsanalyse wurde das PRAM-Modell gewählt. Die p-fache Ausführung der ''binarySearch'' Methode hat eine Laufzeit in <math>\mathcal{O}\left(p\log\left(n/p\right)\right)</math>, falls die Daten über alle <math>p</math> Prozessoren gleichverteilt anliegen. Die erwartete Rekursionstiefe beträgt wie im [[Quickselect]] Algorithmus <math>\mathcal{O}\left(\log\left( \textstyle \sum_i |S_i| \right)\right) = \mathcal{O}(\log(n))</math>. Somit ist die gesamte erwartete Laufzeit <math>\mathcal{O}\left(p\log(n/p)\log(n)\right)</math>.<br />
<br />
Angewandt auf den parallelen Mehrwege-Mergesort muss die ''msSelect'' Methode parallel ausgeführt werden, um alle Trennelemente vom Rang <math display="inline"> i \frac n p</math> gleichzeitig zu finden. Dies kann anschließend verwendet werden, um jede Folge in <math>p</math> Teile zu zerschneiden. Es ergibt sich die gleiche Gesamtlaufzeit <math>\mathcal{O}\left(p\, \log(n/p)\log(n)\right)</math>.<br />
<br />
==== Pseudocode ====<br />
Hier ist der komplette Pseudocode für den parallelen Mehrwege-Mergesort. Dabei wird eine Barriere-Synchronisation vor und nach der Trennelementbestimmung angenommen, sodass jeder Prozessor seine Trennelemente und die Partitionierung seiner Sequenz richtig berechnen kann. <br />
/**<br />
* d: Unsorted Array of Elements<br />
* n: Number of Elements<br />
* p: Number of Processors<br />
* return Sorted Array<br />
*/<br />
'''algorithm''' parallelMultiwayMergesort(d : Array, n : int, p : int) '''is'''<br />
o := '''new''' Array[0, n] // the output array<br />
'''for''' i = 1 '''to''' p '''do in parallel''' // each processor in parallel<br />
<nowiki> S_i := d[(i-1) * n/p, i * n/p] // Sequence of length n/p</nowiki><br />
sort(S_i) // sort locally<br />
'''synch'''<br />
v_i := msSelect([S_1,...,S_p], i * n/p) // element with global rank i * n/p<br />
'''synch'''<br />
(S_i,1 ,..., S_i,p) := sequence_partitioning(si, v_1, ..., v_p) // split s_i into subsequences<br />
<br />
o[(i-1) * n/p, i * n/p] := kWayMerge(s_1,i, ..., s_p,i) // merge and assign to output array<br />
<br />
'''return''' o<br />
<br />
==== Analyse ====<br />
Zunächst sortiert jeder Prozessor die zugewiesenen <math>n/p</math> Elemente lokal mit einem vergleichsbasiertem Sortieralgorithmus der Komplexität <math>\mathcal{O}\left( n/p \; \log ( n/p) \right)</math>. Anschließend können die Trennelemente in Zeit <math>\mathcal{O}\left(p \,\log(n/p) \log (n) \right)</math> bestimmt werden. Schließlich müssen jede Gruppe von <math>p</math> Teilstücken gleichzeitig von jedem Prozessor zusammen gemischt werden. Dies hat eine Laufzeit von <math>\mathcal{O}(\log(p)\; n/p )</math>, indem ein sequentieller [[:en:Merge_algorithm|k-Wege Mischalgorithmus]] verwendet wird. Somit ergibt sich eine Gesamtlaufzeit von<br />
<br />
<math>\mathcal{O}\left( \frac n p \log\left(\frac n p\right) + p \log \left( \frac n p\right) \log (n) + \frac n p \log (p) \right)</math>.<br />
<br /></div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Parallel_merge_sort_deutsch&diff=197512848Benutzer:ParAlgMergeSort/sandbox/Parallel merge sort deutsch2020-03-06T16:30:17Z<p>ParAlgMergeSort: /* Paralleler Mehrwege-Mergesort */ Trennelementbestimmung</p>
<hr />
<div>== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit enwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen erklärt werden, die Teile-phase und anschließend die Mischphase. Die erste besteht aus vielen rekursiven Aufrufen, in denen alle Teilungsprozesse aufrufen bis die Teilsequenzen trivial sortiert sind (das heißt nur ein oder kein Element enthalten). Ein intuitiver Ansatz wäre es, diese rekursiven Aufrufe zu parallelisieren. Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Ausführung der Rekursion indem die [[:en:Fork–join_model|fork and join]] Schlüsselwörter verwendet werden.<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.<br />
<br />
=== Mergesort mit paralleler Mischmethode ===<br />
Ein besserer Parallelismus kann durch eine parallele [[Merge-Algorithmen|Mischmethode]] erreicht werden. [[:en:Introduction_to_Algorithms|Cormen et al.]] präsentieren eine binäre Variante, welche zwei sortierte Teilsequenzen in eine sortierte Ausgabesequenz mischt.<br />
<br />
In der längeren der beiden Sequenzen (falls ungleich lang) wird das Element mit dem mittleren Index ausgewählt. Seien Position wird in der anderen Sequenz in der Weise gesucht, dass die Sequenz sortiert bleibt falls man dieses Element an dieser Position einfügen würde. So weiß man wie viele Elemente insgesamt kleiner sind als das Pivot Element und die finale Position des Pivots kann in der Ausgabesequenz bestimmt werden. Diese Mischmethode wird nun rekursiv auf die Teilsequenzen der kleineren und der größeren Elemente aufgerufen, bis der Basisfall der Rekursion erreicht ist. <br />
<br />
Der folgende Pseudocode illustriert die modifizierte parallele Mischmethode (aus Cormen et al.).<br />
<br />
/**<br />
* A: Input array<br />
* B: Output array<br />
* lo: lower bound<br />
* hi: upper bound<br />
* off: offset<br />
*/<br />
'''algorithm''' parallelMergesort(A, lo, hi, B, off) '''is'''<br />
len := hi - lo + 1<br />
'''if''' len == 1 '''then'''<br />
<nowiki> B[off] := A[lo]</nowiki><br />
'''else''' let T[1..len] be a new array<br />
mid := ⌊(lo + hi) / 2⌋ <br />
mid' := mid - lo + 1<br />
'''fork''' parallelMergesort(A, lo, mid, T, 1)<br />
parallelMergesort(A, mid + 1, hi, T, mid' + 1) <br />
'''join''' <br />
parallelMerge(T, 1, mid', mid' + 1, len, B, off)<br />
Um eine Rekurrenzrelation für den Worst Case zu erhalten müssen die rekursiven Aufrufe von parallelMergesort aufgrund der parallelen Ausführung nur einmal aufgeführt werden. Man erhält<br />
<br />
<math display="inline">T_{\infty}^{\text{sort} }(n) = T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + T_{\infty}^{\text{merge}}(n) <br />
= T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + \Theta \left( \log(n)^2\right)</math>.<br />
<br />
Die Lösung dieser Rekurrenz ist<br />
<br />
<math display="inline">T_{\infty}^{\text{sort}} = \Theta \left ( \log(n)^3 \right)</math>.<br />
<br />
Dieser Algorithmus erreicht eine Parallelisierbarkeit von <math>\Theta \biggr({n \over (\log n)^2}\biggr)</math>, was um einiges besser ist als der Parallelismus des vorherigen Algorithmus. Solch ein Sortieralgorithmus kann, wenn er mit einem schnellen stabilen sequentiellen Sortieralgorithmus und einer sequentiellen Mischmethode als Basisfall für das Mischen von zwei kleinen Sequenzen ausgestattet ist gut in der Praxis funktionieren.<br />
<br />
=== Paralleler Mehrwege-Mergesort ===<br />
Es wirkt unnatürlich, Mergesort Algorithmen auf binäre Mischmethoden zu beschränken, da oftmals mehr als zwei Prozessoren zur Verfügung stehen. Ein besserer Ansatz wäre es, ein [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] zu realisieren. Diese Generalisierung mischt <math>k</math> sortierte Sequenzen zu einer sortierten Sequenz und ist dementsprechend eine Generalisierung des binären Mischens. Diese Mischvariante ermöglicht es, einen Sortieralgorithmus im [[Parallel Random Access Machine|PRAM]]-Modell zu beschreiben.<br />
<br />
==== Grundidee ====<br />
[[File:Parallel_multiway_mergesort_process.svg|verweis=https://en.wikipedia.org/wiki/File:Parallel_multiway_mergesort_process.svg|alternativtext=|mini|Der parallele Mehrwege-Mergesort Algorithmus auf vier Prozessoren <math>t_0</math> bis <math>t_3</math>.]]<br />
Gegeben sei eine Folge von <math>n</math> Elementen. Ziel ist es, diese Sequenz mit <math>p</math> freien Prozessoren zu sortieren. Die Elemente sind dabei gleich auf alle Prozessoren aufgeteilt und werden zunächst lokal mit einem sequentiellen [[Sortierverfahren|Sortieralgorithmus]] vorsortiert. Dementsprechend bestehen die Daten nun aus sortierten Folgen <math>S_1, ..., S_p</math> der Länge <math display="inline">\lceil \frac{n}{p} \rceil</math>. Der Einfachheit halber sei <math>n</math> eine Vielfaches von <math>p</math>, so dass für <math>i = 1, ..., p</math> gilt: <math display="inline">\left\vert S_i \right\vert = \frac{n}{p}</math>. Jede dieser Sequenzen wird wiederum in <math>p</math> Teilsequenzen <math>S_{i,1}, ..., S_{i,p}</math> zerteilt. Dazu werden für <math>i = 1,..., p</math> die Trennelemente <math>v_i</math> mit globalem Rang <math display="inline">k = i \frac{n}{p}</math> bestimmt. Die korrespondierenden Indizes werden in jeder Folge <math>S_i</math> gesucht, sodass die Folge an diesem Index aufgeteilt werden kann. Dadurch ergibt sich <math>S_{i,j} := \{x \in S_i | rank(v_{j-1}) < rank(x) \le rank(v_j)\}</math>.<br />
<br />
Nun werden die Elemente von <math>S_{1,i}, ..., S_{p,i}</math>dem Prozessor <math>i</math> zugeteilt. Dies sind alle Elemente vom globalen Rang <math display="inline">(i-1) \frac{n}{p}</math> bis zum Rang <math display="inline">i \frac{n}{p}</math>. So erhält erhält jeder Prozessor eine Folge von sortierten Sequenzen. Aus der Tatsache, dass der Rang <math>k</math> der Trennelemente <math>v_i</math> global gewählt wurde, ergeben sich zwei wichtige Eigenschaften: Zunächst waren die Trennelemente so gewählt, dass jeder Prozessor nach der Zuteilung der neuen Daten immer noch mit <math display="inline">n/p</math> Elementen betraut ist Der Algorithmus besitzt also eine perfekte [[Lastverteilung (Informatik)|Lastverteilung]]. Außerdem sind alle Elemente des Prozessors <math>i</math> kleiner oder gleich der Elemente des Prozessors <math>i+1</math>. Wenn nun jeder Prozessor ein [[Merge-Algorithmen#k-Wege-Mischen|p-Wege-Mischen]] durchführt, sind aufgrund dieser Eigenschaft die Elemente global sortiert, das Ergebnis muss nur in der Reihenfolge der Prozessoren zusammengesetzt werden.<br />
<br />
==== Trennelementbestimmung ====<br />
In der einfachsten Form sind <math>p</math> sortierte Folgen <math>S_1, ..., S_p</math> gleichverteilt auf <math>p</math> Prozessoren sowie ein Rang <math>k</math> gegeben. Gesucht ist nun ein Trennelement <math>x</math> mit globalem Rang <math>k</math> in der Vereinigung der Folgen. Damit kann jede Folge <math>S_i</math> an einem Index <math>l_i</math> in zwei Teile aufgeteilt werden: Der untere Teil besteht nur aus Elementen, die kleiner <math>x</math> sind, während der obere Teil alle Elemente enthält, welche größer oder gleich als <math>x</math> sind. <br />
<br />
Der hier vorgestellte sequentielle Algorithmus gibt die Indizes der Trennungen zurück, also die Indizes <math>l_i</math>in den Folgen <math>S_i</math>, sodass <math>S_i[l_i]</math> einen global kleineren Rang als <math>k</math> hat und <math>\mathrm{rank}\left(S_i[l_i+1]\right) \ge k</math> ist.<br />
'''algorithm'''<nowiki> msSelect(S : Array of sorted Sequences [S_1,..,S_p], k : int) </nowiki>'''is'''<br />
'''for''' i = 1 '''to''' p '''do''' <br />
(l_i, r_i) = (0, |S_i|-1)<br />
<br />
'''while''' there exists i: l_i < r_i '''do'''<br />
//pick Pivot Element in S_j[l_j],..,S_j[r_j], chose random j uniformly<br />
v := pickPivot(S, l, r)<br />
'''for''' i = 1 '''to''' p '''do''' <br />
m_i = binarySearch(v, S_i[l_i, r_i]) //sequentially<br />
'''if''' m_1 + ... + m_p >= k '''then''' //m_1+ ... + m_p is the global rank of v<br />
r := m //vector assignment<br />
'''else'''<br />
l := m <br />
<br />
'''return''' l<br />
Für die Komplexitätsanalyse wurde das PRAM-Modell gewählt. Die p-fache Ausführung der ''binarySearch'' Methode hat eine Laufzeit in <math>\mathcal{O}\left(p\log\left(n/p\right)\right)</math>, falls die Daten über alle <math>p</math> Prozessoren gleichverteilt anliegen. Die erwartete Rekursionstiefe beträgt wie im [[Quickselect]] Algorithmus <math>\mathcal{O}\left(\log\left( \textstyle \sum_i |S_i| \right)\right) = \mathcal{O}(\log(n))</math>. Somit ist die gesamte erwartete Laufzeit <math>\mathcal{O}\left(p\log(n/p)\log(n)\right)</math>.<br />
<br />
Angewandt auf den parallelen Mehrwege-Mergesort muss die ''msSelect'' Methode parallel ausgeführt werden, um alle Trennelemente vom Rang <math display="inline"> i \frac n p</math> gleichzeitig zu finden. Dies kann anschließend verwendet werden, um jede Folge in <math>p</math> Teile zu zerschneiden. Es ergibt sich die gleiche Gesamtlaufzeit <math>\mathcal{O}\left(p\, \log(n/p)\log(n)\right)</math>.</div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Parallel_merge_sort_deutsch&diff=197511094Benutzer:ParAlgMergeSort/sandbox/Parallel merge sort deutsch2020-03-06T15:28:03Z<p>ParAlgMergeSort: /* Paralleler Mergesort */</p>
<hr />
<div>== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit enwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen erklärt werden, die Teile-phase und anschließend die Mischphase. Die erste besteht aus vielen rekursiven Aufrufen, in denen alle Teilungsprozesse aufrufen bis die Teilsequenzen trivial sortiert sind (das heißt nur ein oder kein Element enthalten). Ein intuitiver Ansatz wäre es, diese rekursiven Aufrufe zu parallelisieren. Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Ausführung der Rekursion indem die [[:en:Fork–join_model|fork and join]] Schlüsselwörter verwendet werden.<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.<br />
<br />
=== Mergesort mit paralleler Mischmethode ===<br />
Ein besserer Parallelismus kann durch eine parallele [[Merge-Algorithmen|Mischmethode]] erreicht werden. [[:en:Introduction_to_Algorithms|Cormen et al.]] präsentieren eine binäre Variante, welche zwei sortierte Teilsequenzen in eine sortierte Ausgabesequenz mischt.<br />
<br />
In der längeren der beiden Sequenzen (falls ungleich lang) wird das Element mit dem mittleren Index ausgewählt. Seien Position wird in der anderen Sequenz in der Weise gesucht, dass die Sequenz sortiert bleibt falls man dieses Element an dieser Position einfügen würde. So weiß man wie viele Elemente insgesamt kleiner sind als das Pivot Element und die finale Position des Pivots kann in der Ausgabesequenz bestimmt werden. Diese Mischmethode wird nun rekursiv auf die Teilsequenzen der kleineren und der größeren Elemente aufgerufen, bis der Basisfall der Rekursion erreicht ist. <br />
<br />
Der folgende Pseudocode illustriert die modifizierte parallele Mischmethode (aus Cormen et al.).<br />
<br />
/**<br />
* A: Input array<br />
* B: Output array<br />
* lo: lower bound<br />
* hi: upper bound<br />
* off: offset<br />
*/<br />
'''algorithm''' parallelMergesort(A, lo, hi, B, off) '''is'''<br />
len := hi - lo + 1<br />
'''if''' len == 1 '''then'''<br />
<nowiki> B[off] := A[lo]</nowiki><br />
'''else''' let T[1..len] be a new array<br />
mid := ⌊(lo + hi) / 2⌋ <br />
mid' := mid - lo + 1<br />
'''fork''' parallelMergesort(A, lo, mid, T, 1)<br />
parallelMergesort(A, mid + 1, hi, T, mid' + 1) <br />
'''join''' <br />
parallelMerge(T, 1, mid', mid' + 1, len, B, off)<br />
Um eine Rekurrenzrelation für den Worst Case zu erhalten müssen die rekursiven Aufrufe von parallelMergesort aufgrund der parallelen Ausführung nur einmal aufgeführt werden. Man erhält<br />
<br />
<math display="inline">T_{\infty}^{\text{sort} }(n) = T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + T_{\infty}^{\text{merge}}(n) <br />
= T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + \Theta \left( \log(n)^2\right)</math>.<br />
<br />
Die Lösung dieser Rekurrenz ist<br />
<br />
<math display="inline">T_{\infty}^{\text{sort}} = \Theta \left ( \log(n)^3 \right)</math>.<br />
<br />
Dieser Algorithmus erreicht eine Parallelisierbarkeit von <math>\Theta \biggr({n \over (\log n)^2}\biggr)</math>, was um einiges besser ist als der Parallelismus des vorherigen Algorithmus. Solch ein Sortieralgorithmus kann, wenn er mit einem schnellen stabilen sequentiellen Sortieralgorithmus und einer sequentiellen Mischmethode als Basisfall für das Mischen von zwei kleinen Sequenzen ausgestattet ist gut in der Praxis funktionieren.<br />
<br />
=== Paralleler Mehrwege-Mergesort ===<br />
Es wirkt unnatürlich, Mergesort Algorithmen auf binäre Mischmethoden zu beschränken, da oftmals mehr als zwei Prozessoren zur Verfügung stehen. Ein besserer Ansatz wäre es, ein [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] zu realisieren. Diese Generalisierung mischt <math>k</math> sortierte Sequenzen zu einer sortierten Sequenz und ist dementsprechend eine Generalisierung des binären Mischens. Diese Mischvariante ermöglicht es, einen Sortieralgorithmus im [[Parallel Random Access Machine|PRAM]]-Modell zu beschreiben.<br />
<br />
==== Grundidee ====<br />
Gegeben sei eine Folge von <math>n</math> Elementen. Ziel ist es, diese Sequenz mit <math>p</math> freien Prozessoren zu sortieren. Die Elemente sind dabei gleich auf alle Prozessoren aufgeteilt und werden zunächst lokal mit einem sequentiellen [[Sortierverfahren|Sortieralgorithmus]] vorsortiert. Dementsprechend bestehen die Daten nun aus sortierten Folgen <math>S_1, ..., S_p</math> der Länge <math display="inline">\lceil \frac{n}{p} \rceil</math>. Der Einfachheit halber sei <math>n</math> eine Vielfaches von <math>p</math>, so dass für <math>i = 1, ..., p</math> gilt: <math display="inline">\left\vert S_i \right\vert = \frac{n}{p}</math>.</div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Parallel_merge_sort_deutsch&diff=197338990Benutzer:ParAlgMergeSort/sandbox/Parallel merge sort deutsch2020-03-02T08:49:55Z<p>ParAlgMergeSort: /* Paralleler Mergesort */</p>
<hr />
<div>== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit enwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen erklärt werden, die Teile-phase und anschließend die Mischphase. Die erste besteht aus vielen rekursiven Aufrufen, in denen alle Teilungsprozesse aufrufen bis die Teilsequenzen trivial sortiert sind (das heißt nur ein oder kein Element enthalten). Ein intuitiver Ansatz wäre es, diese rekursiven Aufrufe zu parallelisieren. Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Ausführung der Rekursion indem die [[:en:Fork–join_model|fork and join]] Schlüsselwörter verwendet werden.<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.<br />
<br />
=== Mergesort mit paralleler Mischmethode ===<br />
Ein besserer Parallelismus kann durch eine parallele [[Merge-Algorithmen|Mischmethode]] erreicht werden. [[:en:Introduction_to_Algorithms|Cormen et al.]] präsentieren eine binäre Variante, welche zwei sortierte Teilsequenzen in eine sortierte Ausgabesequenz mischt.<br />
<br />
In der längeren der beiden Sequenzen (falls ungleich lang) wird das Element mit dem mittleren Index ausgewählt. Seien Position wird in der anderen Sequenz in der Weise gesucht, dass die Sequenz sortiert bleibt falls man dieses Element an dieser Position einfügen würde. So weiß man wie viele Elemente insgesamt kleiner sind als das Pivot Element und die finale Position des Pivots kann in der Ausgabesequenz bestimmt werden. Diese Mischmethode wird nun rekursiv auf die Teilsequenzen der kleineren und der größeren Elemente aufgerufen, bis der Basisfall der Rekursion erreicht ist. <br />
<br />
Der folgende Pseudocode illustriert die modifizierte parallele Mischmethode (aus Cormen et al.).<br />
<br />
/**<br />
* A: Input array<br />
* B: Output array<br />
* lo: lower bound<br />
* hi: upper bound<br />
* off: offset<br />
*/<br />
'''algorithm''' parallelMergesort(A, lo, hi, B, off) '''is'''<br />
len := hi - lo + 1<br />
'''if''' len == 1 '''then'''<br />
<nowiki> B[off] := A[lo]</nowiki><br />
'''else''' let T[1..len] be a new array<br />
mid := ⌊(lo + hi) / 2⌋ <br />
mid' := mid - lo + 1<br />
'''fork''' parallelMergesort(A, lo, mid, T, 1)<br />
parallelMergesort(A, mid + 1, hi, T, mid' + 1) <br />
'''join''' <br />
parallelMerge(T, 1, mid', mid' + 1, len, B, off)<br />
Um eine Rekurrenzrelation für den Worst Case zu erhalten müssen die rekursiven Aufrufe von parallelMergesort aufgrund der parallelen Ausführung nur einmal aufgeführt werden. Man erhält<br />
<br />
<math display="inline">T_{\infty}^{\text{sort} }(n) = T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + T_{\infty}^{\text{merge}}(n) <br />
= T_{\infty}^{\text{sort}}\left(\frac {n} {2}\right) + \Theta \left( \log(n)^2\right)</math>.<br />
<br />
Die Lösung dieser Rekurrenz ist<br />
<br />
<math display="inline">T_{\infty}^{\text{sort}} = \Theta \left ( \log(n)^3 \right)</math>.<br />
<br />
Dieser Algorithmus erreicht eine Parallelisierbarkeit von <math>\Theta \biggr({n \over (\log n)^2}\biggr)</math>, was um einiges besser ist als der Parallelismus des vorherigen Algorithmus. Solch ein Sortieralgorithmus kann, wenn er mit einem schnellen stabilen sequentiellen Sortieralgorithmus und einer sequentiellen Mischmethode als Basisfall für das Mischen von zwei kleinen Sequenzen ausgestattet ist gut in der Praxis funktionieren.<br />
<br /></div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Parallel_merge_sort_deutsch&diff=197338544Benutzer:ParAlgMergeSort/sandbox/Parallel merge sort deutsch2020-03-02T08:31:25Z<p>ParAlgMergeSort: /* Paralleler Mergesort */ Hyperlinks</p>
<hr />
<div>== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des [[Teile-und-herrsche-Verfahren|Teile-und-herrsche]] Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit enwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das [[Merge-Algorithmen#k-Wege-Mischen|K-Wege-Mischen]] verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen erklärt werden, die Teile-phase und anschließend die Mischphase. Die erste besteht aus vielen rekursiven Aufrufen, in denen alle Teilungsprozesse aufrufen bis die Teilsequenzen trivial sortiert sind (das heißt nur ein oder kein Element enthalten). Ein intuitiver Ansatz wäre es, diese rekursiven Aufrufe zu parallelisieren. Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Ausführung der Rekursion indem die [[:en:Fork–join_model|fork and join]] Schlüsselwörter verwendet werden.<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen [[:en:Analysis_of_parallel_algorithms#Overview|Spann]] von <math>\Theta(n)</math>, was nur eine Verbesserung um den Faktor <math>\Theta(\log n)</math> ist im Vergleich zur sequentiellen Version (siehe auch [[:en:Introduction_to_Algorithms|Introduction to Algorithms]]). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.<br />
<br />
<br /></div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Parallel_merge_sort_deutsch&diff=197338368Benutzer:ParAlgMergeSort/sandbox/Parallel merge sort deutsch2020-03-02T08:25:31Z<p>ParAlgMergeSort: /* Paralleler Mergesort */</p>
<hr />
<div>== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des divide-and-conquer Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit enwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das K-Wege-Mischen verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen erklärt werden, die Teile-phase und anschließend die Mischphase. Die erste besteht aus vielen rekursiven Aufrufen, in denen alle Teilungsprozesse aufrufen bis die Teilsequenzen trivial sortiert sind (das heißt nur ein oder kein Element enthalten). Ein intuitiver Ansatz wäre es, diese rekursiven Aufrufe zu parallelisieren. Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Ausführung der Rekursion indem die fork-and-join Schlüsselwörter verwendet werden.<br />
// ''Sort elements lo through hi (exclusive) of array A.''<br />
'''algorithm''' mergesort(A, lo, hi) '''is'''<br />
'''if''' lo+1 < hi '''then''' // ''Two or more elements.''<br />
mid := ⌊(lo + hi) / 2⌋<br />
'''fork''' mergesort(A, lo, mid)<br />
mergesort(A, mid, hi)<br />
'''join'''<br />
merge(A, lo, mid, hi)<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen Spann von , was nur eine Verbesserung um den Faktor ist im Vergleich zur sequentiellen Version (siehe auch Introduction to Algorithms). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.</div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Parallel_merge_sort_deutsch&diff=197338329Benutzer:ParAlgMergeSort/sandbox/Parallel merge sort deutsch2020-03-02T08:23:50Z<p>ParAlgMergeSort: /* Paralleler Mergesort */</p>
<hr />
<div>== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des divide-and-conquer Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit enwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das K-Wege-Mischen verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen erklärt werden, die Teile-phase und anschließend die Mischphase. Die erste besteht aus vielen rekursiven Aufrufen, in denen alle Teilungsprozesse aufrufen bis die Teilsequenzen trivial sortiert sind (das heißt nur ein oder kein Element enthalten). Ein intuitiver Ansatz wäre es, diese rekursiven Aufrufe zu parallelisieren. Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Ausführung der Rekursion indem die fork-and-join Schlüsselwörter verwendet werden.<br />
<br />
<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen Spann von <br />
Θ(n)<br />
, was nur eine Verbesserung um den Faktor ist im Vergleich zur sequentiellen Version (siehe auch Introduction to Algorithms). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.</div>ParAlgMergeSorthttps://de.wikipedia.org/w/index.php?title=Benutzer:ParAlgMergeSort/sandbox/Parallel_merge_sort_deutsch&diff=197338274Benutzer:ParAlgMergeSort/sandbox/Parallel merge sort deutsch2020-03-02T08:21:50Z<p>ParAlgMergeSort: Deutscher Artikel erstellt</p>
<hr />
<div>== Paralleler Mergesort ==<br />
Mergesort lässt sich aufgrund des divide-and-conquer Ansatzes gut parallelisieren. Verschiedene parallele Varianten wurden in der Vergangenheit enwickelt. Manche sind stark verwandt mit der hier vorgestellten sequentiellen Variante, während andere eine grundlegend verschiedene Struktur besitzen und das K-Wege-Mischen verwenden.<br />
<br />
=== Mergesort mit parallelen Rekursionsaufrufen ===<br />
Der sequentielle Mergesort kann in zwei Phasen erklärt werden, die Teile-phase und anschließend die Mischphase. Die erste besteht aus vielen rekursiven Aufrufen, in denen alle Teilungsprozesse aufrufen bis die Teilsequenzen trivial sortiert sind (das heißt nur ein oder kein Element enthalten). Ein intuitiver Ansatz wäre es, diese rekursiven Aufrufe zu parallelisieren. Der folgende Pseudocode beschreibt den klassischen Mergesort Algorithmus mit paralleler Ausführung der Rekursion indem die fork-and-join Schlüsselwörter verwendet werden.<br />
<br />
<br />
Dieser Algorithmus ist die triviale Modifikation des sequentiellen Algorithmus und ist noch nicht optimal. Sein Speedup ist dementsprechend auch nicht beeindruckend. Er hat einen Spann von <br />
Θ(n)<br />
, was nur eine Verbesserung um den Faktor <br />
Θ(logn)<br />
ist im Vergleich zur sequentiellen Version (siehe auch Introduction to Algorithms). Dies liegt hauptsächlich an der sequentiellen Mischmethode, welche der Flaschenhals in der parallelen Ausführung ist.</div>ParAlgMergeSort