|
Antwort |
Registriert seit: 30. Mai 2002
Bitte beachten: Dieses Tutorial muss überarbeitet werden, dazu fehlt im Moment aber einfach die Zeit.sorry
Sortier-Algorithmen Eines der grundlegenden Probleme der Informatik: Das Sortieren einer Menge an Daten. Es gibt viele verschiedene Verfahren, die sich mehr oder weniger gut für den Einsatz in einem Programm eignen. Man kann Sortierverfahren grob in zwei Klassen unterteilen: In die sogenannten internen Sortierverfahren und in die externen Sortierverfahren. Ein internes Sortierverfahren zeichnet sich dadurch aus, dass die zu sortierenden Daten im Speicher (z.B. in einem Array) untergebracht werden. Der Zugriff auf jeden einzelnen Datensatz ist hier leicht und vor allem schnell realisiert. Das genaue Gegenteil haben wir bei den externen Sortierverfahren, bei denen die zu sortierenden Daten auf einem (externen) Speichermedium vorliegen und der Zugriff sequentiell oder blockweise erfolgen muss. Ich werde im Rahmen dieses Tutorials lediglich auf interne Verfahren eingehen. Es sind im Wesentlichen diejenigen Verfahren, die als die Standard-Verfahren bekannt sind:
Bei weniger als zweitausend Datensätzen im Speicher macht es oftmals keinen Sinn, sich große Gedanken über einen schnellen Sortier-Algorithmus zu machen. Die Unterschiede liegen im Bereich von unter einer Sekunde. Um die oben genannten Algorithmen in Bezug auf die drei wesentlichen Kenngrößen analysieren zu können, werde ich sie jetzt hier der Reihe nach vorstellen. Zuvor noch eine kurze Bemerkung zur den Beispiel-Codes: Die Anzahl der zu sortierenden Elemente bezeichne ich mit dem Buchstaben N. (Dies ist die in der Fachliteratur gängige Methode). Zudem unterstelle ich als Datenfeld ein Array, welches von 1..N indiziert ist. Selection-Sort "Sortieren durch direktes Auswählen" Der Selection-Sort ist einer der einfachsten Sortier-Algorithmen und läuft nach folgendem Schema ab: Finde zuerst das kleinste Element und tausche es gegen das an erster Stelle befindliche Element aus, finde danach das zweitkleinste Element und tausche es gegen das an zweiter Stelle befindliche Element aus und setze dies so lange fort, bis das gesamte Feld sortiert ist.
Code:
Ich werde später noch darauf eingehen, dass dieser Algorithmus nicht einer der schnellsten ist. Dennoch hat er eine Eigenschaft, die sich unter Umständen als sehr positiv erweisen kann: Jeder Datensatz wird höchstens einmal verschoben. Gerade bei sehr großen Datensätzen kann dies ein Vorteil sein.
Procedure SelectionSort;
var i, j, min : Integer; Begin For i:= 1 to N-1 Do Begin min:= i; For j:= i+1 To N Do If (Data[j] < Data[min]) Then min:= j; SwapValues( i, min); End; End; Insertion-Sort "Sortieren durch direktes Einfügen" Dieser Algorithmus ist fast so einfach wie der Selection-Sort, jedoch ein wenig flexibler. Dies ist eine Methode, die Menschen oft beim Kartenspiel anwenden, um die auf ihrer Hand befindlichen Karten zu sortieren: Betrachte die Elemente eines nach dem anderen und fügen jedes an seinen richtigen Platz zwischen den bereits betrachteten Elementen ein. Hierzu wird erst eine Lücke geschaffen (die größeren Elemente werden nach rechts gerückt) und dann kann man auf den frei gewordenen Platz das aktuelle Element einfügen.
Code:
Procedure InsertionSort;
var i,j,v : Integer; Begin For i:= 2 To N Do Begin v:= Data[i]; j:= i; While (j > 1) and (Data[j-1] > v) Do Begin Data[j]:= Data[j-1]; dec( j ); End; Data[j]:= v; End; End; Bubble-Sort "Sortieren durch direktes Austauschen" Dieser Algorithmus ist bestimmt in jedem Informatik-Grundkurs und jeder Informatik-Vorlesung gelehrt worden. Er gehört eindeutig zu den gemütlichen Sortier-Algorithmen. Schon bei 100.000 Elementen kann sich erst mal einen Kaffee holen, bevor dieser Algorithmus mit seiner Arbeit fertig ist. Trotzdem ist er leicht zu begreifen: Durchlaufe immer wieder das Feld und tausche wenn nötig zwei benachbarte Elemente miteinander aus.
Code:
Procedure BubbleSort;
var i,j : Integer; Begin For i:= N downto 1 Do For j:= 1 To i Do If (Data[j-1] > Data[j]) Then SwapValues( j-1, j ); End; Shell-Sort Der Shell-Sort ist eine Erweiterung des Insertion-Sort und setzt genau dort an, wo dieser seine Schwäche hat. Insertion-Sort ist langsam, weil stets nur benachbarte Elemente ausgetauscht werden. Steht zufälligerweise das kleinste Element ganz hinten, so werden N Vertauschungen benötigt, um es an seine richtige Position zu bringen. Der Shell-Sort vertauscht also auch weit voneinander entferne Elemente miteinander. Diese Distanz wird als "h" bezeichnet. Der Grundgedanke besteht nun darin, dass man die Daten so umordnet, dass man eine sortierte Reihenfolge erhält, wenn man jedes h-te Element entnimmt. Lässt man dieses "h" nun gegen 1 laufen, wird nach und nach die gesamte Datenmenge sortiert.
Code:
Procedure ShellSort;
var i, j, h, v : Integer; Begin h:= 1; Repeat h:= (3 * h) +1; Until (h > N); Repeat h:= (h div 3); For i:= (h+1) To N Do Begin v:= Data[i]; j:= i; While ((j > h) and (Data[j-h] > v)) Do Begin Data[j]:= Data[j-h]; dec( j, h ); End; Data[j]:= v; End; Until (h = 1); End; Quick-Sort Der Quick-Sort wurde 1960 entwickelt und dürfte einer der am häufigsten angewandten Sortier-Algorithmen sein. Seinen Namen verdient er der Tatsache, dass er für das Sortieren von N Elementen im Durchschnitt nur N * log (N) Operationen benötigt. (Ob das viel oder wenig ist, werden wir noch sehen) Der Quick-Sort kann sowohl rekursiv als auch iterativ programmiert werden. In rekursiver Variante belastet er den Stack-Speicher - dies kann bei großen Datenmengen zu einem Stack-Überlauf führen. In iterativer Variante läuft er zwar ein wenig langsamer, belastet dafür aber nur den Heap, welcher üblicherweise weniger beschränkt ist als der Stack. Einen gravierenden Nachteil hat der Quick-Sort allerdings auch: Im ungünstigsten Fall benötigt er N*N Operationen, um die sortierte Reihenfolgen herzustellen. In diesem Fall zeigt er ein ähnlich schlechtes Verhalten wie der Bubble-Sort. Das Prinzip des Quick-Sort beruht auf dem Prinzip, die Daten in Teilmengen aufzuspalten und diese unabhängig voneinander zu sortieren. Die rekursive Variante des Quick-Sort hat folgenden Rumpf:
Code:
Von entscheidender Bedeutung ist hierbei die Prozedur "Partition". Diese muss das Datenfeld so umsortieren, dass drei Bedingungen erfüllt sind:
Procedure QuickSort( l,r : Integer );
var i : Integer; Begin If (r > l) Then Begin i:= Partition( l, r); QuickSortRekursiv( l, i-1 ); QuickSortRekursiv( i+1, r ); End; End;
Diese Prozedur setzt i = r und wählt somit willkürlich das Element ganz rechts (also ist Data[ i ] = Data[ r ]) und sucht jetzt von links beginnend ein Element, welches größer als Data[ i ] ist. Nun wird von rechts beginnend ein Element gesucht, welches kleiner als Data[ i ] ist. Beide gefundenen Elemente werden vertauscht. Fährt man so fort, ist sicher gestellt, dass die Teildatenmenge sortiert ist und dass das anfangs ausgewählte Element an seiner endgültigen Position liegt. Je näher i in der Mitte des Feldes liegt, desto erfolgreicher war die Partitionierung. (An dieser Stelle werden wir später noch optimieren)
Code:
Man kann wie bereits erwähnt die Rekursion entfernen. Man muss jedoch noch eine Hilfs-Struktur schaffen, in welcher man sich die Indizes der Teilmengen merkt. Hier bietet sich die Datenstruktur "Stack" an. (Ich setzte diese als bekannt voraus, kann aber auf Wunsch gerne einmal tiefer auf "Stacks" eingehen)
Function Partition( l,r : Integer ) : Integer;
var v,t,i,j : Integer; Begin v:= Data[r]; i:= l-1; j:= r; Repeat Repeat inc( i ); Until (Data[i] >= v); Repeat dec( j ); Until (Data[j] <= v); t:= Data[i]; Data[i]:= Data[j]; Data[j]:= t; Until (j<=i); Data[j]:= Data[i]; Data[i]:= Data[r]; Data[r]:= t; Result:= i; End; Die rekursiven Aufrufe werden also durch Stack-Operationen ersetzt und es wird eine innere Schleife hinzugefügt, welche solange läuft, bis der Stack vollständig abgearbeitet ist.
Code:
Wichtig hier bei ist, dass die Indizes der Teilmengen nicht in beliebiger Reihenfolge abgelegt werden, sondern dass die Indizes der größeren Teilmenge immer zuerst auf den Stack geschrieben werden. Dies bewirkt, dass der Stack im Durchschnitt lediglich für log(N) Einträge Platz bieten muss. Im ungünstigsten Fall könnte die Belastung des Stacks N erreichen.
Procedure QuickSortIterativ;
var i, l, r : Integer; Begin l:= 1; r:= N; Stack.Push( l ); Stack.Push( r ); Repeat If (r > l) Then Begin i:= Partition( l, r ); If (i-l) > (r-i) Then Begin Stack.Push( l ); Stack.Push( i-1 ); l:= i+1; End Else Begin Stack.Push( i+1 ); Stack.Push( r ); r:= i-1; End; End Else Begin r:= Stack.Pop; l:= Stack.Pop; End; Until StackisEmpty; End; Heap-Sort (Es sind jetzt Vorkenntnisse in Bezug auf Bäume und deren Repräsentation als Feld (Array) nötig. Auf Wunsch kann ich gerne einmal näher darauf eingehen) Der Heap-Sort basiert auf der Datenstruktur "Heap". Darunter versteht man einen Binärbaum, der die sog. Heap-Bedingung erfüllt. Unter dieser Heap-Bedingung versteht man die Eigenschaft, dass der größte (oder der kleinste) Wert stets an der Wurzel steht und dass alle folgenden Knoten die Bedingung erfüllen, dass der Wert jedes Knotens größer (oder kleiner) gleich dem seiner Nachfolger ist. Das Prinzip des Heap-Sort basiert darauf, aus den zu sortierenden Daten einen Heap zu konstruieren, dann die Elemente nacheinander vom Heap zu entfernen und nach jedem Entfernen die Heap-Bedingung wieder herzustellen.
Code:
Von zentraler Bedeutung ist die Methode "downHeap", welche die Heap-Bedingung wider herstellt:
Procedure HeapSort;
var i, k, m : Integer; Begin m:= N; k:= m div 2; For i:= k downto 1 Do downHeap( i, m ); While (m > 1) Do Begin SwapValues( 1, m ); dec( m ); downHeap( 1, m ); End; End;
Code:
Der Heapsort hat den eindeutigen Vorteil, dass er ohne zusätzlichen Speicher auskommt und zudem selbst im ungünstigsten Fall noch eine nahezu konstante Laufzeit von N * log(N) garantiert. (Ich gehe später darauf ein, wie dieser Umstand zu bewerten ist)
Procedure downHeap( index, heapSize : Integer );
var j, k, m, v : Integer; Begin k:= index; v:= Data[k]; m:= heapSize; While (k <= (m div 2)) Do Begin j:= 2*k; If (j < n) Then If (Data[j] < Data[j+1]) Then inc( j ); If (v > Data[j]) Then Begin Data[k]:= v; Exit; End; SwapValues( k, j ); k:= j; End; End; Merge-Sort Der Merge-Sort sortiert eine Datenmenge, indem er sie in Hälften teilt, diese dann (rekursiv) sortiert und anschließend zusammenfügt. Der Mergesort hat eine Eigenschaft, die als wesentlicher Vorteil gesehen werden kann: Man kann ihn so implementieren, dass der Zugriff auf die Daten hauptsächlich sequentiell erfolgt. Zum Beispiel ist der Mergesort ein gern genutztes Sortier-Verfahren für verkettete Listen, bei denen ein sequentieller Zugriff die einzig mögliche Zugriffsart ist. Leider hat er aber auch einen Nachteil: Er benötigt proportional zu N weiteren Speicher. (Im Code-Beispiel durch 'HilfsArray' dargestellt)
Code:
Das war also ein erster Überblick über die Standard-Sortierverfahren. Ich hoffe, dass ich mich einigermaßen verständlich ausgedrückt habe. Für Fragen und Anmerkungen bin ich natürlich jederzeit offen.
Procedure MergeSort( l, r : Integer );
var i, j, k, m : Integer; Begin If (l < r) Then Begin m:= (r+l) div 2; MergeSort( l, m ); MergeSort( m+1, r ); For i:= l To m Do HilfsArray[i]:= Data[i]; i:= l; For j:= m+1 To r Do HilfsArray[r+m+1-j]:= Data[j]; j:= r; For k:= l To r Do Begin If (HilfsArray[i] < HilfsArray[j]) Then Begin Data[k]:= HilfsArray[i]; inc( i ); End Else Begin Data[k]:= HilfsArray[j]; dec( j ); End; End; End; End; Im nächsten Teil werde ich die vorstellten Algorithmen hinsichtlich der Laufzeit analysieren und vergleichen. In diesem Zusammenhang werde ich auch Varianten des Quick- und Heap-Sorts vorstellen, welche Laufzeit drastisch verbessern. Dann wird es auch das vollständige Programm-Listing zum Download geben. Grüße, Daniel |
Delphi 10.4 Sydney |
#11
Ahem ... Du hast natürlich recht. Da ist noch ein Wurm drin. Ich bin gerade dabei, dieses Tutoral samt Quellcodes zu überarbeiten. Ich gebe Bescheid, wenn ich soweit bin.
Grüße, Daniel
Daniel R. Wolf
|
Zitat |
|
#12
Ich muß sagen, daß die Beiträge sehr hilfreich sind. Gerade, wenn man einen Sortieralgorithmus für einen bestimmten Zweck sucht, sind die Vergleiche und Optimierungsvorschläge sehr nützlich.
Nun habe ich aber ein spezielles Problem. Ich möchte gerne eine Liste sortieren, die aber nicht ganz in den vom Programm nutzbaren Speicher passt. Das heißt, daß nach wenigen bis einigen hundert Einträgen ein Speicherseitenwechsel notwendig wird. Die einzelnen Listeneinträge können zudem bis zu 3KB groß sein. Welchen Algorithmus nehme ich am besten? Ich denke der Quicksort mit Optimierung, wie er hier dargestellt wird, ist noch am besten geeignet, weil er auch für große Datenmengen am schnellsten ist. Allerdings nur dann, wenn man auf verkettete Listen zurückgreift. Bei verketteten Listen ergibt sich aber das Problem, aus einem Bereich jeweils den mittleren Eintrag zu finden, weil ja die Position eines Eintrags in der Liste nicht der tatsächlichen Position in der Liste entspricht. Das ist nur ganz zu Anfang so, wenn die Liste neu erstellt wird. Man müßte also eigentlich, jedesmal, wenn man den mittleren Eintrag sucht, die verkettete Liste sequentiell so lange durchlaufen, bis man beim mittleren Eintrag angekommen ist. Aber gerade bei großen Datenmengen (100.000 Einträge zu je 508 Byte Größe z.B.) wo dann auch immer noch ein Speicherseitenwechsel zwischendurch notwendig wird, bei dem die Daten eventuell auch noch von Festplatte eingelagert werden müssen, bremst das doch den ganzen Algorithmus aus. Oder sehe ich das falsch? Wie kann man denn, vielleicht mit einem Trick, den mittleren Eintrag einer verketteten Liste schnell finden, also ohne das sequenzielle Suchen? Oder gibt es vielleicht noch andre effektivere Ansätze zur Sortierung solcher von mir beschriebener Datenmengen? Torsten |
Zitat |
Delphi 2009 Professional |
#13
Hi Daniel,
sehr sehr nützlich, was du da "offenbarst". Ich hätt vor allen Dingen nie gedacht dass es so viele verschiedene Möglichkeiten gibt, Arrays zu sortieren. Jetz hätt ich zwei Fragen: 1.: Bin ich nur zu blöd, oder hast du den QuickSortRekursiv weggelassen? Der würd mich mal interessieren, weil der laut deinen "Benchmarkwerten" ja doch um einiges schneller ist als der itverative, obwohl er genauso viele Vergleiche macht. 2.: Hast dus aufgegeben, oder dauert das wirklich so lange, dieses Tutorial zu überarbeiten? Bis dann, S - tefano |
Zitat |
Delphi 10.4 Sydney |
#14
hm. Das ist mir jetzt direkt unangenehm. Ich habe aufgrund verschiedener laufender Projekte diese Algorithmen ein wenig aus den Augen verloren. Ein neues und erweitertes Delphi-Projekt mit diesen Algorithmen liegt zu dreiviertel fertig auf meiner Festplatte. Die Quicksorts werden dann auch alle vollständig sein.
Ich kann aber zum gegenwärtigen Zeitpunkt kein Datum der Fertigstellung angeben. Leider.
Daniel R. Wolf
|
Zitat |
Delphi 2009 Professional |
#15
Hi,
naja, solange ich mich (und die anderen sich) drauf freuen kann/können, bald (oder halt irgendwann) das neue Tutorial mit vollständigem QuickSort zu sehen, ist es ja nicht so schlimm. Is schönmal schön dass das Ganze auch nach "so langer Zeit" nicht einstaubt sondern wenigstens halbwegs in Arbeit ist. Bis dann, S - tefano |
Zitat |
Delphi 5 Enterprise |
#16
ein kleiner Tipp: Mir ist aufgefallen dass in den meisten guten quicksortimplementierungen DIV verwendet wird. Leider ist DIV sehr langsam. Shr 1 (den Wert 1 Byte rechts schieben) ist 5-10 mal schneller. Bei großen Arrays kann man damit schon eine ganze Menge einsparen. Ein Kumpel von mir (der ist ein delphi- und asmfreak) hat das getestet. INC und DEC sind auch langsamer als i := i + 1; oder i := i - 1; z.B. Das bringt zwar sogut wie nichts, aber schneller ists so trotzdem.
Scheinbar wird in sämtlichen Delphialgorithmen von quicksort div verwendet. Das kann ich nicht verstehen. Egal Ansonsten frag ich mich, obs nicht auch eine Assemblerimplementierung von Quicksort gibt. Schneller sein dürfte die aber wohl kaum. Die Tausche-Methode kann man in Quicksort auch durch die drei direkten Befehle ersetzen. So hat man 2 Codejumps weniger für den Prozessor pro Durchlauf. Alles in allem lässt sich Quicksort so wie es oben ist noch ganz schön optimieren. |
Zitat |
Delphi 12 Athens |
#17
Zitat von ShadowCaster:
Leider ist DIV sehr langsam. Shr 1 (den Wert 1 Byte rechts schieben) ist 5-10 mal schneller.
Zitat von ShadowCaster:
INC und DEC sind auch langsamer als i := i + 1; oder i := i - 1; z.B. Das bringt zwar sogut wie nichts, aber schneller ists so trotzdem.
......
Daniel Lizbeth
|
Zitat |
Delphi 10.4 Sydney |
#18
Hallo,
es ging ja hierbei auch nicht darum, die vermeintlich ideale Delphi-Implementation zu finden, sondern den Algorithmus als solchen zu verbessern. (Ob ich in diesem Leben noch jemals die Zeit finden werde, dieses Tut zu überarbeiten?... )
Daniel R. Wolf
|
Zitat |
Delphi 5 Enterprise |
#19
naja, habs ja nur gut gemeint mein Kumpel hat das nur in einer großen Schleife getestet und bei ihm war i := i + 1; minimal schnell in delphi als inc(i) bei 100000 Schleifendurchläufen. Er hat Delphi 4. Das mit dem Div hat er auch getestet und da wars sehr groß, der Unterschied. Kann sein, dass er sich irrt. Jedenfalls hab ich heut wieder ne Menge dazugelernt.
|
Zitat |
Ansicht |
Linear-Darstellung |
Zur Hybrid-Darstellung wechseln |
Zur Baum-Darstellung wechseln |
ForumregelnEs ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.
BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus. Trackbacks are an
Pingbacks are an
Refbacks are aus
|
|
Nützliche Links |
Heutige Beiträge |
Sitemap |
Suchen |
Code-Library |
Wer ist online |
Alle Foren als gelesen markieren |
Gehe zu... |
LinkBack |
LinkBack URL |
About LinkBacks |