Algorithmen und Datenstrukturen Dipl. Inform. Andreas Wilkens 1 Organisatorisches Am Freitag, 9. Juni 2006, erfolgt in der Vorlesung eine Zusammenfassung der Vorlesungsinhalte als Vorbereitung auf die Klausur. besteht für Sie nochmal die Möglichkeit Fragen zu bestimmten Inhalten zu stellen. 2 1
Organisatorisches Am Freitag, 16. Juni 2006, findet keine VL statt, dafür 08.00 ca. 09.45 Uhr Präsentation Gruppe A bis D Raum S 309 10.00 ca. 11.45 Uhr Präsentation Gruppe E bis F Raum S 205 3 Quick Sort Eine weitere empfehlenswerte Internet-Seite mit Erläuterungen zu verschiedenen Sortieralgorithmen http://www.matheprisma.uni-wuppertal.de/module/sortieren/index.htm 4 2
Überblick Grundlagen Suchalgorithmen Sortieralgorithmen Analyse von Algorithmen 5 Analyse Warum analysiert man Algorithmen? Was muß bei Algorithmen analysiert werden? 6 3
Analyse Durch die Analyse will man den besten Algorithmus für ein gegebenes Problem ermitteln. Die Auswahl des besten Algorithmus kann sparen helfen: Zeit Ressourcen Geld 7 Analyse Was tatsächlich analysiert werden muß, hängt davon ab, was der Algorithmus tut. 8 4
Analyse Analyse der Sortieralgorithmen Selection Sort Insertion Sort Bubble Sort Quick Sort Was muß verglichen werden, um den besten Sortieralgorithmus zu finden? 9 Vergleich Vergleich von Laufzeit und/oder Speicherplatzbedarf in Abhängigkeit von der Größe der Eingabe Best-case-Analyse (Bester Fall) Average-case-Analyse (Mittel) Worst-case-Analyse (schlechtester Fall) 10 5
Laufzeit Wovon hängt die Laufzeit eines Sortieralgorithmus ab? 11 Laufzeit Die Laufzeit eines Sortieralgorithmus ist abhängig von: Anzahl der zu sortierenden Elemente Anzahl der Vergleiche Anzahl der Bewegungen eines Datensatzes 12 6
Speicherplatzbedarf Kann ein Sortieralgorithmus einen hohen (oder niedrigen) Speicherplatzbedarf haben? 13 Ordnung O Die Ordnung eines Algorithmus ist ein wichtiges Kriterium zur Bewertung seiner Effizienz. Sie gibt an, wie sich die Laufzeit und/oder der zusätzlich zu den vorhandenen Daten benötigte Speicherplatzbedarf in Abhängigkeit von der Länge der Eingabe vergrößern. 14 7
Ordnung O Man sagt: Die Laufzeit T(N) eines Algorithmus in Abhängigkeit von der Problemgröße N ist O(N). von der Größenordnung N. O(N) nennt man die Groß-O-Notation 15 Ordnung O Meist interessiert man sich jedoch nicht für den genauen Verlauf von T(N), sondern nur für dessen Größenordnung, und das auch nur für ''asymptotisch'', also für große N. Auch geht es in der Regel nur um eine obere Grenze für das Wachstum von T(N) mit wachsendem N. Je stärker T(N) mit N anwächst, umso größer ist die Komplexität des Algorithmus. 16 8
Ordnung O O( 1 ) : konstante Ordnung O( N ) : linear wachsende Ordnung O( log N ): logarithmisch wachsende Ordnung O( N 2 ) : quadratisch wachsende Ordnung O( 2 N ) : exponentiell wachende Ordnung 17 Selection Sort void selection_sort(int array[], int n) { int i, j, t, min; for( i=0; i<n-1; i++ ) { min = i; for( j=i+1; j<n; j++ ) { if( array[j] < array[min] ) min = j; t = array[min]; array[min] = array[i]; array[i] = t; return; Vergleiche? Datensatzbewegungen? Speicherplatzbedarf? 18 9
Selection Sort Analyse ergibt: ½ (n 2 n) Vergleiche und 3 (n 1) Datensatzbewegungen Zeitverhalten O( n 2 ) bei Vergleichen Zeitverhalten O(n) bei Datensatzbewegungen 19 Selection Sort Zeitverhalten im Best Case? Average Case? Worst Case? 20 10
Selection Sort Auch wenn der Algorithmus auf ein bereits sortiertes Array angewendet wird, ändert sich das Zeitverhalten nicht. Das Zeitverhalten im worst case, average case und best case ist unabhängig von den Eingabedaten. Es unterscheidet sich in allen drei Fällen nicht. 21 Selection Sort Wie ändert sich der Speicherplatzbedarf, wenn sich n ändert? 22 11
Insertion Sort void insertion_sort(int elem[], int n) { int key; int i, j; for( j=1; j<n; j++ ) { key = elem[j]; i = j-1; while( i>=0 && elem[i]>key ) { elem[i+1] = elem[i]; i--; // while elem[i+1] = key; // for return; 23 Insertion Sort Kann man sich Voraussetzungen bezüglich der Eingabedaten vorstellen, bei denen der Algorithmus besonders schnell / langsam läuft? 24 12
Insertion Sort Im schlechtesten Fall wird der Platz für das einzufügende Element immer erst ganz am Anfang des sortierten Teils gefunden. Dieser Fall tritt ein, wenn die Folge zu Anfang in absteigender Reihenfolge sortiert ist. In der While-Schleife werden dann Folgen der Länge 1, 2, 3,..., n-1 durchsucht. Zeitkomplexität O( n 2 ) 25 Insertion Sort Im besten Fall ist das Array bereits aufsteigend sortiert. In die while-schleife wird dann nie gesprungen, da die Bedingung elem[i]>key immer falsch ist. Zeitkomplexität O( n ) 26 13
Insertion Sort Wie ändert sich der Speicherplatzbedarf, wenn sich n ändert? 27 Bubble Sort void bubble_sort(int array[], int n) { int i, zw, ok; do { ok = 1; for (i=0; i<n-1; i++) { if (array[i] > array[i + 1]) { zw = array[i]; array[i] = array[i+1]; array[i+1] = zw; ok = 0; // if // for while (!ok); return; 28 14
Bubble Sort Im besten Fall ist die Folge bereits richtig sortiert. Dann sind keine Datenbewegungen nötig. Die for-schleife wird nur einmal von 0 bis n-2 durchlaufen, also (n-1)-mal Zeitkomplexität O( n ) 29 Bubble Sort Im schlechtesten Fall ist die Folge beim Aufruf von Bubble Sort absteigend sortiert. Dann sind sehr viele Datenbewegungen nötig: n-1 Vertauschungen für das erste Element n-2 Vertauschungen für das zweite Element n-3 Vertauschungen für das dritte Elemenet usw. Ebensoviele Vergleiche sind nötig. Zeitkomplexität O(n 2 ) 30 15
Bubble Sort Wie ändert sich der Speicherplatzbedarf, wenn sich n ändert? 31 Quick Sort void quick_sort(int array[], int left, int right) { int p; if( right>left ) { p = partition(array, left, right); quick_sort(array, left, p-1); quick_sort(array, p+1, right); return; 32 16
Quick Sort int partition(int array[], int left, int right) { int i, j, t, key; key = array[left]; i=left+1; j=right; for(;;) { while( array[i] <= key && i<right ) i++; while( array[j] >= key && j>left ) j--; if( i>=j ) break; t = array[i]; array[i] = array[j]; array[j] = t; // for t = array[j]; array[j] = array[left]; array[left] = t; return j; 33 Quick Sort Der Algorithmus verläuft optimal, wenn jeder Aufteilungsschritt im Verlauf der Rekursion jeweils etwa gleichlange Teilstücke erzeugt. In diesem günstigsten Fall beträgt die Rekursionstiefe log(n) und in jeder Schicht sind n Elemente zu behandeln. Zeitkomplexität O( n * log n ) (best case) 34 17
Quick Sort Der ungünstigste Fall tritt ein, wenn ein Teilstück stets nur aus einem Element und das andere aus den restlichen Elementen besteht. Die Rekursionstiefe ist dann n-1. Zeitkomplexität O( n 2 ) (worst case) 35 Quick Sort In unserer Implementierung von Quick Sort wird in der Funktion partition() immer das erste Element als Vergleichswert gewählt. Der ungünstigste Fall tritt dann ein, wenn die Folge beim Aufruf von Quick Sort bereits absteigend sortiert ist. Würde man in der Funktion partition() das letzte Element als Vergleichswert wählen, dann tritt der ungünstigste Fall ein, wenn die Folge beim Aufruf aufsteigend sortiert ist. 36 18
Quick Sort Viele praktische Tests und Analysen haben ergeben, dass Quick Sort eine durchschnittliche Laufzeit proportional zu besitzt. n * log n 37 Quick Sort Wie ändert sich der Speicherplatzbedarf, wenn sich n ändert? 38 19
Fazit Quick Sort ist in der Praxis das schnellste Sortierverfahren. Es gibt allerdings andere Verfahren, die einfacher zu implementieren sind. Quick Sort ist sehr anfällig bezüglich Programmierfehlern. Quick Sort ist nicht stabil, d.h. die relative Sortiertheit des Arrays bleibt nicht erhalten. (Bei gleichen Elementen tauschen diese die Reihenfolge im Array) 39 Fazit Selection Sort eignet sich z.b. für Datensätze mit kleinem Schlüssel aber umfangreichem und kompliziert strukturierten Datenteil, da hier nur sehr wenige Datenbewegungen erfolgen. 40 20
41 42 21
43 Verbesserungen für Quicksort Im Laufe der Jahre erschienen immer wieder Verbesserungsvorschläge für Quicksort. Meist war es jedoch so, dass Verbesserungen in einem Teil des Algorithmus zu Verschlechterungen der Laufzeit in anderen Teilen führten. 44 22
Verbesserungen für Quicksort Zwei Verbesserungen haben sich in der Praxis gut bewährt das Finden eines besseren Trennelements mit der Methode median-of-three die Behandlung kleiner Teildateien mit Insertion Sort (Quick Sort feat. Insertion) 45 median-of-three Erinnerung: Ungünstigster Fall, wenn Datenmenge absteigend sortiert ist, wobei immer das linke (erste) Element gewählt wird Frage: Welches Element müßte gewählt werden, damit aus diesem Worst Case der Best Case wird? 46 23
median-of-three Idee: vermeide den ungünstigsten Fall, indem immer ein besseres Element gewählt wird. Nehme drei Elemente aus dem Feld und wähle das (wertmäßig) mittlere von ihnen. Welche drei Elemente soll man nehmen? 47 median-of-three Welche drei Elemente soll man nehmen? Zufallsgenerator zur Auswahl der drei Elemente übertrieben einfache Lösung: nehme das erste, das mittlere und das letzte Element 48 24
median-of-three int middle = left + (right - left) / 2; if (array[right] > array[middle]) { tmp = array[right]; array[right] = array[middle]; array[middle] = tmp; if (array[right] > array[left]) { tmp = array[right]; array[right] = array[left]; array[left] = tmp; else if(array[left] > array[middle]) { tmp = array[left]; array[left] = array[middle]; array[middle] = tmp; 49 median-of-three Wofür sorgt der Quellcode auf der vorangegangenen Folie? An welche Stelle in unseren bisherigen Quick Sort Algorithmus muss der Quellcode von der vorangegangenen Folie eingefügt werden? 50 25
median-of-three In der Praxis wird die Chance, eines der mittleren Elemente zu treffen deutlich erhöht und somit erhöht sich natürlich auch die Wahrscheinlichkeit, dass ein Feld in zwei etwa gleich große Teilfelder zerlegt wird. Im Allgemeinen kann man von einer Geschwindigkeitssteigerung von 5% und (eher) mehr ausgehen. 51 Quick Sort feat. Insertion Wenn man sich den klassischen Quicksort anschaut, so stellt man fest, dass die Zerlegung (die rekursiven Funktionsaufrufe) für viele kleine Teilfelder bis hin zur Feldgröße 1 stattfindet. 52 26
Quick Sort feat. Insertion Um an Geschwindigkeit zu gewinnen, muss eine Möglichkeit gefunden werden, Quicksort für kleine Teilfelder schneller ablaufen zu lassen. 53 Quick Sort feat. Insertion Eine Möglichkeit, die oft angewandt wird, besteht darin, Insertion Sort auf ein Teilfeld loszulassen, falls die Größe des Feldes unter einen konstanten Schwellwert M sinkt. 54 27
void qsort_ins(int array[], int left, int right){ int i, j, tmp; if(right-left > M){ //Quicksort i=left-1; j=right; for(;;){ while(array[++i]<array[right]); while(array[--j]>array[right] && j>i); if(i>=j) break; tmp=array[i]; array[i]=array[j]; array[j]=tmp; tmp=array[i]; array[i]=array[right]; array[right]=tmp; qsort_ins(array, left, i-1); qsort_ins(array, i+1, right); else{ //insertion sort for(i=left+1; i<=right; ++i){ tmp=array[i]; for(j=i-1; j>=left && tmp<array[j]; --j) { array[j+1]=array[j]; array[j+1]=tmp; Quick Sort feat. Insertion Wenn die Bedingung if(right-left > M) nicht mehr erfüllt ist dann ist das Teilfeld "zu klein" für Quicksort dann wird für das behandelte Teilfeld Insertion Sort aufgerufen ansonsten findet weiterhin das klassische Quicksort Anwendung. 56 28
Quick Sort feat. Insertion Was bringt's? Mit dieser Methode lassen sich viele rekursive Aufrufe sparen. In Abhängigkeit von M kann man Laufzeitverbesserungen von 20% und mehr beobachten. 57 Quick Sort feat. Insertion Optimale Wahl von M Bleibt noch die Frage offen, wie groß man den Schwellwert M festlegen sollte. Das folgende Diagramm zeigt die Laufzeit bei N=10 Mio. bis N=60 Mio. Elementen in Abhängigkeit von M=0 bis M=100: 58 29
Quick Sort feat. Insertion 59 Quick Sort feat. Insertion Diagramm zeigt M scheint unabhängig von N zu sein beste Ergebnisse ca. bei M=25 Diesen Wert sollte man auch für M wählen, um die Verbesserung mit Insertion Sort optimal wirken zu lassen. 60 30
Algorithmen und Datenstrukturen Ende 61 31