Analyse von Algorithmen Die O-Notation WS 2012/2013 Prof. Dr. Margarita Esponda Freie Universität Berlin 1
Korrekte und effiziente Lösung von Problemen Problem Wesentlicher Teil der Lösung eines Problems. Sehr kreative Phase Algorithmus Programmierung oder Codierung des Algorithmus in einer bestimmten Programmiersprache. Programm in einer höheren Programmiersprache Einfache Phase 2
Analyse von Algorithmen Korrektheit Wichtigste Eigenschaften bei der Analyse von Algorithmen Terminierbarkeit Rechenzeit Komplexität Speicherplatz Bandbreite oder Datentransfer 3
Analyse von Algorithmen Rechenzeit Anzahl der durchgeführten Elementaroperationen in Abhängigkeit von der Eingabegröße. Speicherplatz Maximaler Speicherverbrauch während der Ausführung des Algorithmus in Abhängigkeit von der Komplexität der Eingabe. Bandbreite Wie groß ist die erforderliche Datenübertragung. 4
Analyse von Algorithmen Charakterisierung unserer Daten (Eingabegröße) Bestimmung der abstrakten Operationen Zeitanalyse (Berechnungsschritte in unserem Algorithmus) Eigentliche mathematische Analyse, um eine Funktion in Abhängigkeit der Eingabegröße zu finden. Komplexitätsanalyse 5
Eingabedaten Zuerst müssen wir unsere Eingabedaten charakterisieren. Meistens ist es sehr schwer eine genaue Verteilung der Daten zu finden, die dem realen Fall entspricht. Deswegen müssen wir in der Regel den schlimmsten Fall betrachten und auf diese Weise eine obere Schranke für die Laufzeit finden. Wenn diese obere Schranke korrekt ist, garantieren wir, dass -für beliebige Eingabedaten- die Laufzeit unseres Algorithmus immer kleiner oder gleich dieser Schranke ist. Beispiel: Die Anzahl der Objekte, die wir sortieren wollen Die Anzahl der Listen, die wir verarbeiten wollen u.s.w. 6
Die zu messenden Operationen Der zweite Schritt unserer Analyse ist die Bestimmung der abstrakten Operationen, die wir messen wollen. D.h., Operationen, die mehrere kleinere Operationen zusammenfassen, welche einzeln in konstanter Zeit ausgeführt werden, aber den gesamten Zeitaufwand des Algorithmus durch ihr häufiges Vorkommen wesentlich mitbestimmen. 7
Die zu messenden Operationen Beispiel: Bei Sortieralgorithmen messen wir Vergleiche Bei Algorithmen in imperativen Sprachen: Speicherzugriffe Anzahl der Multiplikationen Anzahl der Bitoperationen Anzahl der Schleifen-Durchgänge Anzahl der Funktionsaufrufe u.s.w. In Funktionalen Programmiersprachen Anzahl der Reduktionen 8
Die eigentliche Analyse Hier wird eine mathematische Analyse durchgeführt, um die Anzahl der Operationen zu bestimmen. Das Problem besteht darin, die beste obere Schranke zu finden, d.h. eine Schranke, die tatsächlich erreicht wird, wenn die ungünstigsten Eingabedaten vorkommen. (worst case). Die meisten Algorithmen besitzen einen Hauptparameter N, der die Anzahl der zu verarbeitenden Datenelemente angibt. Die obere Schranke ist eine Funktion, die das Wachstum der Laufzeit in Abhängigkeit der Eingabegröße beschreibt. Oft ist es für die Praxis sehr nützlich, den mittleren Fall zu finden, aber meistens ist diese Berechnung sehr aufwendig. 9
Summe und Multiplikation in der Schule Summe n Eingabegröße: 1 2 2 2 2 2 2 2 2 2 2 2 1 n = Zahlenbreite * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Berechnungsschritt: Addition von zwei Ziffern * * * * * * * * * * * * Komplexitätsanalyse: T(n) = Anzahl der Berechnungsschritte, um zwei Zahlen mit n Im schlimmsten Fall: Ziffern zu addieren T(n) = 2n T(n) ist eine lineare Funktion 10
Multiplikation n x * * * * * * * * * * * * * * n 2 Eingabegröße: n = Anzahl der Ziffern n 2 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * n n n n n n n Berechnungsschritt: Multiplikation von zwei Ziffern Komplexitätsanalyse: T(n) = n 2 Im schlimmsten Fall Multiplikation und Addition von Ziffern keine Nullen T(n) = n 2 + cn 2 = (1+c) n 2 = an 2 immer ein Übertrag T(n) ist eine quadratische Funktion 11
Zeit Summe und Multiplikation Ab einem bestimmten Summe T(n)=cn n 0 gilt cn < an 2 T(n)=an 2 Multiplikation n 0 n 12
O-Notation Für die Effizienzanalyse von Algorithmen wird eine spezielle mathematische Notation verwendet, die als O-Notation bezeichnet wird. Die O-Notation erlaubt es, Algorithmen auf einer höheren Abstraktionsebene miteinander zu vergleichen. Algorithmen können mit Hilfe der O-Notation unabhängig von Implementierungsdetails, wie Programmiersprache, Compiler und Hardware-Eigenschaften, verglichen werden. 13
Definition: Die Funktion T(n) = O(g(n)), wenn es positive Konstanten c und n 0 gibt, so dass T(n) c. g(n) für alle n n 0 Zeit c. g(n) T(n) n n 0 14
Bedeutung der O-Notation Wichtig ist, dass O(n 2 ) eine Menge darstellt, weshalb die Schreibweise 2n + n 2 O(n 2 ) besser ist als die Schreibweise n 2 + 2n = O(n 2 ) n 2 beschreibt die allgemeine Form der Wachstumskurve n 2 + 2n = O(n 2 ) Bedeutet nicht "=" im mathematischen Sinn deswegen darf man die Gleichung nicht drehen! O(n 2 ) = n 2 + 2n FALSCH! 15
Eigenschaften der O-Notation Die O-Notation betont die dominante Größe Beispiel: Größter Exponent 3n 3 + n 2 + 1000n + 500 = O(n 3 ) Ignoriert Proportionalitätskonstante Ignoriert Teile der Funktion mit kleinerer Ordnung Beispiel: 5n 2 + log 2 (n) = O(n 2 ) Teilaufgaben des Algorithmus mit kleinem Umfang 16
Bedeutung der O-Notation Die Definition der O-Notation besagt, dass wenn T(n) = O(g(n)), ab irgendeinem n 0 die Gleichung T(n) c. g(n) gilt. Weil T(n) und g(n) Zeitfunktionen sind, ihre Werte also immer positiv sind, gilt: T(n) c ab irgendeinem n 0 g(n) Beide Funktionen können besser verglichen werden, wenn man den Grenzwert berechnet. lim n T(n) g(n) Wenn der Grenzwert existiert, dann gilt: T(n) = O(g(n)) Wenn der Grenzwert gleich 0 ist, dann bedeutet dies, dass g(n) sogar schneller wächst als T(n). Dann wäre g(n) eine zu große Abschätzung der Laufzeit. 17
Bedeutung der O-Notation Beispiel: 100n 3 + 15n 2 + 15 = O(n 3 )? lim n 100n3 + 15n 2 + 15 = 100 n 3 dann gilt: 100n 3 + 15n 2 + 15 = O(n 3 ) Beispiel: 3n + 7 = O(n) = O(n 2 ) = O(n 3 ) Ziel unserer Analyse ist es, die kleinste obere Schranke zu finden, die bei der ungünstigsten Dateneingabe vorkommen kann. 18
Klassifikation von Algorithmen Komplexitätsklassen nach Wachstumsgeschwindigkeit konstant O(1) logarithmisch O(log 2 n) O(log e n) linear O(n) quadratisch O(n 2 ) kubisch O(n 3 ) exponentiell O(2 n ) O(10 n ) 19
O(1) Die meisten Anweisungen in einem Programm werden nur einmal oder eine konstante Anzahl von Malen wiederholt. Wenn alle Anweisungen des Programms diese Eigenschaft haben, spricht man von konstanter Laufzeit. Beste Zielzeit bei der Entwicklung von Algorithmen! 20
O-Notation Beispiel: für T(n) = n 2 + 2n gilt n 2 + 2n = O(n 2 ) denn mit c=3 und ab n 0 =1 gilt 2n + n 2 c n 2 2n + n 2 3 n 2 Die Funktion T(n) liegt in der Komplexitätsklasse O(n 2 ) oder 2n + n 2 2 n 2 + n 2 2n 2 n 2 1 n Die Größenordnung der Funktion T(n) ist O(n 2 ) 21
Die Funktion sum berechnet für ein gegebenes n>0 die Summe aller Zahlen von 1 bis n Rekursiv direkt Formel von Gauß sum :: Integer -> Integer sum 0 = 0 sum :: Integer -> Integer sum n = div (n*(n+1)) 2 sum n = n + sum (n-1) T(n) = c 1 n c 1 = Zeitkosten einer Reduktion + einer Addition T(n) = O(n) T(n) = c 1 + c 2 c 1 = Zeitkosten einer Reduktion c 2 = Arithmetische Operationen T(n) = O(1) 22
Analyse mit Hilfe der O-Notation Nehmen wir an, wir möchten das größte Element einer Liste berechnen. 1. Lösung maxi :: (Ord a) => [a] -> a maxi [x] = x maxi (x:xs) all (<=x) xs = x otherwise = maxi xs Im Schlimmsten Fall brauchen wir n Reduktionen in der all-funktion, mit n gleich die Länge der Liste xs. 23
maxi [x] = x maxi (x:xs) all (<=x) xs = x otherwise = maxi xs maxi [x 1, x 2,, x n 1, x n ] Anzahl der Reduktionen all ( x 1 ) [x 2,, x n 1, x n ] (n) all ( x 2 ) [x 3,, x n 1, x n ] (n-1) all ( x 3 ) [x 4,, x n 1, x n ] (n-2)... all ( x [x n 2 ) n 1, x n ] 3 all ( x [x n 1 ) n ] 2 maxi [x n ] x n 1 Summe aller Reduktionen: (n)*(n+1)/2 T (n) = c 1 2 n2 + 1 2 n mit c=zeitkosten einer Reduktion T (n) = O(n 2 ) 24
Beispiel: Die eigentliche Analyse Das größte Element einer Liste. 2. Lösung quick_maxi :: (Ord a) => [a] -> a quick_maxi [x] = x quick_maxi (a:b:xs) = help_maxi a (b:xs) where help_maxi a [] = a help_maxi a (b:xs) a>b = help_maxi a xs otherwise = help_maxi b xs 25
quick_maxi [x] = x quick_maxi (a:b:xs) = help_maxi a (b:xs) where help_maxi a [] = a help_maxi a (b:xs) a>b = help_maxi a xs otherwise = help_maxi b xs quick_maxi [x 1, x 2,, x n 1, x n ] help_maxi x 1 (x 2, :[x 3,, x n 1, x n ]) Anzahl der Reduktionen help_maxi x 2 (x 3, :[x 4,, x n 1, x n ]) 1 1 T (n) = c n + 1 T (n) = O(n) help_maxi x 2 (x 4, :[x 5,, x n 1, x n ]) 1...... help_maxi x i (x n, :[]) 1 help_maxi x j [x n ] 1 26
Insertion-Sort isort [] = [] isort (a:x) = ins a (isort x) where ins a [] = [a] ins a (b:y) a<= b = a:(b:y) otherwise = b: (ins a y) Eingabe: n Zahlen n 3 7 9 2 0 1 4 5 6 0 8 2 3 7 5 Berechnungsschritte: Vergleichsoperationen oder Reduktionen Komplexitätsanalyse: Im schlimmsten Fall T(n) = 1+2+3+...+(n-1) = (n-1)n 2 = 1 2 n2 + 1 2 n = O(n 2 ) 27
Komplexität eines Algorithmus O-Notation Ω Notation θ-notation Obere Komplexitätsgrenze (höchstens) Untere Komplexitätsgrenze (mindestens) Genaue Komplexität (genau) 28
Definitionen: Ω Notation Komplexität eines Algorithmus Die Funktion T(n) = Ω (g(n)), wenn es positive Konstanten c und n 0 gibt, so dass T(n) c g(n) für alle n n 0 θ-notation Die Funktion T(n) = θ (g(n)) genau dann, wenn T(n) = O(g(n)) und T(n) = Ω (g(n)) 29
Beispiel: factorial 0 = 1 factorial n = n*factorial (n-1) Berechnung: factorial 6 => 6 * factorial 5 => 6 * (5 * factorial 4) => 6 * (5 * (4 * factorial 3)) => 6 * (5 * (4 * (3 * factorial 2))) => 6 * (5 * (4 * (3 * (2 * factorial 1)))) => 6 * (5 * (4 * (3 * (2 * (1 * factorial 0))))) => 6 * (5 * (4 * (3 * (2 * (1 * 1))))) => 6 * (5 * (4 * (3 * (2 * 1)))) => 6 * (5 * (4 * (3 * 2))) => 6 * (5 * (4 * 6)) => 6 * (5 * 24) => 6 * 120 => 720 Der Ausdruck wächst bei jedem rekursiven Aufruf der Funktion. Die Multiplikationen finden dann am Ende statt. 30
Implementierung der Funktion Fakultät Rekursive Definition factorial :: Int -> Int factorial 0 = 1 factorial n = n*factorial (n-1) Rechenzeit: Speicherplatz: T(n) = O(n) M(n) = O(n) 31
Funktionale Programmierung Rekursionsarten Lineare Rekursion Rekursive Funktionen, die in jedem Zweig ihrer Definition maximal einen rekursiven Aufruf beinhalten, werden als linear rekursiv bezeichnet. Endrekursion (tail recursion) Linear rekursive Funktionen werden als endrekursive Funktionen klassifiziert, wenn der rekursive Aufruf in jedem Zweig der Definition die letzte Aktion zur Berechnung der Funktion ist. Prof. Dr. Margarita Esponda
Beispiel einer endrekursiven Definition der Fakultätsfunktion quickfactorial n = factorial_helper 1 n where factorial_helper a 0 = a factorial_helper a n = factorial_helper (a*n) (n-1) Ablauf einer Berechnung: quickfactorial 6 => factorial_helper 1 6 => factorial_helper 6 5 => factorial_helper 30 4 => factorial_helper 120 3 => factorial_helper 360 2 => factorial_helper 720 1 => factorial_helper 720 0 => 720 Keine Zwischenausdrücke müssen gespeichert werden. Endrekursive Funktionen können aus diesem Grund oft vom Übersetzer (Compiler) optimiert werden, indem diese in einfache Schleifen verwandelt werden. 33
Implementierung der Funktion Fakultät Endrekursive Definition factorial :: Int -> Int factorial n = facthelper (1, n) where facthelper (a,0) = a facthelper (a,n) = facthelper (a*n, n-1) Wenn Tail-Recursion-Optimierungen gemacht werden. Rechenzeit: Speicherplatz: T(n) = O(n) M(n) = O(1) 34
Merge-Algorithmus n 0 1 3 4 6 7 8 9 0 0 4 5 7 7 8 9 35
Merge-Algorithmus 2*n 0 0 0 1 3 4 4 5 6 7 7 7 8 8 9 9 Wir hatten ursprünglich zwei sortierte Mengen mit Länge n. Nach jedem Vergleich wird eine Zahl sortiert, d.h. im schlimmsten Fall haben wir 2*n Vergleiche. T(n) = 2n = O(n) 36
Merge-Sort-Algorithmus teile teile 3 7 0 4 9 8 1 6 5 7 0 7 0 4 9 8 3 7 0 4 9 8 1 6 5 7 0 7 0 4 9 8 teile 3 7 0 4 9 8 1 6 5 7 0 7 0 4 9 8 teile teile merge merge 3 7 0 4 9 8 1 6 5 7 0 7 0 4 9 8 3 7 0 4 9 8 1 6 5 7 0 7 0 4 9 8 3 7 0 4 8 9 1 6 5 7 0 7 0 4 8 9 0 3 4 7 1 6 8 9 0 5 7 7 0 4 8 9 merge 0 1 3 4 6 7 8 9 0 0 4 5 7 7 8 9 merge 0 0 0 1 3 4 4 5 6 7 7 7 8 8 9 9 37
Merge-Sort-Algorithmus teile 3 7 0 4 9 8 1 6 5 7 0 7 0 4 9 8 teile 3 7 0 4 9 8 1 6 5 7 0 7 0 4 9 8 n-1 teile 3 7 0 4 9 8 1 6 5 7 0 7 0 4 9 8 Teil- Operatonen teile teile merge merge 3 7 0 4 9 8 1 6 5 7 0 7 0 4 9 8 3 7 0 4 9 8 1 6 5 7 0 7 0 4 9 8 =n 3 7 0 4 8 9 1 6 5 7 0 7 0 4 8 9 0 3 4 7 1 6 8 9 0 5 7 7 0 4 8 9 =n =n lg n merge 0 1 3 4 6 7 8 9 0 0 4 5 7 7 8 9 =n merge 0 0 0 1 3 4 4 5 6 7 7 7 8 8 9 9 n Vergleiche =n n log(n) 38
Merge-Sort-Algorithmus Eine Teilung kostet c 1 Ein Vergleich kostet c 2 T (n) = c 1 (n 1) + c 2 n log(n) Teiloperationen Vergleiche T (n) = O(n log(n))...nicht im Haskell! 39
1. Lösung (Hilfsfunktion): Merge-Sort-Algorithmus merge (Ord a) => [a] -> [a] -> [a] merge [] ys = ys merge xs [] = xs merge (x:xs) (y:ys) = if x <= y then x: (merge xs (y:ys)) else y: (merge (x:xs) ys) 40
1. Lösung: Merge-Sort-Algorithmus mergesortstart :: (Ord a) => [a] -> [a] mergesortstart [ ] = mergesort 0 [ ] mergesortstart xs = mergesort (length xs) xs mergesort :: Integer -> [a] mergesort _ [] = [] mergesort _ [x] = [x] mergesort _ [x,y] = if x <= y then [x,y] else [y,x] mergesort len xs = merge (mergesort h leftlist) (mergesort (len-h) rightlist) where schlecht! h = len `div` 2 leftlist = take h xs rightlist = drop h xs 41
2. Lösung (Hilfsfunktionen) Merge-Sort-Algorithmus split :: [a] -> [[a]] split [] = [] split [x] = [[x]] split (x:xs) = [x]: (split xs) merge :: (Ord a) => [a] -> [a] -> [a] merge [] ys = ys merge xs [] = xs merge (x:xs) (y:ys) x <= y = x: (merge xs (y:ys)) otherwise = y: (merge (x:xs) ys) 42
Merge-Sort-Algorithmus 2. Lösung mergelists :: (Ord a) => [[a]] -> [[a]] mergelists [] = [] mergelists [x] = [x] mergelists (x:y:xs) = (merge x y): mergelists xs mergesort :: (Ord a) => [[a]] -> [[a]] mergesort [x] = [x] mergesort (x:y:xs) = mergesort (mergelists (x:y:xs)) startmergesort :: (Ord a) => [a] -> [a] startmergesort xs = sortedlist where [sortedlist] = mergesort (split xs) 43