Algorithmische Grundlagen Sommersemester 2016 – VL10 Martin Mundhenk Uni Jena, Institut für Informatik 20. Juni 2016 4 Algorithmen Wir haben bisher Programme zur Lösung verschiedener Probleme geschrieben. Jedes Programm basiert auf einer Idee (Algorithmus), die man in jeder Programmierprache aufschreiben kann. Beim Entwickeln von Algorithmen geht es u.a. darum, möglichst schnelle Algorithmen mit wenig Speicherplatzbedarf zu finden. In diesem Kapitel schauen wir uns zuerst an, wie man feststellt, wie schnell“ ” ein Algorithmus ist. Anschließend schauen wir uns Algorithmen zum Sortieren an, die (viel) schneller als das Sortieren durch wiederholte Suche des Minimums ist. Abschließend betrachten wir noch Algorithmen zur Wegesuche in Graphen. 4. Algorithmen 4.1 Rechenzeit 4.2 Schnelles Sortieren 4.3 Suche nach Wegen in Graphen 4.1 Rechenzeit von Programmen und Zeit-Komplexität von Problemen Wir haben z.B. bei der Berechnung von Fibonacci-Zahlen gesehen, dass verschiedene Programme sehr unterschiedlich lang brauchen. Wir wollen diese Zeit messen. #--------------------------------------------# miss-fib.py # Misst die Rechenzeit fuers rekursive Berechnen # der Fibonaccizahlen. # time.time() gibt die aktuelle Zeit an. #-------------------------------------------import stdio, sys, time, fibo_rekursiv oberschranke = int(sys.argv[1]) for n in range(1,50): anfangszeit = time.time() fibo_rekursiv.fib(n) endzeit = time.time() rechenzeit = endzeit - anfangszeit stdio.writef('%d %f\n', n, rechenzeit) if rechenzeit > oberschranke: break # python miss-fib.py 10 # 1 0.000002 # 2 0.000002 # 3 0.000002 ... # 15 0.000405 # 16 0.000654 # 17 0.001052 ... # 32 1.441119 # 33 2.333346 # 34 3.771449 # 35 6.112324 # 36 9.876957 # 37 16.031817 Beobachtungen Die Rechenzeit von Programmen wächst mit der Größe der Eingabe. Beispiel 1: Berechnung der Fibonacci-Zahlen def fib(n): if n==0 or n==1: return n else: return fib(n-1) + fib(n-2) Es wird die Rechenzeit in Sekunden abhängig vom Eingabewert n dargestellt. Hängt die Kurve vom Rechner ab? 4.1.2 Beobachtungen Die Rechenzeit von Programmen wächst mit der Größe der Eingabe. Beispiel 1: Berechnung der Fibonacci-Zahlen def fib(n): if n==0 or n==1: return n else: return fib(n-1) + fib(n-2) Es wird die Rechenzeit in Sekunden abhängig vom Eingabewert n dargestellt. Hängt die Kurve vom Rechner ab? Der grobe Verlauf“ der Kurven ist gleich. ” 4.1.2 Beobachtungen Die Rechenzeit von Programmen wächst mit der Größe der Eingabe. Beispiel 1: Berechnung der Fibonacci-Zahlen def fib(n): if n==0 or n==1: return n else: return fib(n-1) + fib(n-2) Es wird die Rechenzeit in Sekunden abhängig vom Eingabewert n dargestellt. Hängt die Kurve vom Rechner ab? Der grobe Verlauf“ der Kurven ist gleich. ” 4.1.2 Beobachtungen Die Rechenzeit von Programmen wächst mit der Größe der Eingabe. Beispiel 1: Berechnung der Fibonacci-Zahlen def fib(n): if n==0 or n==1: return n else: return fib(n-1) + fib(n-2) Es wird die Rechenzeit in Sekunden abhängig vom Eingabewert n dargestellt. Hängt die Kurve vom Rechner ab? Der grobe Verlauf“ der Kurven ist gleich. ” Er ist ähnlich der Kurve von f pnq “ 2n . 4.1.2 Beobachtungen Die Rechenzeit von Programmen wächst mit der Größe der Eingabe. Beispiel 1: Berechnung der Fibonacci-Zahlen def fib(n): if n==0 or n==1: return n else: return fib(n-1) + fib(n-2) Es wird die Rechenzeit in Sekunden abhängig vom Eingabewert n dargestellt. Hängt die Kurve vom Rechner ab? Der grobe Verlauf“ der Kurven ist gleich. ” Er ist ähnlich der Kurve von f pnq “ 2n . Und ziemlich genau der der Kurve von f pnq “ 0.00000155 ¨ 1.61n 4.1.2 Beobachtungen Die Rechenzeit von Programmen wächst mit der Größe der Eingabe. Beispiel 1: Berechnung der Fibonacci-Zahlen def fib(n): if n==0 or n==1: return n else: return fib(n-1) + fib(n-2) Es wird die Rechenzeit in Sekunden abhängig vom Eingabewert n dargestellt. Hängt die Kurve vom Rechner ab? Der grobe Verlauf“ der Kurven ist gleich. ” Er ist ähnlich der Kurve von f pnq “ 2n . Und ziemlich genau der der Kurve von f pnq “ 0.00000155 ¨ 1.61n bzw. f pnq “ 0.0000004 ¨ 1.61n . 4.1.2 Beobachtungen Die Rechenzeit von Programmen wächst mit der Größe der Eingabe. Beispiel 1: Berechnung der Fibonacci-Zahlen def fib(n): if n==0 or n==1: return n else: return fib(n-1) + fib(n-2) Es wird die Rechenzeit in Sekunden abhängig vom Eingabewert n dargestellt. Hängt die Kurve vom Rechner ab? Der grobe Verlauf“ der Kurven ist gleich. ” Er ist ähnlich der Kurve von f pnq “ 2n . Und ziemlich genau der der Kurve von f pnq “ 0.00000155 ¨ 1.61n bzw. f pnq “ 0.0000004 ¨ 1.61n . Alle Kurven sind anscheinend welche von Funktionen f pnq “ c ¨ 1.61n für verschiedene c. n Deshalb sagen wir: die Rechenzeit von fibpnq ist „ 1.61 . 4.1.2 Beispiel 2: Quadrieren einer Matrix def quadratM(A): C = stdarray.create2D(len(A),len(A),0) for z in range(len(A)): for s in range(len(A)): for i in range(len(A)): C[z][s] += A[z][i]*A[i][s] return C Zuerst messen wir die Zeit für das Quadrieren einer Matrix mit 200 Zeilen und Spalten, wobei wir größer werdende Werte in die Matrix eintragen. Die Rechenzeiten bleiben ziemlich gleich: sie hängen kaum“ von der Größe ” der zu multiplizierenden Zahlen ab. 4.1.3 Beispiel 2: Quadrieren einer Matrix def quadratM(A): C = stdarray.create2D(len(A),len(A),0) for z in range(len(A)): for s in range(len(A)): for i in range(len(A)): C[z][s] += A[z][i]*A[i][s] return C Zuerst messen wir die Zeit für das Quadrieren einer Matrix mit 200 Zeilen und Spalten, wobei wir größer werdende Werte in die Matrix eintragen. Die Rechenzeiten bleiben ziemlich gleich: sie hängen kaum“ von der Größe ” der zu multiplizierenden Zahlen ab. Messen wir dagegen die Zeit abhängig von der Seitenlänge der Matrix, steigt die Rechenzeit stark an. 4.1.3 Beispiel 2: Quadrieren einer Matrix def quadratM(A): C = stdarray.create2D(len(A),len(A),0) for z in range(len(A)): for s in range(len(A)): for i in range(len(A)): C[z][s] += A[z][i]*A[i][s] return C Zuerst messen wir die Zeit für das Quadrieren einer Matrix mit 200 Zeilen und Spalten, wobei wir größer werdende Werte in die Matrix eintragen. Die Rechenzeiten bleiben ziemlich gleich: sie hängen kaum“ von der Größe ” der zu multiplizierenden Zahlen ab. Messen wir dagegen die Zeit abhängig von der Seitenlänge der Matrix, steigt die Rechenzeit stark an. Wir vermuten, dass die Kurve der von f pnq “ n3 ähnelt. Die Rechenzeit von quadratMpAq ist „ n3 . Die Eingabegröße n ist dabei die Seitenlänge der Matrix A. 4.1.3 Beispiel 2: Quadrieren einer Matrix def quadratM(A): C = stdarray.create2D(len(A),len(A),0) for z in range(len(A)): for s in range(len(A)): for i in range(len(A)): C[z][s] += A[z][i]*A[i][s] return C Zuerst messen wir die Zeit für das Quadrieren einer Matrix mit 200 Zeilen und Spalten, wobei wir größer werdende Werte in die Matrix eintragen. Die Rechenzeiten bleiben ziemlich gleich: sie hängen kaum“ von der Größe ” der zu multiplizierenden Zahlen ab. Messen wir dagegen die Zeit abhängig von der Seitenlänge der Matrix, steigt die Rechenzeit stark an. Wir vermuten, dass die Kurve der von f pnq “ n3 ähnelt. Die Rechenzeit von quadratMpAq ist „ n3 . Die Eingabegröße n ist dabei die Seitenlänge der Matrix A. 4.1.3 Beispiel 3: Sortieren eines Arrays mit MinSort def minsort(liste): for i in range(len(liste)-1): min = i for j in range(i+1,len(liste)): if liste[min] > liste[j]: min = j t = liste[min] liste[min] = liste[i] liste[i] = t return liste Wir nehmen als Eingabegröße n gleich die Länge des Arrays. 4.1.4 Beispiel 3: Sortieren eines Arrays mit MinSort def minsort(liste): for i in range(len(liste)-1): min = i for j in range(i+1,len(liste)): if liste[min] > liste[j]: min = j t = liste[min] liste[min] = liste[i] liste[i] = t return liste Wir nehmen als Eingabegröße n gleich die Länge des Arrays. Der Vergleich mit f pnq “ n3 scheint nicht zu gehen. 4.1.4 Beispiel 3: Sortieren eines Arrays mit MinSort def minsort(liste): for i in range(len(liste)-1): min = i for j in range(i+1,len(liste)): if liste[min] > liste[j]: min = j t = liste[min] liste[min] = liste[i] liste[i] = t return liste Wir nehmen als Eingabegröße n gleich die Länge des Arrays. Der Vergleich mit f pnq “ n3 scheint nicht zu gehen. Der Vergleich mit f pnq “ n2 sieht vielversprechend aus. 4.1.4 Beispiel 3: Sortieren eines Arrays mit MinSort def minsort(liste): for i in range(len(liste)-1): min = i for j in range(i+1,len(liste)): if liste[min] > liste[j]: min = j t = liste[min] liste[min] = liste[i] liste[i] = t return liste Wir nehmen als Eingabegröße n gleich die Länge des Arrays. Der Vergleich mit f pnq “ n3 scheint nicht zu gehen. Der Vergleich mit f pnq “ n2 sieht vielversprechend aus. Die Rechenzeit von minsortpAq ist „ n2 . Die Eingabegröße n ist dabei die Länge des Arrays A. 4.1.4 Beispiel 4: Suche des Maximums def maxsuche(liste): max = liste[0] for i in range(1,len(liste)): if liste[i] > max: max = liste[i] return max Wir nehmen als Eingabegröße n die Anzahl der Einträge im Array. 4.1.5 Beispiel 4: Suche des Maximums def maxsuche(liste): max = liste[0] for i in range(1,len(liste)): if liste[i] > max: max = liste[i] return max Wir nehmen als Eingabegröße n die Anzahl der Einträge im Array. Der Vergleich mit f pnq “ n scheint nicht zu gehen. 4.1.5 Beispiel 4: Suche des Maximums def maxsuche(liste): max = liste[0] for i in range(1,len(liste)): if liste[i] > max: max = liste[i] return max Wir nehmen als Eingabegröße n die Anzahl der Einträge im Array. Der Vergleich mit f pnq “ n scheint nicht zu gehen. Die Rechenzeit von maxsuche(liste) ist „ n. Die Eingabegröße n ist dabei die Länge von liste. 4.1.5 Zusammenfassung der Beobachtungen Wir haben die Rechenzeiten verschiedener Programme durch einfache Funktionen charakterisieren können. Funktion fib(m) quadratM(A) minsort(A) maxsuche(A) Eingabegröße n Wert von m Seitenlänge von A Länge von A Länge von A Rechenzeit „ n 1, 61 Bezeichnung der Rechenzeit exponentiell n 3 kubisch n 2 quadratisch n ¨ log n quasi-linear n linear Diese Art der Analyse der Rechenzeit beruht auf Experimenten (Ermittlung der Rechenzeit für einige Eingaben) und Herumprobieren, bis ein hinreichend ähnlicher Funktionsgraph gefunden wurde. Wie kann man ohne Experimente die Rechenzeit abschätzen? 4.1.6 Mathematische Analyse Die Rechenzeit einer einfachen Wertzuweisung an eine Variable dauert Zeit c, ist also „ 1. Beispiel: a = b + 2*c if liste[min] > liste[j]: min = j Eine Schleife (while, for) besteht aus einer Bedingung und einem Anweisungsblock. a = 0 while n > 1: n = n/2 a = a+1 for i in min = for j if range(len(liste)-1): i in range(i+1,len(liste)): liste[min] > liste[j]: min = j t = liste[min] liste[min] = liste[i] liste[i] = t Die Rechenzeit einer Schleife ist die Summe der Rechenzeiten jeder Wiederholung des Anweisungsblocks . 4.1.7 Programm Rechenzeit „ n = int(sys.argv[1]) 1 a = 0 1 Wiederholungen der Schleife log n while n > 1: n = n/2 1 a = a+1 1 Die Rechenzeit des Programms ist „ 1 ` 1 ` 2 ¨ log n. Faustregeln für Abschätzungen mit „: § Formeln so weit wie möglich zusammenfassen. § Bei Summen streicht man alles bis auf den größten Summanden. Eine Summe 1 ` 1 ` 1 ` 1 ist „ 1. § Bei Produkten streicht man alle Konstanten Faktoren. Damit bleibt übrig: die Rechenzeit des Programms ist „ log n. Das ist im Prinzip“ die Anzahl der Wiederholungen der Schleife. ” 4.1.8 Im folgenden Programm ist die Eingabegröße n die Länge von liste. Programm max = liste[0] for i in range(1,len(liste)): if liste[i] > max: max = liste[i] Rechenzeit „ 1 Wiedh. der Schleife n´1 1 Die Rechenzeit des Programms ist „ 1 ` pn ´ 1q „ n. Das ist im Prinzip“ wieder die Anzahl der Wiederholungen der Schleife. ” 4.1.9 Im folgenden Programm ist die Eingabegröße n die Länge von liste. Programm for i in range(len(liste)-1): min = i for j in range(i+1,len(liste)): if liste[min] > liste[j]: min = j t = liste[min] liste[min] = liste[i] liste[i] = t Rechenzeit „ Wiedh. n´2 1 n´i ´1 1 1 1 1 Die Rechenzeit des Programms ist „ pn ´ 2q ¨ 4 ` pn ´ 2q ` pn ´ 3q ` pn ´ 4q ` . . . ` 1 “ pn ´ 2q ¨ 4 ` n´2 ÿ i “ pn ´ 2q ¨ 4 ` i“1 1 “ 4n ´ 8 ` ¨ pn2 ´ 3n ` 2q 2 „ 1 ¨ pn ´ 1q ¨ pn ´ 2q 2 n ` n2 ´ n ` 1 „ n2 Damit bleibt übrig: die Rechenzeit des Programms ist „ n2 . Im Prinzip“: ” zwei ineinandergeschachtelte Schleifen mit „ n Wiederholungen haben Rechenzeit „ n2 . 4.1.10 Im folgenden Programm ist die Eingabegröße n die Seitenlänge von A. # quadratM(A) gibt das Quadrat A**2 der quadratischen Matrix A # als Ergebnis zurück. def quadratM(A): C = stdarray.create2D(len(A),len(A),0) for z in range(len(A)): for s in range(len(A)): for i in range(len(A)): C[z][s] += A[z][i]*A[i][s] return C Die Rechenzeit für einen Funktionsaufruf von quadratM(A) ist „ n ¨ n ¨ n “ n3 . Faustregel“: ” drei ineinandergeschachtelte Schleifen mit „ n Wiederholungen haben Rechenzeit „ n3 . Die Analyse der Rechenzeit rekursiver Funktionen kann komplizierter sein. def fib(n): if n==0 or n==1: return n else: return fib(n-1) + fib(n-2) Wir definieren eine Funktion T , so dass T pnq die Rechenzeit von fibpnq ist. § Für T p0q “ 1 und T p1q “ 1. § Für n ě 2 ist T pnq “ T pn ´ 1q ` T pn ´ 2q. Man sieht: T pnq “ fibpn ` 1q. Wir erinnern uns: ˜ˆ ? ˙n ˆ ? ˙n ¸ 1` 5 1´ 5 1 ´ fibpnq “ ? ¨ 2 2 5 Dann ist 1 T pnq „ ? ¨ 5 ˜ˆ ? ˙n`1 ˆ ? ˙n`1 ¸ 1` 5 1´ 5 ´ 2 2 „ 1, 61n`1 ´ 0, 6n`1 „ 1, 61n 4.1.12 Wir haben bei der mathematischen Analyse die gleichen Ergebnisse gefunden wie bei der Analyse mittels Herumprobieren. Ein weiterer Vorteil der mathematischen Analyse ist, dass man die Rechenzeit eines Algorithmus analysieren kann, ohne dass man ihn in einer Programmiersprache implementiert haben muss. Eine mathematische Bemerkung: Wir schreiben „ an Stelle der mathematischen O-Notation ( Oh-Notation“). ” Die O-Notation dient dem Abschätzen des asymptotischen Wachstums von Funktionen. Wir schreiben f „ g statt f P Opg q. Deshalb bedeutet f „ g : f wächst asymptotisch nicht stärker als g . Die Rechenzeit ist „ n3“ bedeutet dann: ” die Rechenzeit wächst nicht stärker als n3 . Die formale Definition von f P Opg q ist Dc Dn0 @n ě n0 : f pnq ď c ¨ g pnq ` c. 4.1.13 Zusammenfassung Wir haben gesehen, dass es zur Abschätzung von Rechenzeiten von Programmen ein realistisches und gut benutzbares mathematisches Modell gibt. Die folgende Tabelle enthält typische Rechenzeiten und den Zuwachs an Rechenzeit eines Programm, das bei Eingabegröße m wenige Sekunden braucht, bei einer Eingabe der Größe 100 ¨ m. Rechenzeit Verlängerung der Rechenzeit (s.o.) linear wenige Minuten quasi-linear wenige Minuten quadratisch einige Stunden kubisch ein paar Wochen exponentiell ewig 4.1.14