algorithmische grundlagen einführung in das programmieren ... · zum messen der rechenzeit...

37
4 Algorithmen Wir haben bisher Programme zur L¨ osung 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¨ oglichst schnelle Algorithmen mit wenig Speicherplatzbedarf zu finden. In diesem Kapitel schauen wir uns zuerst an, wie man experimentell feststellt, wie schnell“ ein Algorithmus ist. Anschließend schauen wir uns Algorithmen zum Sortieren an, die verbl¨ uffend schnell sind. 4. Algorithmen 4.1 Rechenzeit 4.2 Schnelles Sortieren

Upload: nguyendieu

Post on 25-Aug-2019

218 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

4 Algorithmen

Wir haben bisher Programme zur Losung verschiedener Probleme geschrieben.Jedes Programm basiert auf einer Idee (Algorithmus), die man in jederProgrammierprache aufschreiben kann.Beim Entwickeln von Algorithmen geht es u.a. darum, moglichst schnelleAlgorithmen mit wenig Speicherplatzbedarf zu finden.

In diesem Kapitel schauen wir uns zuerst an, wie man experimentell feststellt,wie

”schnell“ ein Algorithmus ist.

Anschließend schauen wir uns Algorithmen zum Sortieren an,die verbluffend schnell sind.

4. Algorithmen4.1 Rechenzeit4.2 Schnelles Sortieren

Page 2: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

4.1 Rechenzeit von Programmen

Wir haben z.B. bei der Berechnung von Potenzen 2**n und bei der

Berechnung von Fibonacci-Zahlen gesehen, dass

verschiedene Programme/Funktionen fur ein Berechnungsproblem zwar

das gleiche Ergebnis berechnen, jedoch sehr unterschiedlich schnell sind.

Dieser Unterschied zeigte sich insbesondere dadurch, dass bei großeren

Eingaben der Unterschied immer großer wurde.

Wir wollen die Rechenzeit von Programmen bei großer werdenden

Eingaben messen und eine quantitative Aussage uber den Unterschied der

Rechenzeiten machen.

Diese Aussage muss allgemeiner sein als eine konkrete Voraussage der

Rechenzeit in Sekunden, da sie z.B. fur Rechner verschiedener

Geschwindigkeit nutzlich sein soll.

4.1.1

Page 3: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modultime.py, die die Zeit seit dem 1.1.1970 in Sekunden als float zuruckgibt.

Jede Messung sieht wie folgt aus:schaue auf die Uhrstarte die zu messende Funktion und warte, bis sie beendet istschaue auf die Uhrdie Differenz der Uhrzeiten ist die Rechenzeit

Wir werden eine Messreihe aus einer Reihe von Messungen durchfuhren, beidenen das Argument fur die Funktion immer großer wird.

Der Einfachheit halber wollen wir Funktionen betrachten,die nur von einem Argument abhangig sind.

Wir beginnen mit zwei Funktionen aus dem Modul potenzieren.py:

potLangsam(n,a=2) gibt a**n zuruck; das Ergebnis wird mit n-fachem Multipli-zieren berechnet

potSchnell(n,a=2) gibt a**n zuruck; das Ergebnis wird mit wiederholten Qua-drieren berechnet

4.1.2

Page 4: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

#----------------------------------------------------------------------------------

# potenzieren.py

#----------------------------------------------------------------------------------

# Das Modul stellt zwei Funktion zur Berechnung von a**n bereit:

# potLangsam(n,a=2) und potSchnell(n,a=2).

#----------------------------------------------------------------------------------

# potLangsam(n,a=2) berechnet a**n duch n-fache Multiplikation von a.

# Das ist die langsame Art des Potenzierens.

def potLangsam(n,a=2):

erg = 1

for i in range(n):

erg = erg * a

return erg

#----------------------------------------------------------------------------------

# potSchnell(n,a=2) berechnet "a hoch n" mittels wiederholtem Quadrieren.

# Das ist die schnelle Art des Potenzierens.

def potSchnell(n,a=2):

if n==0: return 1

if n%2==1: return potSchnell(n-1,a)*a

b = potSchnell(n//2,a)

return b*b

4.1.3

Page 5: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

# messreihe-v1.py

#---------------------------------------------------------------------------------------------------

# Liest von der Kommandozeile einen Startwert fur n (int) und eine Versuchsdauer (int) in Sekunden.

# Gibt die Zeit-Messungen der Funktion potLangsam aus dem Modul potenzieren.py aus.

# Die Argumente fur den Funktionsaufruf beginnen mit n+1 und werden bei jeder Messung

# um 1 erhoht, bis die Versuchsdauer erreicht wird.

#---------------------------------------------------------------------------------------------------

import sys, time, random

n = int(sys.argv[1])

versuchsdauer = int(sys.argv[2])

# Importiere das Modul mit der Funktion, die fur die Messreihe benutzt wird.

from potenzieren import potLangsam

# versuchsEnde ist die Zeit, bei der der Versuch beendet wird.

versuchsEnde = time.time() + versuchsdauer

# messEnde ist die Zeit der Beendigung der letzten Messung.

messEnde = 0

# Fuhre die Messreihe mit wachsendem n durch, bis die Zeit abgelaufen ist.

while messEnde < versuchsEnde:

# Bereite die Messung vor.

n += 1

# Rufe die Funktion auf und miss die verbrauchte Rechenzeit.

messAnfang = time.time()

potLangsam(n)

messEnde = time.time()

messErgebnis = messEnde - messAnfang

# Gib das Ergebnis der Messung aus.

print( '%d %f' % (n, messErgebnis) )

4.1.4

Page 6: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Wir messen die Rechenzeit mit Startwert 1000 fur 100 Sekundenund bekommen einen Haufen Daten zuruck.Die Ergebnisse des Experiments sind in der Graphik dargestellt.

# python messreihe-v1.py 1000 100

1000 0.000124

1001 0.000120

1002 0.000114

1003 0.000115

1004 0.000237

1005 0.000123

1006 0.000132

1007 0.000119

1008 0.000128

1009 0.000128

...

21428 0.014031

21429 0.013911

21430 0.014911

21431 0.013716

21432 0.013503

21433 0.015214

Kann man irgendwas erkennen?

4.1.5

Page 7: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Wir wollen unseren Versuchsaufbau auch fur andere Funktionen aus anderenModulen benutzen konnen.Dafur nehmen wir ein paar kleine Anderungen am Programm vor.

Der Modulname und der Funktionsname soll von der Kommandozeile gelesenwerden.

Wir machen etwas, was man eigentlich nicht machen soll:wir benutzen die Python-Funktion exec(s).

Ihr Aufruf fuhrt die Programmzeile s (string) aus.

Beispielprogramm versuch.py:

modulname = sys.argv[1]

funktionsname = sys.argv[2]

n = int(sys.argv[3])

exec('import ' + modulname)

exec(modulname + '.' + funktionsname + '(n)')

Bei Aufruf python versuch.py potenziere potLangsam werden(1) die drei Argumente von der Kommandozeile gelesen(2) import potenziere ausgefuhrt(3) potenziere.potLangsam(n) ausgefuhrt

4.1.6

Page 8: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

# messreihe-v2.py

#-----------------------------------------------------------------------------------

# Wie messreihe-v1.py, aber liest folgendes von der Kommandozeile:

# einen Modulnamen (string), einen Funktionsnamen aus diesem Modul (string)

# einen Startwert fur n (int), eine Versuchsdauer (int) in Sekunden.

#-----------------------------------------------------------------------------------

import sys, time

modulname = sys.argv[1]

funktionsname = sys.argv[2]

n = int(sys.argv[3])

versuchsdauer = int(sys.argv[4])

# Importiere das Modul mit der Funktion, die fur die Messreihe benutzt wird.

exec('from ' + modulname + ' import ' + funktionsname)

# versuchsEnde ist die Zeit, bei der der Versuch beendet wird.

versuchsEnde = time.time() + versuchsdauer

# messEnde ist die Zeit der Beendigung der letzten Messung.

messEnde = 0

# Fuhre die Messreihe mit wachsendem n durch, bis die Zeit abgelaufen ist.

while messEnde < versuchsEnde:

# Bereite die Messung vor.

n += 1

# Rufe die Funktion auf und miss die verbrauchte Rechenzeit.

messAnfang = time.time()

exec(funktionsname + '(n)')

messEnde = time.time()

messErgebnis = messEnde - messAnfang

# Gib das Ergebnis der Messung aus.

print( '%d %f' % (n, messErgebnis) )4.1.7

Page 9: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Wir fuhren (fast) den gleichen Versuch mit der schnellen Potenzierfunktiondurch und bekommen noch mehr Daten zuruck.

# python messreihe-v2.py potenzieren potSchnell 1000000 25

1000000 0.005106

1000001 0.004900

1000002 0.005005

1000003 0.004979

1000004 0.004953

1000005 0.004787

...

1005177 0.004764

1005178 0.004735

1005179 0.004802

1005180 0.004742

1005181 0.004771

1005182 0.004751

1005183 0.004791

1005184 0.004708

1005185 0.004743

1005186 0.004723

Kann man irgendwas erkennen?

4.1.8

Page 10: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Bei”ahnlich großen“ Zahlen unterscheiden sich die Rechenzeiten kaum.

Deshalb betrachtet man die Rechenzeit eines Programms nicht abhangig vomZahlenwert der Argumente,sondern von ihrem Speicherbedarf – er wird als Lange bezeichnet.

Zahlen (int) werden binar gespeichert – als Folge von 0/1-Bits.

Die Lange einer Zahl ist die Anzahl ihrer Bits.

Um aus einer Zahl m eine Zahl zu machen,deren Lange ein Bit großer ist,

kann man m mit 2 multiplizieren.

Statt die Rechenzeit fur Eingabewertem,m ` 1,m ` 2,m ` 3, . . .

zu ermitteln, machen wir es nun furm, 2 ¨m, 2 ¨ 2 ¨m, . . . , 2i ¨m, . . .

Damit erwischen wir dann ab Startwert m jeweils eine Zahl, deren Lange 1großer ist als bei der vorigen Zahl.

Im Programm mussen wir dafur nur die Zeile n += 1 durch n *= 2 ersetzen.

Das ist dann messreihe-v3.py. 4.1.9

Page 11: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Wir messen die Rechenzeit von potLangsam mit Startwert 10 fur 100Sekunden.Es wird also 2n berechnet fur n “ 10, 20, 40, . . ..Wir bekommen nicht mehr besonders viele Daten zuruck.

# python messreihe-v3.py potenzieren potLangsam 10 100

10 0.000022

20 0.000018

40 0.000015

80 0.000018

160 0.000026

320 0.000043

640 0.000083

1280 0.000190

2560 0.000425

5120 0.001185

10240 0.003683

20480 0.011922

40960 0.040362

81920 0.146418

163840 0.575278

327680 2.267954

655360 9.310225

1310720 37.495275

2621440 153.379177

# python messreihe-v3.py potenzieren potSchnell 10 100

10 0.000037

20 0.000030

40 0.000025

80 0.000026

160 0.000027

320 0.000027

...

1310720 0.005600

2621440 0.011480

5242880 0.023633

10485760 0.049603

...

2684354560 18.744394

5368709120 38.501850

10737418240 81.293822

Kann man irgendwas erkennen?4.1.10

Page 12: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Wir zeichnen die Messergebnisse mal als Funktionsgraphen . . .

. . . und konnen nichts erkennen, weil die Achsen sich so stark unterscheiden . . .

4.1.11

Page 13: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Also zeichnen wir die Messergebnisse gemeinsam in ein Koordinatensystem . . .

Man sieht, dass potSchnell viel schnellerals potLangsam ist . . . aber das wusstenwir auch vorher schon.

Der offensichtlichste Unterschied zwischen den beiden Funktionen ist ihre Steigung.

Wie kann man diesen Unterschied quantifizieren?

Dazu vergleichen wir nun die Rechenzeiten von aufeinanderfolgenden Messungen

und geben den Faktor an, um den die Rechenzeit bei der zweiten Messung großer ist.

4.1.12

Page 14: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

# expoTest-int.py

#-----------------------------------------------------------------------------------

# Wie messreihe-v3.py, aber:

# Bei jeder Messung wird der Faktor ausgegeben, um den die Rechenzeit im Vergleich

# zur vorherigen Messung gewachsen ist.

#-----------------------------------------------------------------------------------

import sys, time

# Kommandozeile lesen und Funktion aus Modul exportieren: wie gehabt ...

versuchsEnde = time.time() + versuchsdauer # die Zeit, bei der der Versuch beendet wird

messEnde = 0 # die Zeit der Beendigung der letzten Messung

letzteMessung = 0 # das Ergebnis der letzten Messung

print( ' %11s | %12s | %6s \n%s' % ('n', ' Rechenzeit', 'Faktor', ' '+'-'*36) )

# Fuhre die Messreihe mit wachsendem n durch, bis die Zeit abgelaufen ist.

while messEnde < versuchsEnde:

# Bereite die Messung vor.

n *= 2

# Rufe die Funktion auf und miss die verbrauchte Rechenzeit.

messAnfang = time.time()

exec(funktionsname + '(n)')

messEnde = time.time()

messErgebnis = messEnde - messAnfang

# Gib das Ergebnis der Messung aus.

if letzteMessung>0:

print( ' %12d | %10.3f | %6.2f ' % (n, messErgebnis, messErgebnis/letzteMessung) )

# Bereite die nachste Messung vor.

letzteMessung = messErgebnis

4.1.13

Page 15: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Jetzt sehen wir tatsachlich mal einen quantitativen Unterschied zwischen denRechenzeiten der beiden Potenzierfunktionen.

python expoTest-int.py \

potenzieren potLangsam 10000 3600

n | Rechenzeit | Faktor

--------------------------------

40000 | 0.031 | 2.27

80000 | 0.113 | 3.63

160000 | 0.434 | 3.85

320000 | 1.690 | 3.89

640000 | 7.062 | 4.18

1280000 | 27.944 | 3.96

2560000 | 113.503 | 4.06

5120000 | 477.411 | 4.21

10240000 | 1865.149 | 3.91

python expoTest-int.py \

potenzieren potSchnell 1000000 100

n | Rechenzeit | Faktor

------------------------------------

4000000 | 0.020 | 2.21

8000000 | 0.039 | 1.98

16000000 | 0.078 | 2.01

32000000 | 0.160 | 2.06

64000000 | 0.331 | 2.07

128000000 | 0.684 | 2.06

256000000 | 1.410 | 2.06

512000000 | 2.955 | 2.10

1024000000 | 6.277 | 2.12

2048000000 | 13.130 | 2.09

4096000000 | 27.160 | 2.07

8192000000 | 56.722 | 2.09

Wenn die Lange der Eingabe um 1 vergroßert wird, dannwachst bei potLangsam die Rechenzeit mit Faktor 4, undbei potSchnell wachst die Rechenzeit mit Faktor etwa 2.1.

4.1.14

Page 16: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Was haben wir bis jetzt?

Wir suchen die Funktion t,die als Argument die Lange ` einer Zahl bekommtund als Funktionswert tp`q die maximale Rechenzeit von potLangsam

bei Eingabe einer Zahl der Lange ` hat.

Wir haben experimentell festgestellt, dass tp`` 1q “ 4 ¨ tp`q ist (fur alle `).

Wenn tpkq “ w ist fur irgendein k und irgendeinen Wert w , dann wissen wir:

tpkq “ w

tpk ` 1q “ 4 ¨ w

tpk ` 2q “ 4 ¨ 4 ¨ w

......

...

tpk ` iq “ 4i ¨ w

Also gibt es eine Konstante c mit w “ 4k ¨ c , so dassdie experimentell geschatzte Rechenzeit tp`q “ c ¨ 4` ist.

4.1.15

Page 17: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Rechenzeitfunktionen

Bei Rechenzeitfunktionen ignorieren wir konstante Faktoren und”kleine“

Summanden. Mathematisch gesprochen: man interessiert sich nur fur dieGroßenordnung des Wachstums der Funktion.

Fur die experimentell geschatzte Rechenzeit tp`q “ c ¨ 4` von potLangsam

erhalten wir dann die Rechenzeitfunktion T p`q “ 4`.

Fur potSchnell erhalten wir so die Rechenzeitfunktion T p`q “ 2.1`.

Großenordnung des Wachstums

Beschreibung Rechenzeitfunktion T p`q

konstant 1

logarithmisch log `

linear `

quasi-linear ` ¨ log `

quadratisch `2

kubisch `3

exponentiell 2`, 3`, . . .

Ganz grob sagt man:

potLangsam und potSchnell habenexponentielle Rechenzeit.

4.1.16

Page 18: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Zusammenfassung:

Test auf exponentielle Rechenzeit

§ Miss die Rechenzeit der Funktion fur Eingaben aufeinanderfolgender Lange.Sind die Eingaben Zahlen, dann verdoppelt man sie fur jede Messung.Sind die Eingaben Arrays,

dann vergroßert man das Array fur jede Messung um einen Eintrag.

§ Bestimme den Faktor z ,um den sich die Rechenzeit von Messung zu Messung vergroßert.

§ Wenn der Faktor z mindestens 2 ist,dann hat die Funktion exponentielle Rechenzeit: T p`q “ z`,

d.h. T p`q “ p2`qlog2 z .

§ Wenn der Faktor z kleiner als 2 ist,dann ist die Rechenzeit der Funktion schneller als exponentiell.

Wir haben gesehen: potLangsam und potSchnell haben exponentielle Rechenzeit.

Wir werden den Test jetzt fur verschiedene Funktionen zur Berechnung derFibonacci-Zahlen durchfuhren.

4.1.17

Page 19: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

# fibonacci.py

#-------------------------------------------------------------------------------

# Berechnung der Fibonacci-Funktion mit schnellen und langsamen Funktionen.

#-------------------------------------------------------------------------------

# fiboDirekt(n) gibt fib_n zuruck.

# Die Berechnung erfolgt iterativ durch Berechnung von fib_0, fib_1, ..., fib_n.

def fiboDirekt(n):

f = [ 0 , 1 ]

for i in range(2,n+1): f[i%2] = f[0] + f[1]

return f[n%2]

#-------------------------------------------------------------------------------

# fiboRekursiv(n) gibt fib_n zuruck.

# Die Berechnung erfolgt rekursiv uber die Definition der Fibonacci-Zahlen.

def fiboRekursiv(n):

if n==0 or n==1: return n

else: return fiboRekursiv(n-1) + fiboRekursiv(n-2)

#-------------------------------------------------------------------------------

# fibo(b) gibt (fib_{b-1},fib_{b}) zuruck. Wird von fiboSchnell benutzt.

# Es gilt: fib_{2n} = fib_{n}*fib_{n-1} + fib_{n+1}*fib_{n}

# fib_{2n+1} = fib_{n}**2 + fib_{n+1}**2

def fibo(b):

if b<=1: return (0,b)

if b%2==1:

(x,y) = fibo(b//2)

return ( y*x+(x+y)*y, y**2+(x+y)**2 )

(x,y) = fibo(b//2)

return ( x*x+y*y, y*(2*x+y) )

#-------------------------------------------------------------------------------

# fiboSchnell(n) gibt fib_n zuruck.

def fiboSchnell(n): return fibo(n)[1]4.1.18

Page 20: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

fiboDirekt hat exponentielle Rechenzeit

# python expoTest-int.py fibonacci fiboDirekt 1000 300

n | Rechenzeit | Faktor

------------------------------------

4000 | 0.002 | 2.48

8000 | 0.004 | 2.30

16000 | 0.012 | 2.63

32000 | 0.030 | 2.61

64000 | 0.078 | 2.61

128000 | 0.267 | 3.40

256000 | 0.895 | 3.36

512000 | 2.654 | 2.97

1024000 | 10.895 | 4.10

2048000 | 42.981 | 3.95

4096000 | 169.500 | 3.94

8192000 | 672.399 | 3.97

Es sieht aus, dass fiboDirekt Rechenzeitfunktion T p`q “ 4` hat.

4.1.19

Page 21: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

fiboRekursiv hat exponentielle Rechenzeit

Die Zeitmessung von fiboRekursiv fliegt einem gleich um die Ohren.

Die Rechenzeit ist tatsachlich doppelt exponentiell,

also sowas wie T p`q “ 22` .

# python expoTest-int.py fibonacci fiboRekursiv 5 100

n | Rechenzeit | Faktor

------------------------------------

20 | 0.005 | 34.35

40 | 31.082 | 6801.26

...

4.1.20

Page 22: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

fiboSchnell hat exponentielle Rechenzeit

fiboSchnell hat Rechenzeit T p`q “ 3`.

Das ist auch exponentiell, aber deutlich schneller als fiboDirekt.

# python expoTest-int.py fibonacci fiboSchnell 1000 200

n | Rechenzeit | Faktor

------------------------------------

4000 | 0.000 | 0.01

8000 | 0.000 | 1.01

16000 | 0.000 | 2.05

32000 | 0.001 | 3.19

64000 | 0.002 | 2.42

128000 | 0.007 | 3.02

256000 | 0.022 | 3.00

512000 | 0.060 | 2.79

1024000 | 0.119 | 1.98

2048000 | 0.235 | 1.98

4096000 | 0.762 | 3.24

8192000 | 2.206 | 2.90

16384000 | 6.048 | 2.74

32768000 | 17.911 | 2.96

65536000 | 54.247 | 3.03

131072000 | 163.394 | 3.01

Es sieht aus, dass fiboDirekt Rechenzeitfunktion T p`q “ 3` hat.4.1.21

Page 23: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Beim Test der Funktion log aus dem Modul testfunktionen.py

erhalten wir einen Faktor, der kleiner ist als 2.

# python expoTest-int.py testfunktionen log 100000 0.0003

n | Rechenzeit | Faktor

------------------------------------

400000 | 0.000 | 0.55

800000 | 0.000 | 0.91

1600000 | 0.000 | 1.00

3200000 | 0.000 | 0.90

...

3276800000 | 0.000 | 1.11

6553600000 | 0.000 | 1.00

13107200000 | 0.000 | 1.00

26214400000 | 0.000 | 1.00

Die Rechenzeit der Funktion ist nicht exponentiell,

sondern lasst sich durch eine kleinere Rechenzeitfunktion abschatzen:

ein Polynom T p`q “ ` k .4.1.22

Page 24: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Test auf polynomielle Rechenzeit

§ Miss die Rechenzeit der Funktion fur Eingaben,deren Lange sich von Messung zu Messung verdoppelt.

Sind die Eingaben Zahlen, dann quadriert man sie fur die folgende Messung.Sind die Eingaben Arrays,

dann verdoppelt man die Array-Große fur die folgende Messung.

§ Bestimme den Faktor z ,um den sich die Rechenzeit von Messung zu Messung vergroßert.

§ Wenn der Faktor z mindestens 2 ist,dann hat die Funktion polynomielle Rechenzeit: T p`q “ ` log2 z .

Wir werden uns Beispiele dazu anschauen.

4.1.23

Page 25: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Aus expoTest-int.py erhalten wir polyTest-int.py,indem wir die Zeile n *= 2 andern zu n *= n.Wir werden den Polynomialzeittest an folgenden Funktionen ausprobieren.

# testfunktionen.py

#------------------------------------------------------------------------------------

# ggT(a,b) gibt den großten gemeinsamen Teiler von a und b zuruck.

def ggT(a,b):

while not a%b==0: a, b = b, a%b

return b

#------------------------------------------------------------------------------------

# ggTwrapper(a) ruft die Funktion ggT (s.o.) auf, hat aber nur ein Argument,

# damit man sie mit polynomialzeit-test.py benutzen kann.

# Man weiß, dass die Berechnung ggT(a,b) am langsten dauert, wenn a und b

# aufeinanderfolgende Fibonacci-Zahlen sind. Diese unterscheiden sich etwa um den

# Faktor 1.61. Das habe ich hier versucht mit ganzzahligen Operationen hinzubekommen.

def ggTwrapper(a): return ggT(a*161//100, a)

#------------------------------------------------------------------------------------

# Funktion log(n,b=2) gibt den

# ganzzahligen Logarithmus von n zur Basis b zuruck.

def log(n,b=2):

ergebnis = 0

while n > 1:

ergebnis += 1

n = n//b

return ergebnis

#------------------------------------------------------------------------------------

# quadrat(a) gibt a*a zuruck.

def quadrat(a): return a*a

4.1.24

Page 26: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

# python polyTest-int.py testfunktionen log 100000000 100

Argumentlange | Rechenzeit | Faktor

-------------------------------------

33 | 0.000 | 0.98

65 | 0.000 | 1.63

129 | 0.000 | 2.68

257 | 0.001 | 1.82

513 | 0.003 | 3.89

1025 | 0.008 | 3.23

2049 | 0.025 | 3.02

4097 | 0.093 | 3.66

8193 | 0.207 | 2.23

16385 | 0.629 | 3.04

32769 | 2.742 | 4.36

65537 | 9.815 | 3.58

131073 | 39.272 | 4.00

262145 | 155.463 | 3.96

Der Faktor, um den sich die Rechenzeitverlangert, ist etwa 4.

Also hat log die RechenzeitfunktionT p`q “ `2.

4.1.25

Page 27: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

# python polyTest-int.py testfunktionen ggTwrapper 1000000000 100

Argumentlange | Rechenzeit | Faktor

-------------------------------------

37 | 0.000 | 0.54

73 | 0.000 | 1.81

145 | 0.000 | 0.84

289 | 0.000 | 2.06

577 | 0.000 | 0.58

1153 | 0.000 | 1.01

2305 | 0.000 | 1.26

4609 | 0.000 | 1.50

9217 | 0.000 | 1.57

18433 | 0.001 | 1.78

36865 | 0.001 | 2.16

73729 | 0.001 | 1.25

147457 | 0.002 | 1.20

294913 | 0.005 | 3.29

589825 | 0.005 | 1.02

1179649 | 0.011 | 1.95

2359297 | 0.021 | 1.99

4718593 | 0.043 | 2.05

Der Faktor, um den sich die Rechenzeitverlangert, ist etwa 2.

Also hat ggT die RechenzeitfunktionT p`q “ `.

4.1.26

Page 28: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

# python polyTest-int.py testfunktionen quadrat 1000000000 100

Arg.lange | Rechenzeit | Faktor

--------------------------------------

37 | 0.000 | 0.63

73 | 0.000 | 1.31

145 | 0.000 | 0.91

289 | 0.000 | 1.07

577 | 0.000 | 1.17

1153 | 0.000 | 1.55

2305 | 0.000 | 2.25

4609 | 0.000 | 2.18

9217 | 0.001 | 2.80

18433 | 0.002 | 1.95

36865 | 0.003 | 2.08

73729 | 0.010 | 2.92

147457 | 0.046 | 4.69

294913 | 0.085 | 1.85

589825 | 0.264 | 3.12

1179649 | 0.775 | 2.93

2359297 | 2.375 | 3.06

4718593 | 6.834 | 2.88

Der Faktor, um den sich die Rechenzeitverlangert, ist etwa 3.

Also hat n*n die RechenzeitfunktionT p`q “ ` 1.58.

4.1.27

Page 29: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Polynomialzeittest fur Funktionen mit einem Array als

Argument

Statt einer Zahl, die von Messung zu Messung quadriert wird,

erzeugt man ein Array, das von Messung zu Messung verdoppelt wird.

Wenn das Array Lange n haben soll, dann erzeugen wir zuerst das Array

[0, 1, 2, 3, ..., n].

Das ist der Ruckgabewert der Python-Funktion range(n).

Anschließend mischen wir das Array mit random.shuffle(array).

Wir machen das,

da wir die Rechenzeit von Sortierprogrammen messen wollen.

Auf bereits sortierten Arrays konnen Sortierprogramme ungewohnlich

schnell sein.

4.1.28

Page 30: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

# polyTest-array.py

#-------------------------------------------------------------------------------------------------

import sys, time, random

# Lies die Argumente von der Kommandozeile.

modulname = sys.argv[1]

funktionsname = sys.argv[2]

n = int(sys.argv[3])

versuchsdauer = int(sys.argv[4])

# Importiere das Modul mit der Funktion, die fur das Experiment benutzt wird.

exec('from ' + modulname + ' import ' + funktionsname)

versuchsEnde = time.time() + versuchsdauer # die Zeit, die Messreihe zu beenden

messEnde = 0 # messEnde ist die Zeit der Beendigung der letzten Messung

letzteMessung = 0 # letzteMessung ist die zuletzt gemessene Rechenzeit.

# Fuhre die Messreihe mit wachsender Sample-Große n durch, bis die Zeit abgelaufen ist.

while messEnde < versuchsEnde:

# Erzeuge das Argument-Sample fur den Funktionsaufruf der Messung.

sample = range(n)

random.shuffle(sample)

# Fuhre die Messung durch: rufe die Funktion auf und miss die verbrauchte Rechenzeit.

messAnfang = time.time()

exec(funktionsname + '(sample)')

messEnde = time.time()

neueMessung = messEnde - messAnfang

# Gib das Ergebnis der Messung aus.

if letzteMessung>0: print('Zeit %6.3f, Faktor %2.2f' % (neueMessung, neueMessung/letzteMessung))

# Bereite die nachste Messung vor.

letzteMessung = neueMessung

n = 2*n4.1.29

Page 31: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

# arrayfunktionen.py

#----------------------------------------------------------------------

# Funktionen zum Suchen und Sortieren mit Arrays.

#----------------------------------------------------------------------

#----------------------------------------------------------------------

# minIndexSuche(liste, start=0) gibt den Index des kleinsten Eintrags

# im Array liste[start:] zuruck.

def minIndexSuche(liste, start=0):

minIndex = start

for i in range(start+1,len(liste)):

if liste[i] < liste[minIndex]: minIndex = i

return minIndex

#----------------------------------------------------------------------

# minsort(liste) sortiert liste (Seiteneffekt!) mittels wiederholter

# Minimumssuche.

def minsort(liste):

for i in range(len(liste)-1):

# Tausche den kleinsten Eintrag in liste[i:] an Stelle i.

indexOfMin = minIndexSuche(liste, i)

liste[i], liste[indexOfMin] = liste[indexOfMin], liste[i]

#---------------------------------------------------------------------

# pythonsSorted(liste) ruft Python's Funktion sorted() auf.

def pythonsSorted(liste):

return sorted(liste)

4.1.30

Page 32: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

#----------------------------------------------------------------------

# binSuche(liste, ziel) sucht mittels binarer Suche nach ziel im

# (sortierten) Array liste.

# Falls ziel gefunden wird, dann wird der Index von ziel zuruckgegeben.

# Falls ziel nicht gefunden wird, dann wird -1 zuruckgegeben.

# (Der Default-Wert fur ziel wird gebraucht, damit polyTest-array.py

# benutzt werden kann.)

def binSuche(liste, ziel=123):

links = 0

rechts = len(liste)-1

mitte = (links+rechts)//2

while not liste[mitte]==ziel and links<rechts:

if liste[mitte]>ziel: rechts = mitte-1

else: links = mitte+1

mitte = (links+rechts)//2

if liste[mitte]==ziel: return mitte

else: return -1

4.1.31

Page 33: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

python polyTest-array.py arrayfunktionen minIndexSuche 100 40

Arg.lange | Rechenzeit | Faktor

--------------------------------------

200 | 0.000 | 0.56

400 | 0.000 | 1.38

800 | 0.000 | 1.98

1600 | 0.000 | 1.32

3200 | 0.000 | 1.87

6400 | 0.001 | 1.97

12800 | 0.002 | 2.01

25600 | 0.004 | 2.23

51200 | 0.007 | 1.83

102400 | 0.027 | 3.78

204800 | 0.049 | 1.81

409600 | 0.076 | 1.56

819200 | 0.174 | 2.30

1638400 | 0.423 | 2.43

3276800 | 1.007 | 2.38

6553600 | 2.002 | 1.99

13107200 | 4.334 | 2.17

26214400 | 9.595 | 2.21

Der Faktor, um den sich die Rechenzeitverlangert, ist etwa 2.

Also hat minIndexSuche dieRechenzeitfunktion T p`q “ `.

4.1.32

Page 34: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

# python polyTest-array.py arrayfunktionen binSuche 100000 100

Arg.lange | Rechenzeit | Faktor

--------------------------------------

200000 | 0.000 | 0.63

400000 | 0.000 | 0.91

800000 | 0.000 | 0.99

1600000 | 0.000 | 0.88

3200000 | 0.000 | 1.04

6400000 | 0.000 | 1.06

12800000 | 0.000 | 0.93

25600000 | 0.000 | 1.22

51200000 | 0.000 | 0.87

102400000 | 0.000 | 1.07

Der Faktor, um den sich die Rechenzeitverlangert, ist etwa 1.

Also hat binSuche eineRechenzeitfunktion, die kleiner alsT p`q “ ` ist!

4.1.33

Page 35: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

python polyTest-array.py arrayfunktionen minsort 100 100

Arg.lange | Rechenzeit | Faktor

--------------------------------------

200 | 0.002 | 3.28

400 | 0.007 | 3.74

800 | 0.025 | 3.52

1600 | 0.079 | 3.15

3200 | 0.219 | 2.77

6400 | 1.038 | 4.74

12800 | 4.054 | 3.91

25600 | 15.986 | 3.94

51200 | 64.858 | 4.06

102400 | 274.798 | 4.24

Der Faktor, um den sich die Rechenzeitverlangert, ist etwa 4.

Also hat minsort dieRechenzeitfunktion T p`q “ `2.

4.1.34

Page 36: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Python hat auch die eingebaute Sortierfunktion sorted().Wir haben sie als pythonsSorted() in arrayfunktionen.py eingebaut.

# python polyTest-array.py arrayfunktionen pythonsSorted 100 100

Arg.lange | Rechenzeit | Faktor

--------------------------------------

200 | 0.000 | 0.77

400 | 0.000 | 1.54

800 | 0.000 | 1.70

1600 | 0.000 | 1.98

3200 | 0.001 | 2.22

6400 | 0.002 | 2.02

12800 | 0.003 | 1.69

25600 | 0.007 | 2.16

51200 | 0.014 | 2.06

102400 | 0.027 | 1.86

204800 | 0.064 | 2.43

409600 | 0.295 | 4.58

819200 | 0.508 | 1.72

1638400 | 1.161 | 2.28

3276800 | 2.686 | 2.31

6553600 | 6.514 | 2.42

13107200 | 15.389 | 2.36

26214400 | 35.654 | 2.32

52428800 | 82.343 | 2.31

Der Faktor, um den sich die Rechenzeitverlangert, ist etwa 2.3.

Damit fallt sorted aus dem bisherigenRahmen.

sorted hat die RechenzeitfunktionT p`q “ ` ¨ log `.

4.1.35

Page 37: Algorithmische Grundlagen Einführung in das Programmieren ... · Zum Messen der Rechenzeit benutzen wir die Funktion time() aus dem Modul time.py, die die Zeit seit dem 1.1.1970

Zusammenfassung

Das Wachstum der Rechenzeit von Programmen gibt verlassliche Auskunftuber die

”Schnelligkeit“ des Programms.

Wir haben gesehen, wie man es experimentell abschatzen kann.Es gibt auch ein mathematisches Modell,

mit dem man (im Prinzip) die gleichen Ergebnisse erzielt.

Die folgende Tabelle enthalt typische Rechenzeitfunktionen mit dem Namenihres Wachstums, und den Zuwachs an Rechenzeit eines Programms, das beiEingabegroße m wenige Sekunden braucht, bei einer Eingabe der Große100 ¨m.

Wachstum der Verlangerung der

Rechenzeitfunktion Beispiel Rechenzeit (s.o.)

linear T p`q “ ` wenige Minuten

quasi-linear T p`q “ ` ¨ log ` wenige Minuten

quadratisch T p`q “ ` 2 einige Stunden

kubisch T p`q “ ` 3 ein paar Wochen

exponentiell T p`q “ 2` ewig4.1.36