Algorithmen und Programmierung Daniel Waeber Semester 3
Inhaltsverzeichnis 1 Analyse von Algorithmen 5 1.1 Laufzeit von Algorithmen............................. 5 1.1.1 Experimentelle Analyse.......................... 5 1.1.2 Theoretische Analyse von Algorithmen.................. 5 1.1.3 Pseudo-Code................................ 5 1.1.4 Random Accesss Machsine........................ 6 1.1.5 Laufzeit eines Algorithmus........................ 7 1.1.6 Wachstum der Laufzeit.......................... 7 1.1.7 O-Notation................................. 7 1.1.8 O-Notation HOWTO........................... 8 1.1.9 MEGA-Beispiel.......................... 8 1.1.10 Rekursionsbäume.............................. 9 1.1.11 Rekursion in der Registermaschine.................... 13 1.2 Datenstrukturen.................................. 14 1.2.1 Datenstruktur Stack............................ 14 1.2.2 Indirekte Anwendung von Stapeln................... 15 1.2.3 Dynamisierung Array-basierter Datenstrukturen............ 17 3
Kapitel 1 Analyse von Algorithmen Definition: Ein Algorithmus ist eine Schritt-für-Schritt Anweisung, mit dem ein Problem in endlich vielen Schritten lösbar ist. Frage nach Korrektheit und Effizienz des Algorithmus. 1.1 Laufzeit von Algorithmen im Allgemeinen transformieren Algorithmen eine Eingabe in eine Ausgabe die Laufzeit eines Algorithmus hängt typischerweise von der Eingabe-Grö se ab die Bestimmung der mittleren Laufzeit eines Algorithmus ist mathematisch oft sehr anspruchsvoll (selbst experimentell schwierig) Analyse der Laufzeit im schlechtesten Fall ( worst case ) 1.1.1 Experimentelle Analyse Implementation des Algorithmus. Messung der Laufzeit des Programms auf Eingaben verschiedener Grö se und Struktur. 1.1.2 Theoretische Analyse von Algorithmen Statt einer konkreten Implementierung verwenden wir eine abstraktere Beschreibung. (Pseudo Code) Charakterisierung der Laufzeit als Funktion der Länge der Eingabe Dabei werden alle Eingaben der Länge n berücksichtigt 1.1.3 Pseudo-Code abstrakte Beschreibung eines Algorithmus strukturierter als prosaische Beschreibung weniger detailierter als Java Programm 5
Beispiel: Algorithm arraymax(a,n) Input: Feld A von n ganzen Zahlen Output: Ein groesstes Element in A currentmax <- A[0] for i=1 to n-1 do if A[i] > currentmax then currentmax <- A[i] return currentmax Psedo-Code im Detail: Kontrollflussanweisungen for...do while...do repeat...until if...then...else... Deklarationen Methodenaufrufe f(arg1, arg2,...) Rückgabe return Ausdrücke Zuweisung Gleichheitstest =... 1.1.4 Random Accesss Machsine Rechenkern arithmetische Ausdrücke +,,,/ Kontrollflussoperatoren bedingte/unbedingte Sprünge konstante Zeit pro Operation linear organisierter Speicher mit wahlfreiem Zugriff jede Zelle speichert beliebige Zahl auf Zellen kann über ihre Adresse in konstanter Zeit zugegriffen werden unendlich gro ser Speicher primitive Operationen des Pseudocodes können in konstant vielen RAM-Anweisungen realisiert werden.
1.1.5 Laufzeit eines Algorithmus Definition: A Algorithmus (implementiert auf einer RAM) I Eingabe fuer A T A (I) = Anzahl der elementaren RAM Op. die A auf I durchfuehrt, Laufzeit von A auf I T A (n) = max(t A (I)) (I Eingabe der Groesse n) worst case Laufzeit von A Beispiel: Algorithm double(x) Eingabe: x N Ausgabe: 2 x y 2 x return y LOAD 0,0 MULT 0,2 STORE 0,0 RET Algorithm double(x) y x y y + x return z Algorithm double(x) z x for i = 1 to x do z z + 1 return z Da jede Zeile in 5 RAM-Operationen übersetzt werden kann, kann die Laufzeit auch in Pseudocode berechnet werden 1.1.6 Wachstum der Laufzeit Eine vernünftige Änderung des Maschinenmodells verändert die Laufzeit von Algorithmen nur um einen konstanten Faktor. Asymptotische Laufzeit berücksichtigt keine konstanten Faktoren und keine Terme niederer Ordnung. Wir interessieren uns daher für das asymptotische Wachstum von T A (n). 1.1.7 O-Notation Definition: f(n) O(g(n)) def n 0 c 0 : f(n) c g(n) n n 0
Beispiel: 12n 4 = O(n) 12n 4 12 n n 2 = O(n) nein Definition: f = Ω(g) gdw. g = Ω(f) 1.1.8 O-Notation HOWTO 1. Falls f(n) = d i=0 a in i (a d 0) f(n) = O(n d ) 2. Wir sagen 2n = O(n) statt 2n = O(n 2 ) 3. Wir sagen O(n) statt O(3n 6) Beispiel: Gegeben eine Folge von n Zahlen X[0],, X[n 1]. Berechne die Folge A[0],,A[n 1], wobei A[i] = 1 i+1 (X[0] + + X[i]) Algorithm PrefixAverage(X,n) Eingabe: X[0],...,X[n-1] Ausgabe: A[0],...,A[n-1], mit A[i] = (X[0],...,X[i) A <- leeres Feld mit n Zellen n for i=0 to n-1 n sum <- 0 n for j=0 to i n*(n+1)/2 sum <- sum + X[j] n*(n+1)/2 A[i] = sum / (i+1) n return A 1 T pa (n) = O(n + n + n + n(n+1) 2 + n(n+1) 2 + n + 1) = O(n 2 ) Algorithm Pr3f1x4v3r4g3(X,n) Eingabe: X[0],...,X[n-1] Ausgabe: A[0],...,A[n-1], mit A[i] = (X[0],...,X[i) A <- Feld mit n Zellen 1 sum <- A[0] 1 for 1 to n-1 n-1 sum <- sum + X[i] n-1 A[i] = sum / (i+1) n-1 return A 1 T p4 (n) = O(1 + 1 + (n 1) + (n 1) + (n 1) + 1) = O(n) 1.1.9 MEGA-Beispiel Wie löse ich ein Problem, wenn ich wei s, wie ich ein einfacheres Problem löse?
Beispiel: n! = n i; n! = i=0 { n (n 1)! n > 0 1 n = 0 Algorithmus fact(n) Eingabe: n Ausgabe: n! if(n=0) then 1 return 1 1 else return n*fact(n-1) 1+T(n-1) Sei T fact die Laufzeit von fact. T fact (0) c T fact (n) c + T fac (n 1) T fact (n) c + T fact (n 1) (1.1) c + (c + T fact (n 2) (1.2) 3c + T fact (n 3) (1.3) kc + T fact (n k) (1.4) (Beweis mit vollständiger Induktion über k) T fact (n) cn + T fact (0) cn + c = O(n) 1.1.10 Rekursionsbäume Beispiel: Algorithm Summe(A,n) Eingabe: Feld A von n Elementen Ausgabe: Summe \"uber A if n=1 return A[0] else return A[n]+Summe(A,n-1)
Aufruf durch C.K. ;) Summe(A,5) Summe(A,4) Summe(A,3) Summe(A,2) Summe(A,1) Beispiel: Potenzieren p(x,n) = x n wobei x,n N { 1 n = 0 p(x,n) = x p(x,n 1) n 1 Es geht aber besser, da x 2n = (x n ) 2 bzw. x 2n+1 = x(x n ) 2 1 n = 0 p(x,n) = (x k ) 2 = p(x, n 2 ) n = 2k x(x k ) 2 = x p(x, n 1 2 ) n = 2k + 1 Algorithm Power(x,n) Eingabe: nat\"urliche Zahlen x,n Ausgabe: x^n if n=0 then return 1 else if n gerade then y <- Power(x,n/2) return y*y else y <- Power(x,(n-1)/2) return y*y*x lineare Rekursion O(log n)
Power(x,n) Power(x,n/2) Power(x,n/4)... Power(x,1) T(x,n) O(1) + T(x, n 2 ) falls n gerade T(x,n) O(1) + T(x, n 2 ) falls n ungrade Algorithm Power(x,n) Eingabe: nat\"urliche Zahlen x,n Ausgabe: x^n if n=0 then return 1 else if n gerade then return Power(x,n/2)*Power(x,n/2) else return Power(x,n/2)*Power(x,n/2)*x # Op auf Schicht log n: c n # Op auf Schicht log n 1: c n/2 # Op auf Schicht log n k: c n/2 k Insgesamt: log n k=0 c n 2 k = c log n k=0 1 2 k c n Beispiel: Verfahren zum Summieren der Einträge eines Feldes. Algorithm Sum(A,i,n) Eingabe: Feld A von Zahlen mit Index i und Anzahl n Ausgabe: A[i]+A[i+1]+.. +A[i+n] if n=1 return A[i] return Sum(i,n/2)+Sum(i+n/2,(n+1)/2) Allgemein: # Op c log n i=0 2i = O(n)
Beispiel: Fibonacci Zahlen: F 0 = 0;F 1 = 1;F i = F i 1 + F i 2 Algorithm Fib(i) Eingabe i Ausgabe F(i) if(i=0) then return 0 if(i=1) then return 1 return fib(i-1)+fib(i-2) Schlechte Idee! Sei T(k) die Anzahl der rekursiven Aufrufe von Fib(k) T(0) = 1 T(1) = 1 T(2) = 1 + T(O) + T(1) T(k) = 1 + T(k 1) + T(k 2) F k 1 Da F k 2F k 2 hat man ein exponentielles Wachstum. F[0]=0 F[1]=1 F[i]=undef f\"ur 1<i<n Algorithm Fib(i) Eingabe i Ausgabe F(i) if F[i]=undef then F[i] = Fib(i-1) + Fib(i-2) return F[i] Beispiel für dynamisches Programmieren geht auch direkt über lineare Rekursion Übung Noch ( besser: ) ( ) Fi Fi 1 F = i 2 = F i 1 F i 1 ( 1 1 1 0 ) ( Fi 1 F i 2 d.h. v i = ( M v ) i 1 = M k v i k 1 mit v 1 = ; v 0 i = M i 1 v 1 M i 1 kann mittels sukz. Quadrieren in O(log(i)) berechnet werden! )
1.1.11 Rekursion in der Registermaschine Algorithm main x<-5 subroutine1(x) Algorithm subroutine1(i) k<- i+1 subroutine2(k) Algorithm subroutine2(j)... merken, welche Unterroutine gerade ausgeführt wird main subroutine1 subroutine2 Die Liste der laufenden Unterroutinen wird auf einem Methodenstapel (nach dem LIFO Prinzip) verwaltet Beim Aufruf einer neuen Methode legen wir auf den Stapel eine Datensatz der angibt, wer der Aufrufer ist, welche Rücksprungadresse ist zu verwenden und was sind die Werte der lokalen Variablen. ermöglicht Rekursion!
1.2 Datenstrukturen 1.2.1 Datenstruktur Stack Verwaltung eine Menge von Objekten nach LIFO Prinzip (im Moment soll der Stack nur ganze Zahlen beinhalten) erforderliche Operationen einfügen (push) füge das Argument der push-operation zu Menge verwalteten der Objekte hinzu extrahieren (pop) entfernt und liefert das letzte Element (isempty?) liefert true, wenn Stack leer ist (top) liefert das oberste Element ohne es zu entfernen Vorschau auf die algebraische Spezifikation Pop(Push(S,x)) = (S;x) Implementierung von Stacks mittels Feldern (Arrays) Allozieren eines Felds F der Größe M eine Zeigervariable z zeigt auf den ersten freien Eintrag in F ein push(x) speichert x in F[z] und setzt z z + 1 Falls z = M melde Fehler Stapel voll ein pop liefert F[z 1] und setzt z z 1 Falls z < 0 melde Fehler Stapel leer geht gut, solange nicht das Maximum M erreicht wird class IntArrayStack { int F[]; int z; int M; IntArrayStack(int m){ M = m; z = 0; F = new int[m]; } int pop() throws EmptyStackException {
} }... if(z == 0) throw... return F[--z]; Analyse: Linearer (in der Anzahl der in der Struktur verwalteten Objekte (M)) Platzbedarf O(1) für push + pop evtl. Ω(M) Zeit bei Initialisierung Problem/Einschränkung: Maximalgröße des Stapels muss bei Initialisierung bekannt sein Implementierungspezifische Ausnahmen! Implementierung von Stacks mittels verketteten Listen einfach verkettete Liste einfügen und entfernen am Anfang der Liste effektiv möglich Einfügen/Löschen am Anfang der Liste in O(1) am besten am 2. Element einfügen und vertauschen, damit Referenzen nicht zerstört werden. Einfügen/Löschen am Ende der Liste in Θ(n) Implementierung des Stapels Element im Stapel werden in der entsprechenden Reihenfolge in der e.v. Liste gespeichert. Dabei wird das oberste Element im Stapel am Anfang der Liste gespeichert. Platzbedarf Θ(n) Laufzeit push/pop/top O(1) 1.2.2 Indirekte Anwendung von Stapeln als Hilfsstruktur in anderen Datenstrukturen beim Entwurf von Algorithmen
Beispiel: Gegeben ist Feld X[0],,X[n 1] Berechnen für 0 i n 1 S[i] S[i] = maximale Anzahl von aufeinanderfolgenden unmittelbaren Element vor X[i], sodass X[j] X[i] Algorithm Span1(X,n) Eingabe: Feld X mit n Zahlen Ausgabe: Feld S S <- Neuse Feld von n Zahlen mit 0 for i=0 to n-1 do for j=0 to i do if X[i-j] <= X[i] then S[i]+=1 else break return S Analyse Auf benötigt der Algorithmus Θ(n 2 ) Es geht auch besser Abarbeiten von X von links nach rechts Wir speichern in einem Stack die Indizes der sichtbaren Elemente Am Index i hole Elemente aus dem Stapel, bis X[j] > X[i] (pop) Setze S[i] = i j Lege i auf den Stack (push) Algorithm Span2(X,n) S <- Neues Feld von n Zahlen A <- neuer leerer Stapel for i=0 to n-1 do while(!isempty(a) && X[top(A)] <= X[i]) pop(a) if(isempty(a)) then S[i] = i+1
else S[i] = i-top(a) push(a,i) Da nur n-mal gepushed wird, können wir nur n-mal popen. lineare Laufzeit 1.2.3 Dynamisierung Array-basierter Datenstrukturen Ziel: Stapel mit Arrays implementieren, aber die Größe der Struktur soll beliebig sein. Idee: Wir haben zu jeden Zeitpunkt ein Feld, in dem die Elemente des Stapels abspeichert sind. Wenn der Platz in dem Feld nicht mehr ausreicht, um alle Elemente des Stapels aufzunehmen, dann legen wir ein neues Feld (doppelter Größe) an und kopieren das alte Feld an dessen Anfang. Diese Struktur hat im worst-case ein schlechtes Laufzeitverhalten, da es push-operationen gibt, mit Ω(n) Laufzeit. (n = # Elemente im Stapel) ABER: Jede Folge von n push/pop Operationen benötigt O(n) Zeit Eine pop-operation benötigt im worst-case O(1) Zeit. D.h. alle pop-operationen Zusammen in einer Folge von n push/pop Operationen benötigen O(n) viel Zeit. Wir betrachten Folge von n push-operationen. Kosten # push m Kosten/Op 1000 1000 1000 1000 1 2000 < 2/op 2000 1000... 2000 1 4000 < 2/op 2000 2000... 4000 1 8000 < 2/op 4000 4000............ 1500 8000 < 2/op Die 2 i -te push-operation benoetigt O(2 i ) Alle anderen push-operationen benoetigen O(1)
{ O(2 mit T i ) fallsk = 2 i k O(1) sonst Gesamtzeit fuer n push-operationen: O(2 log n +1 1) + O(n) = O(n) n k=0 log n +1 log n T k = ( T 2 i)+o(n) = O( 2 i )+O(n) = k=0 k=0