Die Präsentation wird geladen. Bitte warten

Die Präsentation wird geladen. Bitte warten

Grundlagen der Informatik 1 Thema 7: Komplexität von Algorithmen

Ähnliche Präsentationen


Präsentation zum Thema: "Grundlagen der Informatik 1 Thema 7: Komplexität von Algorithmen"—  Präsentation transkript:

1 Grundlagen der Informatik 1 Thema 7: Komplexität von Algorithmen
Prof. Dr. Max Mühlhäuser Dr. Guido Rößling

2 Auswahl von Algorithmen
Zwei Algorithmen berechnen die gleiche Funktion. Beispiel: merge-sort und insertion-sort Welcher Algorithmus ist der bessere? Betrachtung der nicht-funktionalen Eigenschaften von Algorithmen: Im Folgenden betrachten wir vor allem das Kriterium der Zeit, Speicher wird ähnlich behandelt Zeitkomplexität Wie lange dauert die Ausführung? Speicherbedarf Wie viel Speicher wird zur Ausführung benötigt? Benötigte Netzwerkbandbreite

3 Wie beurteilt man die Kosten der Berechnung?
Das Messen der Zeit, die ein Programm für bestimmte Argumente benötigt, kann helfen, um sein Verhalten in einer bestimmten Situation zu verstehen Aber mit einer anderen Eingabe kann das Programm eine völlig andere Zeit beanspruchen … Zeitmessungen von Programmen für bestimmte Eingaben entsprechen dem Testen von Programmen für bestimmte Eingaben: So wie das Testen Fehler aufdecken kann, können Zeitmessungen Anomalien im Ausführungsverhalten für bestimmte Eingaben aufspüren Allerdings lässt sich davon keine generelle Aussage über das Verhalten eines Programms ableiten Diese Vorlesung gibt einen ersten Einblick in die Mittel zum Treffen allgemeiner Aussagen über die Ausführungskosten von Programmen GdI 2 widmet sich diesem Thema genauer erste Intuition: einfach die Ausfuehrungszeit messen kann ersten Eindruck/Einblick geben wir wollen das aber deutlich allgemeiner machen

4 Überblick Abstraktes Zeit- und Komplexitätsmaß
O-Notation und andere Wachstumsmaße Techniken zur Bestimmung der Komplexität Vektoren in Scheme

5 Konkretes Zeitmaß, Abstraktes Zeitmaß
Die tatsächliche Ausführzeit ist von mehreren Faktoren abhängig Prozessorgeschwindigkeit Typ des Computers Programmiersprache Qualität des Compilers, … Um sinnvolle Vergleiche von Algorithmen zu ermöglichen, benötigen wir ein Maß der Zeitkomplexität, das von derartigen Faktoren unabhängig sind. Home computer Desktop Computer Minicomputer Mainframe computer Supercomputer 51.915 11.508 2.382 0.431 0.087 Art des Computer Zeit Tatsächliche Ausführungszeit (Millisekunden) für das Berechnen von f

6 Komplexitätsmessung Idee: Beschreibung des Zeitverbrauchs als mathematische Funktion  Kostenfunktion Definitionsbereich der Kostenfunktion: Eingabegröße n Abhängig vom untersuchten Problem Für die Sortierung einer Liste: n = Anzahl der Elemente Matrizenmultiplikation: n = Anzahl der Zeilen, m = Anzahl Spalten Graphenalgorithmen: n = Anzahl der Knoten, e = Anzahl Kanten Wertebereich der Kostenfunktion: benötigte Anzahl der Rechenschritte T(n) Ansatz: Anzahl der Rekursionsschritte ist ein gutes Maß der Größe der Auswertungssequenz die naechsten folien argumentieren, dass die anzahl der rekursionsschritte tatsaeclich ein gutes mass sind…

7 Veranschaulichung des abstrakten Zeitmaßes
Untersuchen wir das Verhalten der Funktion length: Sie erhält eine Liste mit beliebigem Dateninhalt und berechnet wie viele Elemente in der Liste enthalten sind (define (length a-list) (cond [(empty? a-list) 0] [else (+ (length (rest a-list)) 1)])) length (list 'a 'b 'c)) = (+ (length (list 'b 'c)) 1) = (+ (+ (length (list 'c)) 1) 1) = (+ (+ (+ (length empty) 1) 1) 1) = (+ (+ (+ 0 1) 1) 1) = 3 Rekursions-schritte

8 Veranschaulichung des abstrakten Zeitmaßes
Nur die Anzahl der Rekursionsschritte ist relevant für die Bestimmung der Komplexität. Schritte zwischen den Rekursionen unterscheiden sich nur bzgl. der Substitution von a-list. (length (list 'a 'b 'c)) = (cond [(empty? (list 'a 'b 'c)) 0] [else (+ (length (rest (list 'a 'b 'c))) 1)]) [false 0] = (+ (length (rest (list 'a 'b 'c))) 1) = (+ (length (list 'b 'c)) 1) If we apply how-many to a shorter list, we need fewer natural recursion steps: (how-many (list 'e)) = (+ (how-many empty) 1) = 1 If we apply how-many to a longer list, we need more natural recursion steps. The number of steps between natural recursions remains the same.

9 Veranschaulichung des abstrakten Zeitmaßes
Das Beispiel zeigt zweierlei: Die Anzahl der Auswertungsschritte hängt von der Länge der Eingabeliste ab Die Anzahl der Rekursionsschritte ist ein gutes Maß für die Länge einer Auswertungssequenz Wir können die eigentliche Anzahl benötigter Schritte aus diesem Maß und der Funktionsdefinition wieder rekonstruieren. Die abstrakte Laufzeit eines Programms ist das Verhältnis zwischen der Eingabegröße und der Anzahl der Rekursionsschritte in einer Auswertung “abstrakt” heißt, die Messung ignoriert konstante Faktoren: wie viele primitive Schritte pro Rekursion benötigt werden wie viel Zeit primitive Schritte benötigen wie viel tatsächliche Zeit die gesamte Berechnung verbraucht In our first example, the size of the input is simply the size of the list. More specifically, if the list contains one item, the evaluation requires one natural recursion. For two items, we recur twice. For a list with N items, the evaluation requires N steps.

10 Veranschaulichung des abstrakten Zeitmaßes
(define (contains-doll? alos) (cond [(empty? alos) false] [(symbol=? (first alos) 'doll) true] [else (contains-doll? (rest alos))])) Die folgende Applikation von contains-doll? benötigt keinen Rekursionsschritt. Die folgende Applikation benötigt so viele Rekursionsschritte, wie es Elemente in der Liste gibt. Im besten Fall wird die Lösung unmittelbar gefunden Im schlimmsten Fall wird die gesamte Liste durchsucht (contains-doll? (list 'doll 'robot 'ball 'game-boy)) (contains-doll? (list 'robot 'ball 'game-boy 'doll))

11 Abstraktes Zeitmaß und Eingabeform
Wir können nicht davon ausgehen, dass Eingaben immer in bestmöglicher Form bereitgestellt werden Genauso wenig können wir hoffen, dass sie nie in schlechtmöglichster Form vorliegt. Stattdessen können wir analysieren, wie viel Zeit die Funktion durchschnittlich benötigt. Zum Beispiel würde contains-doll? im Durchschnitt 'doll irgendwo in der Mitte der Liste finden. Deshalb können wir sagen, dass die abstrakte Laufzeit von contains-doll? (ungefähr oder durchschnittlich) n/2 beträgt, wenn die Eingabe n Elemente beinhaltet Im Durchschnitt gibt es halb so viele Rekursionsschritte wie Elemente in der Eingabe.

12 Komplexitätsklassen Da das abstrakte Zeitmaß konstante Faktoren ignoriert, können wir die Division durch 2 ignorieren. Genauer gesagt Nehmen wir an, dass jeder Rekursionsschritt K Zeiteinheiten benötigt Wenn wir stattdessen K/2 als Konstante wählen, haben wir: Um zu zeigen, dass wir solche Konstanten vernachlässigen, sagen wir, contains-doll? benötige die „Größenordnung von n Schritten“, um 'doll in einer Liste mit n Elementen zu finden. K ist ein konstanter Faktor - wenn N nur hinreichend gross ist, wird das Produkt von N dominiert, nicht K… insbesondere fuer N vs. N*N vs. exp(N) vs. N*N*N…

13 Komplexitätsklassen F ~ in der Größenordnung von n
G ~ in der Größenordnung von n2 G F T At first glance the table seems to say that G's performance is better than F's, because for inputs of the same size (N), G's running time is always smaller than F's. But a closer look reveals that as the inputs get larger, G's advantage decreases. Indeed, for an input of size 1000, the two functions need the same number of steps, and thereafter G is always slower than F. Figure compares the graphs of the two expressions. It shows that the linear graph for 1000 · N dominates the curve of N · N for some finite number of points but thereafter it is below the curve. 1000 n n 1 10 50 500 1000 5000 F (1000 · n) 10000 50000 500000 G (n · n) 100 2500 250000

14 Analyse: insertion-sort
;; insertion-sort : list-of-numbers  ->  list-of-numbers ;; creates a sorted list of numbers from numbers in alon (define (insertion-sort alon) (cond [(empty? alon) empty] [else (insert (first alon) (insertion-sort (rest alon)))])) (sort (list 3 1 2)) = (insert 3 (insertion-sort (list 1 2))) = (insert 3 (insert 1 (insertion-sort (list 2)))) = (insert 3 (insert 1 (insert 2 (insertion-sort empty)))) = (insert 3 (insert 1 (insert 2 empty))) = (insert 3 (insert 1 (list 2))) = (insert 3 (cons 1 (list 2))) = (insert 3 (list 1 2)) = (cons 1 insert 3 (list 2)) = (cons 1 (cons 2 (insert 3 empty))) = (list 1 2 3)

15 Analyse: insertion-sort
Zwei Phasen der Auswertung: Rekursive Anwendung von insertion-sort aktiviert so viele Applikationen von insert, wie sich Elemente in der Liste befinden Jede Anwendung von insert durchläuft eine Liste von 1, 2,…,n - 1 Elementen (n ist die Anzahl der Elemente in der ursprünglichen Liste) – nach Gauß‘scher Formel ist das quadratisch. insert verhält sich ähnlich dem Suchen eines Elements: Anwendungen von insert auf einer Liste mit n Elementen benötigen im Schnitt ~ n Rekursionsschritte. Bei n Anwendungen von insert gibt es eine Größenordnung von n2 Rekursionsschritte von insert Zusammengefasst: wenn lst n Elemente hat… benötigt die Auswertung von (insertion-sort lst) n Rekursionsschritte von insertion-sort und n2 Rekursionsschritte von insert. Zusammen: n2 + n , d.h. ~ n2

16 Komplexitätsklassen insertion-sort benötigt ungefähr c1n2 Schritte, um n Elemente zu sortieren (proportional zu n2) c1 ist eine Konstante unabhängig von n Im Folgenden werden wir merge-sort analysieren Dies benötigt ungefähr c2n log2 n Schritte, um n Elemente zu sortieren c2 ist eine Konstante unabhängig von n c2 > c1 Egal, wie viel kleiner c1 als c2 ist, es wird immer einen Schnittpunkt in den Eingabedaten geben (n groß genug), ab dem merge-sort schneller ist Wir können also die Konstanten ignorieren

17 Komplexitätsklassen Stellen Sie sich vor, wir verwenden einen schnellen Computer A für insertion-sort und einen langsamen Computer B für merge-sort A ist 100 mal schneller als B betreffend der Rechenleistung A führt eine Milliarde (109) Anweisungen pro Sekunde aus B führt nur 10 Millionen Anweisungen (107) pro Sekunde Zusätzlich nehmen wir an: insertion-sort ist von dem weltbesten Programmierer in der Maschinensprache für A implementiert Der resultierende Code benötigt 2n2 (c1 = 2) Anweisungen, um n Zahlen zu sortieren merge-sort ist von einem durchschnittlichen Programmierer in einer high-level Programmiersprache mit ineffizientem Compiler implementiert Der resultierende Code benötigt 50 n log2 n (c2 = 50) Anweisungen

18 Komplexitätsklassen Wir sortieren eine Liste mit 1 Million Zahlen (n=106)… A (insert-sort) 2 x (106)2 instructions = sec 109 instructions/sec B (merge-sort) 50 x 106 x log2 106 instructions ≈ 100 sec 107 instructions/sec Da wir einen Algorithmus verwenden, dessen Laufzeit langsamerer wächst, läuft B 20 mal schneller als A (trotz des langsameren Rechners und schlechten Compilers)! Im Allgemeinen wächst der relative Vorteil von merge-sort mit der Problemgröße.

19 Zusammenfassung: abstrakte Zeit
Unsere abstrakte Beschreibung ist immer eine Aussage über das Verhältnis zweier Mengen: (mathematische) Funktion, die ein abstraktes Größenmaß der Eingabe auf ein abstraktes Maß der Laufzeit abbildet (Anzahl natürlicher Rekursionen) Die genaue Anzahl der ausgeführten Operationen ist weniger wichtig Wichtiger ist die Komplexitätsklasse, zu der der Algorithmus gehört z.B. linear, logarithmisch Wenn wir „Größenordnungs“-Eigenschaften von Algorithmen vergleichen, wie z.B. n, n2, 2n…, vergleichen wir die entsprechenden Funktionen, die n Elemente konsumieren und obige Ergebnisse produzieren.

20 Überblick Abstraktes Zeit- und Komplexitätsmaß
O-Notation und andere Wachstumsmaße Techniken zur Bestimmung der Komplexität Vektoren in Scheme

21 O-Notation Funktionen für alle natürlichen Zahlen N zu vergleichen ist schwierig: Die Domäne der natürlichen Zahlen ist unendlich Wenn eine Funktion f größere Ausgaben produziert als eine Funktion g für alle n in N, dann ist f eindeutig größer als g Aber was können wir aussagen, wenn dieser Vergleich für einige Eingaben fehlschlägt? Z.B. für 1000? Um abschätzende Aussagen zu treffen, übernehmen wir eine mathematische Notation, die zum Vergleichen von Funktionen bis zu einem Faktor und bis auf eine endliche Anzahl von Ausnahmen verwendet wird T n 1000 F G Funktion f und g: die berechnungszeit fuer 2 algorithmen Was passiert, wenn f > g fuer kleine n aber f<g fuer grosse n?

22 O-Notation Größenordnung-von (Groß-O):
Falls g eine Funktion auf den natürlichen Zahlen ist, so ist O(g) (ausgesprochen: „groß-O von g“) eine Klasse von Funktionen auf den natürlichen Zahlen. Eine Funktion f ist in O(g), wenn es die Zahlen c und n0 = großGenug gibt, so dass für alle n > n0 gilt: f(n) <= cg(n) Die „O-Notation“ geht auf den Zahlentheoretiker Edmund Landau ( ) zurück; das "O" bezeichnet man daher auch als „Landau Symbol“ Wir sagen: "f(n) wächst höchstens so schnell wie g(n)" (g ist eine obere Schranke für f)

23 O-Notation f „ist höchstens so groß wie“ g (n)
f(n)  O(g(n)), wenn zwei positive Konstanten c und n0 existieren, mit |f(n)|  c |g(n)| für alle n  n0 Funktion g definiert die Komplexitäts-klasse c g(n) 3000 Funktion f verhält sich asymptotisch wie g; ab n0 gilt immer f(n) < c g(n) 2000 f(n) 1000 Asymptotisches Maß (n  ). Es abstrahiert von unwichtigen Details für die Bestimmung der Komplexitätsklasse. 125 250 500 1000 2000 n0

24 O-Notation: Beispiele
Für f(n) = 1000 n und g(n) = n2 können wir sagen, dass f in O(g) ist, weil für alle n > 1000 gilt, f(n) <= c.g(n) (n0 = 1000 und c = 1) Die Groß-O Notation bietet eine Kurzform an, um Aussagen über die Laufzeit von Funktionen zu treffen: Die Laufzeit von length ist O(n). Im schlechtesten Fall ist die Laufzeit von insertion-sort O(n2) Dabei sind n und n2 Standardabkürzungen für die (mathematischen) Funktionen f(n) = n und g(n) =n2

25 Komplexitätsklassen O(n log n) O(n) Anzahl der Vergleiche Eingabegröße (n) O(n3) O(n2) O(log n) Polynomielles Wachstum (O(nx)) verkleinert die Größe sinnvoller Eingaben stark Exponentielles Wachstum (O(an)) noch stärker

26 Eigenschaften der O-Notation
Vergleiche der „O“ Komplexität sind nur sinnvoll für große Eingaben Bei kleinen Eingaben kann ein ineffizienter Algorithmus manchmal schneller sein als ein effizienter Beispiel: eine Funktion aus 2n2 wächst schneller als eine in (184 log2n), sie ist aber besser für kleinere Eingaben (n < 20) Insbesondere bei Algorithmen mit linearem oder schwächerem Wachstum können sich derartige Faktoren bemerkbar machen Der Vergleich der Komplexitätsklasse ist in diesen Fällen u.U. nicht ausreichend.

27 Eigenschaften der O-Notation
Die O-Notation blendet proportionale Faktoren, kleine Eingaben und kleinere Terme aus 10 125 250 500 1000 2000 0.121 2.8 11.0 43.4 172.9 690.5 f(n) an2 0.017 2.7 10.8 43.1 172.4 689.6 n2 - Ausdruck in % vom ganzen f(n) = an2 + bn +c mit a = , b = und c = 0.1 14.2 94.7 98.2 99.3 99.7 99.9 n Beispiele: 2n3+n2-20  O(n3) log10 n  O(log2 n) n  O(n) n  O(n2) O(1)  O(log n)  O(n)  O(n2)  O(n3)  O(2n)  O(10n)

28 O-Notation: andere Symbole
Es gibt noch mehr Symbole für verschiedene Zwecke: Asymptotische untere Schranke [f(n)  (g(n))]: f(n)  (g(n)), wenn positive Konstanten c und n0 N existieren, so dass 0  cg(n)  f(n),  n  n0 Wir sagen: „f(n) wächst mindestens so schnell wie g(n)“ Asymptotisch exakte Schranke [f(n)  (g(n))]: f(n) (g(n)), wenn die positiven Kostanten c1, c2, und n0 N existieren, so dass c1 g(n)  f(n)  c2 g(n), n  n0 f(n) (g(n)) genau dann, wenn f(n)  O(g(n)) und f(n)  (g(n)) Wir sagen „f(n) wächst genauso schnell wie g(n)“

29 O-Notation: andere Symbole
Schema für O,  und : n0 n0 n0 obere Schranke untere Schranke exakte Schranke

30 Andere asymptotische Notationen
Eine Funktion f(n) ist in o(g(n)), wenn es positive Konstanten c und n0 gibt, so dass f(n) < c g(n)  n  n0 Eine Funktion f(n) ist in (g(n)), wenn es positive Konstanten c und n0 gibt, so dass c g(n) < f(n)  n  n0 Intuitiv: () ist ähnlich > () ist ähnlich  () ist ähnlich = o() ist ähnlich < O() ist ähnlich 

31 Übersicht Abstraktes Zeit- und Komplexitätsmaß
O-Notation und andere Wachstumsmaße Techniken zur Bestimmung der Komplexität Vektoren in Scheme

32 Beispiel: Größter gemeinsamer Teiler
(define (gcd a b)   (cond  [(= b 0) a]     [else (gcd b (remainder a b))])) Komplexitätsanalyse des Euklidischen Algorithmus ggT (gcd) ist nicht trivial Lamés Theorem: Sei k die Anzahl der Schritte, die der Euklidische Algorithmus braucht, um den ggT zweier Zahlen m und n zu berechnen, wobei n die kleinere Zahl ist Dann muss die kleinere Zahl der beiden größer oder gleich der k-ten Fibonacci Zahl sein  n Fib(k) < k Die Größe des Wachstums ist O(log(n)) message: komplexitaet ist nicht immer direkt und einfach zu berechnen d.h. eine technik ist das problem auf ein anderes zurueckzufuehren

33 Beispiel: Exponentieren
Eingabe: Basis b und positiver ganzzahliger Exponent n Ausgabe: bn Idee: bn = b* b(n-1) , b0 = 1 Angenommen die Multiplikation benötigt eine konstante Zeit c Dann gilt T(n) = cn = O(n) (define (expt b n)   (cond [(= n 0) 1]     [else (* b (expt b (- n 1)))]))

34 Beispiel: Exponentieren
Idee: Weniger Schritte durch sukzessives Quadrieren Statt b8 als b*b*b*b*b*b*b*b zu berechnen, können wir es auch so machen: b2 = b*b, b4 = (b2)2, b8 = (b4)2 Generell gilt die Regel bn = (bn/2) 2 wenn n gerade ist bn = b*bn wenn n ungerade ist Zu welcher Komplexitätsklasse gehört dieser Algorithmus? (define (fast-expt b n)   (cond [(= n 0) 1]         [(even? n) (sqr (fast-expt b (/ n 2))))         (else (* b (fast-expt b (- n 1)))))) O(log(n))

35 Analyse von Teile-und-Herrsche Algorithmen
Für rekursive Algorithmen kann die Laufzeit oft als Rekurrenzgleichung (Rekurrenz) beschrieben werden Gesamtlaufzeit wird mittels der Laufzeit für kleinere Eingaben definiert Beispiel: Problem (n) wird in 2 Teilprobleme (n/2) zerlegt Aufwand cn für Zerlegung und Kombination der Teillösungen Eine Rekurrenz T(n) für die Laufzeit eines Teile-und-Herrsche Algorithmus der Größe n basiert auf den drei Schritten des Paradigma… 3 schritte des paradigmas: teile, hersche, kombiniere

36 Analyse von Teile-und-herrsche Algorithmen
Fall 1: Die Problemgröße ist klein genug, sagen wir n <= c für eine Konstante c, so dass es trivial gelöst werden kann  Lösung benötigt konstante Zeit  (1) Fall 2: Das Problem ist nicht-trivial: Die Teilung des Problems ergibt a Teilprobleme, die alle 1/b der Größe des Originals haben Beispiel: für merge-sort haben wir a = b = 2 D(n): Aufwand für die Zerlegung in Teilprobleme C(n): Aufwand für das Kombinieren der Teillösungen

37 Die Türme von Hanoi ! Das „Türme von Hanoi“ Puzzle wurde 1883 vom französischen Mathematiker Édouard Lucas erfunden. Wir bekommen einen Turm von Scheiben, in größer werdender Reihenfolge auf einen der drei Stäbe gesteckt. Das Ziel ist, den ganzen Turm auf einen der anderen Stäbe zu bringen, wobei jeweils nur eine Scheibe bewegt und niemals eine größere auf eine kleinere Scheibe gelegt werden darf.

38 Die Türme von Hanoi: Algorithmus
1 B C A 3 2 n Um n Scheiben von Stange A zu Stange B zu bewegen: Bewege n−1 Scheiben von A nach C. Damit bleibt Scheibe #n auf A Bewege Scheibe #n von A nach B Bewege n−1 Scheiben von C nach B, so dass sie auf Scheibe #n sitzen Rekursiver Algorithmus: Um Schritte 1 und 3 auszuführen, wende den gleichen Algorithmus wieder für n-1 an. Die gesamte Prozedur ist eine endliche Anzahl von Schritten, denn irgendwann wird der Algorithmus für n = 1 angewendet werden. Dieser Schritt, eine einzelne Scheibe zu verschieben, ist trivial.

39 Die Türme von Hanoi Ausgegeben wird die Liste der Scheibenbewegungen
(move 'A 'B 'C 4) (define (move T1 T2 T3 n) (cond [(= n 0) empty] [else (append (move T1 T3 T2 (- n 1)) (list (list T1 T2)) (move T3 T2 T1 (- n 1))) ] ) (list (list 'A 'C) (list 'A 'B) (list 'C 'B) (list 'B 'A) (list 'B 'C) (list 'C 'A) (list 'C 'B)) Ausgegeben wird die Liste der Scheibenbewegungen A B C Wie viele Scheibenbewegungen sind erforderlich, um einen Stapel der Höhe n zu bewegen?

40 Rekursive Komplexitätsfunktion für die Türme von Hanoi
Wie viele Umlagerungen von Scheiben sind notwendig …? für n < 2 ist die Antwort einfach: T(0)=0, T(1)=1 für n > 1 ist die Antwort rekursiv definiert: T(n) = T(n-1)+1+T(n-1) = 2×T(n-1)+1 = 2×(2×T(n-2)+1)+1 = 2×(2×(2×T(n-3)+1)+1)+1 = 2i×T(n-i) , für i=n, n-i wird 0 å = 2k i-1 k = 2n×T(0) + 2n-1 = 2n-1 => exponentielle Komplexität!

41 Wenn das Universum sein Ende finden wird…
Es gibt eine Legende über ein buddhistisches Kloster bei Hanoi, in dem sich ein riesiger Raum mit drei abgenutzten Pfosten befindet, die von 64 goldenen Scheiben umgeben waren. Seit der Gründung des Klosters vor über tausend Jahren führen Mönche die Anordnung einer alten Prophezeiung aus: Sie bewegen die Scheiben in Übereinstimmung mit den Regeln des Puzzles Jeden Tag bewegen die Mönche eine Scheibe Man sagt, sie glauben, wenn die letzte Bewegung ausgeführt würde, wird die Welt mit einem Donnerschlag untergehen.

42 Wenn das Universum sein Ende finden wird…
Zum Glück sind sie nicht mal annäherungsweise fertig  Angenommen, die Legende wäre wahr und die Mönche könnten eine Scheibe pro Sekunde bewegen, mit der kleinsten Anzahl an nötigen Bewegungen: Dann bräuchten sie zur Lösung 264−1 Sekunden - das sind etwa 585 Milliarden Jahre Unser Universum ist zur Zeit ungefähr 13,7 Milliarden Jahre alt

43 Analyse von Merge Sort Vereinfachung: Größe des ursprünglichen Problems ist eine Potenz von 2 Jeder Teilung liefert zwei Teilsequenzen von Länge n/2 Es gibt Beweise, dass eine solche Annahme die Komplexitätsklasse der Lösung zur Rekurrenz nicht beeinflusst Worst case: n > 1 Teile: Das Extrahieren der Elemente der beiden Teillisten benötigt jeweils Zeit der Ordnung n  D(n) = 2n ~ (n) Herrsche: Das rekursive Lösen der 2 Teilprobleme dauert 2T(n/2) Kombiniere: Zusammenfügen der beiden Listen benötigt auch (n) worst-case Laufzeit für merge sort

44 Lösen von Rekurrenzen Frage: Wie löst man Rekurrenzen wie diese?
Antwort: Mathematische Methoden die uns dabei helfen: Substitutionsmethode Rekursionsbaum-Methode Master-Methode basierend auf dem Master-Theorem Eine „Kochbuch“-Methode, um Rekurrenzen der Form T(n) = aT(n/b)+f(n) zu lösen, a 1, b>1, f(n) asymptotisch positive Funktion Kann benutzt werden, um zu zeigen, dass T(n) von merge sort in (n log n) ist Dabei vernachlässigen wir technische Details: Wir ignorieren Rundungen nach unten und oben (absorbiert durch O- oder Q-Notation) Wir nehmen ganzzahlige Argumente für Funktionen an Wir vernachlässigen Grenzbedingungen

45 Das Master-Theorem Betrachte falls n < c
wobei a >= 1 und b >= 1 falls n < c falls n > 1 Wenn für eine Konstante e > 0, dann Wenn , dann Wenn für eine Konstante e > 0 und wenn a f(n/b) <= c f(n) für eine Konstante c < 1 und alle hinreichend großen n, dann T(n) =Q(f(n)) zum verstaendnis: z.B. a = b = 2 - damit (log_b a) = 1 merge-sort ist der 2. Fall (a=b=2) und O(n) fuer f(n) fall 1: aT(n/b) dominiert die Berechnungskomplezitaet fall 3: f(n) dominiert die Berechnungskomplezitaet fall 2: f(n) und aT(n/b) sind beide wichtig…

46 Die Rekursionsbaum-Methode
In einem Rekursionsbaum repräsentiert jeder Knoten die Kosten eines Teilproblems in der Kette der rekursiven Funktionsapplikationen Summiere Kosten auf jeder Ebene, um Kosten pro Ebene zu bekommen Summiere alle Kosten pro Ebenen, um Gesamtkosten zu ermitteln Besonders nützlich für Rekurrenzen, welche die Laufzeit von Teile-und-herrsche Algorithmen beschreiben Oftmals verwendet, um eine gute Annäherung zu finden, die dann mit anderen Methoden verifiziert wird Wenn sorgfältig entworfen, kann sie auch als direkter Beweis einer Lösung für eine Rekurrenz dienen For example used for proving the theorem that forms the basis for the master method

47 Rekursionsbaum für Merge Sort
Schreiben wir die Rekurrenz für merge-sort wie folgt: T(n/2) cn Baum für die erste Applikation T(n/4) cn cn/2 Baum für zwei Applikationsschritte

48 Rekursionsbaum für Merge Sort
Gesamt: cn (log n + 1) cn cn cn cn/2 cn/2 cn cn/4 cn/4 cn/4 cn/4 log n + 1 Ebene (Induktion) ... ... ... ... ... ... ... ... ... 2ic(n/2i) Ebene i  2i Knoten ... ... ... ... ... ... ... ... ... cn c c c c c c c c n

49 Die Substitutionsmethode
Auch bekannt als die “making a good guess method” („eine gute Annäherung finden“ - Methode) Funktionsweise: Rate die Form der Antwort, Benutze Induktion, um die Konstanten zu finden und zeige, dass die Lösung funktioniert Beispiele: T(n) = 2T(n/2) + (n)  T(n) = (n log n) T(n) = 2T(n/2) + n  ??? T(n) = 2T(n/2 )+ 17) + n  ??? T(n) = 2T(n/2) + n  T(n) = (n log n) T(n) = 2T(n/2+ 17) + n  (n log n)

50 Die Substitutionsmethode
Rekurrenz: Geschätzte Lösung: Zu zeigen ist, dass für eine geeignete Konstante c > 0 Anfangsannahme: die Schranke gilt für also: Lösung: die Abschätzung ist für n>n0 zu zeigen

51 Performanz von Quicksort: bester Fall
Die Laufzeit von quicksort hängt von der Qualität der Partitionierung ab, d.h., von den Pivot-Elementen Master Theorem Bester Fall: die Partitionierung erzeugt in jedem Schritt zwei Teilprobleme der Größe n/2

52 Performanz von Quicksort: schlechtester Fall
Schlechtester Fall: die Partitionierung erzeugt in jedem Rekursionsschritt ein Teilproblem mit n-1 Elementen und eines mit 0 Elementen Der schlechteste Fall tritt ein, wenn die Liste bereits sortiert ist! Sei D(n) = (n); Für die leere Liste  T(0) = (1) n n - 1 n - 2 n - 3 2 1 3 (n2) T(n)=T(n-1)+T(0)+(n) = T(n-1)+(n) Die aufsummierten Kosten ergeben eine arithmetische Reihe, die (n2)ergibt

53 Performanz von Quicksort
Im Durchschnitt ist quicksort deutlich näher am besten als am schlechtesten Fall. Um n Elemente zu sortieren, nimmt es Θ(n log n) Vergleiche vor. Um das nachzuvollziehen, muss man verstehen, wie die Balance ("Ausgewogenheit") der Partitionierung sich in der Rekurrenz für quicksort wiederfindet Ausgewogene Partitionierung: die Partitionierung erzeugt immer eine konstante Aufteilung

54 Performanz von Quicksort: ausgewogene Partitionierung
9-zu-1-Aufteilung  erscheint ziemlich unausgewogen T(n) = T(9n/10) + T(n/10) + n Sogar eine 99:1-Aufteilung ergibt O(n log n) (logn) Der Grund: Aufteilungen mit konstanten Proportionen ergeben Rekursions- bäume der Tiefe (log n) mit Kosten von O(n) auf jeder Ebene

55 Performanz von Quicksort
Durchschnittlicher Fall Alle Permutationen der Eingabezahlen sind gleich wahrscheinlich Bei einer zufälligen Eingabeliste wird eine Mischung von ausgewogenen und unausgewogenen Aufteilungen vorliegen Gute und schlechte Aufteilungen sind zufällig über den Baum verteilt Gesamtkosten: 2n-1 = (n) Gesamtkosten: n = (n) n n - 1 1 n (n – 1)/2 (n – 1)/2 + 1 (n – 1)/2 abwechselnd gute und schlechte Aufteilung nahezu ausgewogene Aufteilung Die Laufzeit von quicksort bei abwechselnd guten und schlechten Aufteilungen ist O(n log n)

56 Performanz von Quicksort
Typischerweise ist quicksort im praktischen Einsatz schneller als andere Θ(n log n)-Algorithmen Seine innere Schleife kann auf den meisten Architekturen effizient implementiert werden Die meisten in der Praxis auftretenden Daten erlauben Entwürfe, welche die Wahrscheinlichkeit des Auftretens quadratischer Komplexität minimieren

57 Zusammenfassung der Berechnungskosten
Algorithmen können nach ihrer Komplexität eingeteilt werden  O-Notation Nur relevant für große Eingaben Maße sind maschinenunabhängig Zählen der Operationen im Verhältnis zur Größe der Eingabe Analyse für den schlechtesten, durchschnittlichen, besten Fall Algorithmen variieren sehr stark in ihrer Effizienz Gute Programmierung  ein oder zwei Komplexitätsklassen weniger Manche Probleme sind in sich komplex

58 Überblick Abstraktes Zeit- und Komplexitätsmaß
O-Notation und andere Wachstumsmaße Techniken zur Bestimmung der Komplexität Vektoren in Scheme

59 Die Kosten der Suche in Listen
Rückbesinnung auf das Beispiel der Pfadsuche in Graphen: (define (find-route origination destination G) (cond [(symbol=? origination destination) (list destination)] [else (local ((define possible-route (find-route/list (neighbors origination G) destination G))) [(boolean? possible-route) false] [else (cons origination possible-route)]))]))

60 Die Kosten der Suche in Listen
;; neighbors : node graph -> (listof node) ;; to lookup the node in graph (define (neighbors node graph) (cond [(empty? graph) (error “no neighbors")] [(symbol=? (first (first graph)) node) (second (first graph))] [else (neighbors node (rest graph))])) neighbors ist der Funktion contains-doll? ähnlich, d.h. neighbors ist in O(n)

61 Die Kosten der Suche in Listen
Die Komplexität von neighbors ist O(n) Der Algorithmus benötigt in neighbors O(n2) Schritte neighbors wird in jedem Schritt von find-route benutzt, also n mal im Fall eines maximalen Pfades neighbors kann der Flaschenhals von find-route sein! Es wird eine Datenstruktur benötigt, die den Zugriff auf die Nachbarn eines Knotens durch dessen Namen in konstanter Zeit erlaubt Vektoren sind Datenstrukturen in Scheme, die Elementzugriffe in konstanter Zeit ermöglichen. Für den Graphenalgorithmus wird eine Struktur benötigt, die den Zugriff auf die Nachbarn eines Knotens durch dessen Namen in konstanter Zeit erlaubt.

62 Operationen auf Vektoren
vector erzeugt einen Vektor aus gegebenen Werten: build-vector ist das Vektor-Analog zu build-list: (vector V V-n) (build-vector n f) = (vector (f 0) ... (f (- n 1))) vector-ref extrahiert einen Wert aus einem Vektor vector-length liefert die Anzahl der Elemente: vector? ist das Vektor-Prädikat: (vector-ref (vector V V-n) i) = V-i, 0 ≤ i ≤ n (vector-length (vector V V-n)) = (+ n 1) Ein Vektor ist eine wohldefinierte Datenklasse mit spezifischen grundlegenden Operationen build-vector: f ist eine funktion mit eibnem Parameter! vektoren sind sehr aehnlich zu arrays in anderen programmiersprachen (vector? (vector V V-n)) = true (vector? U) = false

63 Graphen als Vektoren Zur Darstellung der Graphenknoten können Zahlen
verwendet werden: A B C D E F G 1 2 3 4 5 6 Listenbasierte Darstellung: Vektorbasierte Darstellung: (define Graph-as-list '((A (B E)) (B (E F)) (C (D)) (D ()) (E (C F)) (F (D G)) (G ()))) (define Graph-as-vector (vector (list 1 4) (list 4 5) (list 3) empty (list 2 5) (list 3 6) empty)) Das i-te Feld des Vektors enthält die Liste der Nachbarn des i-ten Knotens

64 (vectorof (listof node))
Graphen als Vektoren Ein Knoten ist eine natürliche Zahl zwischen 0 und n – 1, wobei n die Anzahl der Knoten ist Ein Graph ist ein Vektor von Knoten: Jetzt ist neighbors für einen Knoten in konstanter Zeit ausführbar => Diese Operation kann bei der Betrachtung des abstrakten Zeitmaßes von find-route ignoriert werden. A B C D E F G 1 2 3 4 5 6 (vectorof (listof node)) ;; neighbors : node graph -> (listof node) ;; to lookup the node in graph (define (neighbors node graph) (vector-ref graph node))

65 Graphen als Vektoren Die neighbors eines Knoten sind nun eine Operation mit konstanter Laufzeit => Wir können sie bei der Betrachtung der abstrakten Laufzeit von find-route ignorieren. ;; neighbors : node graph -> (listof node) ;; to lookup the node in graph (define (neighbors node graph) (vector-ref graph node))

66 Verarbeitung von Vektoren
Mit Vektoren programmieren heißt, mit vector-ref zu programmieren – man betrachtet Vektoren und Indizes in Vektoren. Beispiel: Die Funktion vector-sum-for-3 übernimmt aus drei Zahlen bestehende Vektoren und liefert deren Summe zurück: ;; (vector number number number) -> number (define (vector-sum-for-3 v) (+ (vector-ref v 0) (vector-ref v 1) (vector-ref v 2)))

67 Verarbeitung von Vektoren
Die allgemeinere Funktion vector-sum verarbeitet Vektoren beliebiger Größe: ;; vector-sum : (vectorof number) -> number ;; to sum up the numbers in v (define (vector-sum v) ...) Ein paar Beispiele: (= 0 (vector-sum (vector -1 3/4 1/4))) (= 1 (vector-sum (vector )) (= 0 (vector-sum (vector)))

68 Verarbeitung von Vektoren
vector-sum erhält nicht die Anzahl der zu verarbeitenden Elemente. Eine Hilfsfunktion mit einem solchen Argument ist zu definieren: Dann ist vector-sum wie folgt definiert: ;; (vectorof number) N -> number ;; to sum up the numbers in v with index in ;;[i, (vector-length v)) (define (vector-sum-aux v i) ...) (define (vector-sum v) (vector-sum-aux v (vector-length v)))

69 Verarbeitung von Vektoren
Zunächst entwerfen wir eine Schablone für diese Funktion. Die Implementierung von vector-sum-for-3 legt nahe, dass i die Veränderliche in der Schablone ist. Die Schablone legt nahe, dass i die Anzahl der Elemente von v bezeichnet, die vector-sum-aux berücksichtigen muss. ;; (vectorof number) n -> number ;; to sum up the numbers in v with index in [0, i) (define (vector-sum-aux v i) (cond [(zero? i) ...] [else ... (vector-sum-aux v (pred i)) ...]))

70 Verarbeitung von Vektoren
Falls i gleich 0 ist, so müssen keine weiteren Elemente berücksichtigt werden => Das Ergebnis ist 0. Andernfalls Berechne die Summe der Zahlen in v mit Index kleiner als i-1: Nimm den Wert des Vektorfelds mit dem Index i-1:  Das Ergebnis ist ihre Summe: (vector-sum-aux v (pred i)) (vector-ref v (pred i)) Um die Schablone in eine wirkliche Funktionsdefinition zu überführen, betrachten wir jeden Fall des cond-Ausdrucks: (+ (vector-ref v (pred i)) (vector-sum-aux v (pred i))

71 Verarbeitung von Vektoren
;; (vectorof number) -> number ;; to compute the sum of the numbers in v (define (vector-sum v) (vector-sum-aux v (vector-length v))) ;; (vectorof number) n -> number ;; to sum the numbers in v with index in [0, i) (define (vector-sum-aux v i) (cond [(zero? i) 0] [else (+ (vector-ref v (pred i)) (vector-sum-aux v (pred i)))])) vector-sum-aux extrahiert die Zahlen von rechts nach links aus dem Vektor, wobei i in Richtung 0 schrumpft

72 Verarbeitung von Vektoren
Die Summe kann auch von links nach rechts berechnet werden: ;; lr-vector-sum : (vectorof number) -> number ;; to sum up the numbers in v (define (lr-vector-sum v) (vector-sum-aux v 0)) ;; vector-sum : (vectorof number) -> number ;; to sum up the numbers in v with index in ;; [i, (vector-length v)) (define (vector-sum-aux v i) (cond [(= i (vector-length v)) 0] [else (+ (vector-ref v i) (vector-sum-aux v (succ i)))]))

73 Vektoren erstellen In den nächsten Folien werden wir kurz betrachten, wie man Vektoren erstellt…. Zur Illustration entwickeln wir eine Funktion, welche die Elemente eines angegebenen Vektors mit einer bestimmten Geschwindigkeit verschiebt

74 Vektoren erstellen Die Geschwindigkeit eines Objekts kann durch einen Vektor repräsentiert werden: (vector 1 2) – die Geschwindigkeit eines Objekts in der Ebene, das sich je Zeiteinheit um 1 Einheit nach rechts und 2 nach unten bewegt. (vector ) – Geschwindigkeit im Raum; -1 Einheiten in x-Richtung, 2 Einheiten in y-Richtung, und 1 Einheit in z-Richtung. Entwickeln wir nun eine Funktion zur Berechnung der Verschiebung (displacement) eines Objekts mit der Geschwindigkeit v in t Zeiteinheiten: ;; (vectorof number) number -> (vectorof number) ;; to compute the displacement of v and t (define (displacement v t) ...)

75 Vektoren erstellen Ein paar Beispiele
(equal? (displacement (vector 1 2) 3) (vector 3 6)) (equal? (displacement (vector ) 6) (vector )) (equal? (displacement (vector -1 -2) 2) (vector -2 -4)) Um das Ergebnis zu berechnen, multiplizieren wir jede Komponente des Geschwindigkeitsvektors mit der Zeit t

76 Vektoren erstellen Wir konstruieren einen Vektor mit der gleichen Länge wie v: (build-vector (vector-length v) ...) Wir müssen ... mit einer Funktion ersetzen, die die 0te, 1te, … Komponente des neuen Vektors berechnet: ;; new-item : n -> number (define (new-item index) ...) Dann multiplizieren wir (vector-ref v i) mit t – fertig! ;; (vectorof number) number -> (vectorof number) ;; to compute the displacement of v and t (define (displacement v t) (local ((define (new-item i) (* (vector-ref v i) t))) (build-vector (vector-length v) new-item)))

77 Vektoren erstellen Da die lokale Funktion new-item nicht rekursiv ist, können wir sie mit einem lambda-Ausdruck ersetzen ;; (vectorof number) number -> (vectorof number) ;; to compute the displacement of v and t (define (displacement v t) (build-vector (vector-length v) (lambda (i) (* (vector-ref v i) t)))) In der Mathematik nennen wir dies ein Skalarprodukt Um das Ergebnis zu berechnen, multiplizieren wir einfach jede Dimension des Geschwindigkeitsvektors mit der Zeit (repräsentiert durch eine Zahl) und liefern einen neuen Vektor zurück. Diese und viele andere mathematische Operationen mit Vektoren sind direkt in Scheme ausdrückbar.


Herunterladen ppt "Grundlagen der Informatik 1 Thema 7: Komplexität von Algorithmen"

Ähnliche Präsentationen


Google-Anzeigen