3.6.4 Rekursive Funktionen
3.6.4 Rekursive Funktionen
3.6.4 Rekursive Funktionen
Erfolgreiche ePaper selbst erstellen
Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Beispiel: Keine Einführung in die Informatik ohne die Türme von Hanoi.<br />
Das Spiel wurde 1883 von dem französischen Mathematiker Edouard<br />
Lucas erfunden.<br />
Die Scheiben auf Stab A (links) sollen nach Stab C (rechts) gebracht<br />
werden, ohne dass jemals eine größere Scheibe auf einer kleineren<br />
liegt. Es darf immer nur die oberste Scheibe eines Stapels bewegt<br />
werden; ablegen neben den Stäben ist verboten.<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
148
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Wenn das Universum sein Ende finden wird …<br />
Legende über ein buddhistisches Kloster bei Hanoi, in dem sich ein<br />
riesiger Raum mit drei abgenutzten Pfosten befindet, die von 64<br />
goldenen Scheiben umgeben waren<br />
� Seit der Gründung des Klosters vor über tausend Jahren führen<br />
Mönche die Anordnung einer alten Prophezeiung aus:<br />
• Sie bewegen die Scheiben, in Übereinstimmung mit den Regeln<br />
des Puzzles<br />
• Jeden Tag eine Scheibe<br />
� Sie glauben, wenn die letzte Bewegung ausgeführt würde, wird die<br />
Welt mit einem Donnerschlag untergehen<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
149
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Algorithmus in rekursiver Formulierung:<br />
n Scheiben von A unter Zuhilfenahme des Stabes<br />
B nach C bringen<br />
� n − 1 Scheiben von A unter Zuhilfenahme des<br />
Stabes C nach B bringen<br />
� Bewege eine Scheibe von A nach C<br />
� n − 1 Scheiben von B unter Zuhilfenahme des<br />
Stabes A nach C bringen<br />
(define(move T1 T2 T3 n)<br />
(cond [(= n 0) empty]<br />
[else (append<br />
(move T1 T3 T2 (- n 1))<br />
(list (list T1 T2))<br />
(move T3 T2 T1 (- n 1)))]))<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
> (move 'A 'B 'C 4)<br />
(list<br />
(list 'A 'C)<br />
(list 'A 'B)<br />
(list 'C 'B)<br />
(list 'A 'C)<br />
(list 'B 'A)<br />
(list 'B 'C)<br />
(list 'A 'C)<br />
(list 'A 'B)<br />
(list 'C 'B)<br />
(list 'C 'A)<br />
(list 'B 'A)<br />
(list 'C 'B)<br />
(list 'A 'C)<br />
(list 'A 'B)<br />
(list 'C 'B))<br />
150
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Spielzüge mit drei Scheiben:<br />
Spielzüge mit vier Scheiben:<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
Quelle: Wikipedia<br />
151
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Wenn t n die Rechenzeit für (move X Y Z n) bezeichnet, dann gilt:<br />
Die Anzahl der Bewegungen zur Lösung des Problems mit n Scheiben ist<br />
bei diesem Verfahren 2 n − 1. Man kann beweisen, dass das optimal ist.<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
152
Exkurs: Wachstum von Zweierpotenzen<br />
Eine praktische Umsetzung einer Lösung<br />
mit exponentieller Komplexität ist nur für<br />
kleine n möglich. Die nebenstehende<br />
Tabelle zeigt die Dauer unter der Annahme,<br />
dass eine Scheibe pro Sekunde<br />
verschoben wird.<br />
Zum Glück sind die Mönche nicht mal annäherungsweise fertig!<br />
� Sie bräuchten 264−1 Sekunden oder ungefähr 585 Milliarden Jahre<br />
� Unser Universum ist zur Zeit ungefähr 13,7 Milliarden Jahre alt<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
153
Exkurs: Wachstum von Zweierpotenzen<br />
Weizenkornlegende: Sissa (auch: Sessa) lebte<br />
angeblich im dritten oder vierten Jahrhundert<br />
n.Chr. in Indien und gilt Legenden zufolge als<br />
der Erfinder des Schachspiels bzw. seiner<br />
indischen Urform Tschaturanga.<br />
Der indische Herrscher Shihram tyrannisierte seine<br />
Untertanen und stürzte sein Land in Not und Elend.<br />
Um die Aufmerksamkeit des Königs, ohne seinen Zorn zu entfachen, auf seine<br />
Fehler zu lenken, schuf der weise Sissa ein Spiel, in dem die wichtigste Figur,<br />
der König, ohne Hilfe anderer Figuren und Bauern nichts ausrichten kann.<br />
Der Unterricht im Schachspiel hat auf Shihram einen starken Eindruck gemacht.<br />
Er wurde milder und ließ das Schachspiel verbreiten, damit alle davon Kenntnis<br />
nähmen. Um sich für die anschauliche Lehre von Lebensweisheit und zugleich<br />
Unterhaltung zu bedanken, gewährte er Sissa einen freien Wunsch. Dieser<br />
wünschte sich Weizenkörner: Auf das erste Feld eines Schachbretts wollte er ein<br />
Korn, auf das zweite Feld die doppelte Menge, also zwei, auf das dritte wiederum<br />
doppelt so viele, also vier und so weiter. Quelle: Wikipedia<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
154
Exkurs: Wachstum von Zweierpotenzen<br />
Der König lachte und war gleichzeitig erbost ob der vermeintlichen<br />
Bescheidenheit des Brahmanen. Als sich Shihram einige Tage später<br />
erkundigte, ob Sissa seine Belohnung in Empfang genommen habe,<br />
musste er hören, dass die Rechenmeister die Menge der Weizenkörner<br />
noch nicht berechnet hatten. Nach mehreren Tagen ununterbrochener<br />
Arbeit meldete der Vorsteher der Kornkammer, dass er die Menge<br />
Getreidekörner im ganzen Reich nicht aufbringen könne. Auf allen<br />
Feldern zusammen wären es 2 64 −1 oder 18.446.744.073.709.551.615<br />
Weizenkörner (eine Wagenkolonne mit dieser Menge würde<br />
231666mal um die Erde reichen).<br />
Nun stellte sich die Frage, wie das Versprechen eingelöst werden<br />
könne. Der Rechenmeister half dem Herrscher aus der Verlegenheit,<br />
indem er ihm empfahl, er solle Sissa ganz einfach das Getreide Korn<br />
für Korn zählen lassen.<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
155
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Linear rekursiver Prozess:<br />
(define (fak n) (if (= n 0) 1 (∗ n (fak (- n 1)))))<br />
Auswertung im Substitutionsmodell:<br />
(fak 4) →<br />
(∗ 4 (fak 3)) → Expansion<br />
(∗ 4 (∗ 3 (fak 2))) →<br />
(∗ 4 (∗ 3 (∗ 2 (fak 1)))) →<br />
(∗ 4 (∗ 3 (∗ 2 (∗ 1 (fak 0))))) →<br />
(∗ 4 (∗ 3 (∗ 2 (∗ 1 1)))) →<br />
(∗ 4 (∗ 3 (∗ 2 1))) →<br />
(∗ 4 (∗ 3 2)) → Kontraktion<br />
(∗ 4 6) →<br />
24<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
Interpreter muss über die<br />
später durchzuführenden<br />
Operationen Buch führen.<br />
Anzahl aufgeschobener<br />
Operationen wächst linear<br />
mit n � Zeit- und Speicherplatzbedarf<br />
wächst linear<br />
mit n<br />
� <strong>Rekursive</strong>r Prozess: Operationen werden aufgeschoben und erst nach<br />
Beenden der rekursiven Aufrufe durchgeführt<br />
� fak erzeugt einen linear rekursiven Prozess<br />
156
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Linear iterativer Prozess:<br />
Berechne n! durch 1 → 1 ∗ 1 → 2 ∗ 1 → 3 ∗ 2 → 4 ∗ 6 → 5 ∗ 24 → …<br />
d.h. verwalte ein Zwischenprodukt p, das mit 1 initialisiert wird, und<br />
einen Zähler c, der von 1 bis n zählt. p und c ändern sich in jedem Schritt<br />
simultan gemäß<br />
p ← c ∗ p c ← c + 1<br />
(define (fak n) (ifak 1 1 n))<br />
(define (ifak p c max)<br />
(if (> c max) p (ifak (∗ c p) (+ c 1) max)))<br />
Auswertung im Substitutionsmodell:<br />
(fak 5) →<br />
(ifak 1 1 5) → (ifak 1 2 5) → (ifak 2 3 5) →<br />
(ifak 6 4 5) → (ifak 24 5 5) → (ifak 120 6 5) → 120<br />
Variablen p, c und max beschreiben zu jedem Zeitpunkt den<br />
Berechnungsprozess vollständig<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
157
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Linear iterativer Prozess:<br />
� Zustand des Prozesses wird durch eine feste, von der Eingabe<br />
unabhängige Anzahl von Zustandsvariablen beschrieben<br />
• Fixe Regel beschreibt, wie die Zustandsvariablen aktualisiert<br />
werden, wenn der Prozess von einem Zustand in den nächsten<br />
Zustand übergeht<br />
• (Optionaler) Test, der die Bedingungen spezifiziert, unter denen<br />
der Prozess terminiert<br />
� Interpreter muss sich einzig die Zwischenwerte der Zustandsvariablen<br />
merken � Speicherplatzbedarf ist konstant<br />
� Zeitbedarf ist bestimmt durch Anzahl der rekursiven Aufrufe, die linear<br />
mit n wächst<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
158
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Beispiel: Fibonacci-Zahlen<br />
� Folgen lassen sich in vielen Fällen durch eine iterative Funktion<br />
beschreiben<br />
� Die Fibonacci-Folge kann man fortsetzen, wenn man jeweils die<br />
beiden letzten Elemente kennt:<br />
0 1 � 1 1 � 1 2 � 2 3 � 3 5 � 5 8 � 8 13 � ……<br />
(define (fib n) (fib-iter 0 1 n))<br />
(define (fib-iter a b n)<br />
(cond [(= n 0) a]<br />
[else (fib-iter b (+ a b) (- n 1))]))<br />
> (fib 10)<br />
55<br />
> (map fib (list 0 1 2 3 4 5 6 7 8 9 10))<br />
(list 0 1 1 2 3 5 8 13 21 34 55)<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
159
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Endrekursion:<br />
Erzeugt eine linear rekursive Funktion bei ihrer Auswertung einen<br />
iterativen Prozess, d.h. das Ergebnis eines rekursiven Aufrufs wird nicht<br />
im Rumpf der rekursiven Funktion weiterverarbeitet und mit dem<br />
Beenden des letzten rekursiven Aufrufs liegt auch das Ergebnis der<br />
Berechnung vor, so nennt man die Funktion endrekursiv bzw.<br />
endrekursion (tail recursion).<br />
� Weniger Platzbedarf bei der Abarbeitung, da die Größe des auszuwertenden<br />
Ausdrucks nicht mit jedem rekursiven Aufruf wächst<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
160
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Beispiel: Berechnung von<br />
(define (sum n)<br />
(if (= n 0) 0 (+ n (sum (- n 1)))))<br />
sum ist nicht endrekursiv<br />
(define (sum n) (isum n 0))<br />
(define (isum n result)<br />
(if (= n 0) result (isum (- n 1) (+ n result))))<br />
isum ist endrekursiv<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
n<br />
∑<br />
i =0<br />
i<br />
(isum 5 0) � (isum 4 5) � (isum 3 9) � (isum 2 12) � (isum 1 14) �<br />
(isum 0 15) � 15<br />
161
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Beispiel: Länge einer Liste (vgl. Folie 112, Kap.3)<br />
Endrekursion:<br />
(define (len list-of-anything)<br />
(cond [(empty? list-of-anything) 0]<br />
[else (+ 1 (len (rest list-of-anything)))]))<br />
(define (len list-of-anything) (len-iter list-of-anything 0))<br />
(define (len-iter l n)<br />
(cond [(empty? l) n]<br />
[else (len-iter (rest l) (+ n 1))]))<br />
(len-iter (list 1 2 3) 0) � (len-iter (list 2 3) 1) � (len-iter (list 3) 2) �<br />
(len-iter empty, 3) � 3<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
162
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Eine Funktion kann rekursiv in der syntaktischen Definition, aber iterativ<br />
in der effektiven Ausführung sein<br />
� Funktion ist rekursiv � syntaktische Eigenschaft<br />
� Prozess ist linear rekursiv oder linear iterativ � Aussage über die<br />
Entwicklung des Berechnungsprozesses, nicht aber über die Syntax<br />
der Funktion<br />
Für den Speicher- und Zeitbedarf ist das Prozessverhalten, nicht die<br />
syntaktische Definition, entscheidend<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
163
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Terminierung bei rekursiven <strong>Funktionen</strong>:<br />
� Terminierung bei nicht-rekursiven <strong>Funktionen</strong><br />
Komplexe <strong>Funktionen</strong> stützen sich auf einfachere <strong>Funktionen</strong> ab,<br />
dieser Abstützprozess endet nach endlich vielen Schritten bei den<br />
vordefinierten <strong>Funktionen</strong> von Scheme<br />
� Versuche, die Vorgehensweise auf die Kombination aus<br />
Funktionsaufrufen und Parametern zu übertragen. Gelingt es also,<br />
für eine Funktion nachzuweisen, dass die aufgerufenen <strong>Funktionen</strong><br />
und ihre Parameter als Ganzes in dem Sinne einfacher werden, dass<br />
sie nach endlich vielen Schritten die Abbruchbedingung erfüllen, so<br />
kann daraus die Terminierung der (rekursiven) Funktion gefolgert<br />
bzw. nachgewiesen werden.<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
164
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Beispiel:<br />
(define (fak n) (if (= n 0) 1 (∗ n (fak (- n 1)))))<br />
� fak ist linear rekursive Funktion mit einem Parameter; Definition<br />
stützt sich auf { ∗ , - , = } und auf fak selbst ab<br />
� Rekursion terminiert genau dann, wenn das Abbruchkriterium (= n 0)<br />
erfüllt ist<br />
� In jedem Aufruf von fak wird das Argument um 1 dekrementiert<br />
• Für Startwerte n∈ N ist (fak n) bzgl. des Abbruchkriteriums<br />
komplexer als (fak (- n 1)) � Funktion terminiert nach n<br />
rekursiven Aufrufen<br />
• Für Startwerte n
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
� Wichtig: kombinierte Betrachtung der Parameter der rekursiven<br />
Aufrufe, der Abbruchbedingung und der erlaubten Eingabewerte<br />
� Dabei sind nicht unbedingt alle Parameter relevant, sondern nur die,<br />
die in den Abbruchbedingungen berücksichtigt werden. Man sagt<br />
dann, dass bestimmte Parameter die Rekursion steuern.<br />
Beispiel:<br />
(define (fak n) (ifak 1 1 n))<br />
(define (ifak p c max)<br />
(if (> c max) p (ifak (∗ c p) (+ c 1) max)))<br />
Nur c und max steuern die Rekursion; p dient lediglich zur<br />
Speicherung des Zwischenprodukts<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
166
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Beispiel: Linear rekursive Funktion t mit zwei Parametern<br />
(define (t x y) (cond [(< y 0) x]<br />
[(
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Bei nicht-linearer Rekursion muss man das Terminieren aller rekursiven<br />
Aufrufe zeigen, d.h. dass jede der auftretenden Rekursionen eine<br />
Abbruchbedingung erreicht<br />
Beispiel:<br />
(define (fib n)<br />
(cond [(= n 0) 0]<br />
[(= n 1) 1]<br />
[else (+ (fib (- n 1)) (fib (- n 2)))]))<br />
Die baumrekursive Fibonacci-Funktion terminiert für alle n ∈ N 0, da<br />
beide auftretenden rekursiven Aufrufe in jedem Schritt zu um 1 oder 2<br />
kleineren Parameterwerten führen und die Abbruchbedingungen<br />
(= n 0) bzw. (= n 1) immer erreicht werden<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
168
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Vorgehen zum Sicherstellen von Terminierung:<br />
� Welche Abbruchbedingungen sind für welche Parameter relevant?<br />
� Wie werden die relevanten Parameter in jedem Rekursionsschritt<br />
verändert?<br />
� Auf Grundlage dieses Wissens muss man versuchen,<br />
sicherzustellen (nachzuweisen), dass<br />
• alle vorkommenden rekursiven Aufrufe<br />
• für mindestens einen relevanten Parameter<br />
• nach endlich vielen Schritten<br />
• eine der Abbruchbedingungen erreichen durch Zuordnen einer<br />
Reihenfolge von komplexen zu einfacheren Aufrufen<br />
Berücksichtige diese Überlegungen nicht erst am (scheinbar) fertigen<br />
Programm, sondern bei der Entwicklung des Programms.<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
169
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
Korrektheitsbeweis:<br />
Man kann für eine Funktion eine Zusicherung ihrer Eigenschaften<br />
geben. Zusicherungen für <strong>Funktionen</strong>, die mit Listen arbeiten, werden<br />
oft mit vollständiger Induktion über die Länge der beteiligten Listen<br />
bewiesen.<br />
Beispiel: Konkatenieren zweier Listen (vgl. Folie 114, Kap.3)<br />
(define (concat l1 l2)<br />
(cond [(empty? l1) l2]<br />
[else (cons (first l1) (concat (rest l1) l2))]))<br />
Zusicherung: Ist l1 = (x1 . . . xn) eine Liste der Länge n und l2 = (y1 . . .<br />
ym) eine Liste der Länge m, so ist der Wert von (concat l1 l2) die Liste<br />
(x1 . . . xn y1 . . . ym) der Länge n + m.<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
170
<strong>3.6.4</strong> <strong>Rekursive</strong> <strong>Funktionen</strong><br />
(define (concat l1 l2)<br />
(cond [(empty? l1) l2] ; Zeile 1<br />
[else (cons (first l1) (concat (rest l1) l2))])) ; Zeile 2<br />
Beweis mit Induktion über n (Länge der Liste l1):<br />
Induktionsanfang: Für n = 0 folgt die Behauptung aus Zeile 1<br />
Induktionsschritt: Ist n > 0 und die Zusicherung für Listen der Länge n − 1<br />
richtig, dann gilt sie für Listen der Länge n: Nach Induktionsvoraussetzung<br />
ist der Wert von (concat (rest l1) l2) die Liste (x2 . . . xn y1 . . . ym) der<br />
Länge n − 1 + m, daraus und aus Zeile 2 folgt, dass (cons (first l1) ...) und<br />
damit das Resultat die Liste (x1 . . . xn y1 . . . ym) der Länge n + m ist.<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
171
Exkurs: Beweis durch Induktion<br />
Wir wollen zeigen, dass eine Aussage P(n) für alle natürlichen Zahlen<br />
n ≥ k gilt<br />
Beweismethode der vollständigen Induktion:<br />
� Induktionsanfang (IA, auch Anker genannt): Zeige P(k)<br />
� Induktionsschritt (IS): Zeige, dass für alle natürlichen Zahlen n > k gilt:<br />
P(n) � P(n + 1)<br />
P(n) heisst auch Induktionsannahme oder Induktionsvoraussetzung (IV)<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
172
Exkurs: Beweis durch Induktion<br />
Der folgende Satz ist benannt nach Carl Friedrich Gauss (1777–1855),<br />
der ihn bereits während seiner Grundschulzeit bewiesen hat<br />
Theorem: Für alle natürlichen Zahlen n gilt: Die Summe der ersten n<br />
natürlichen Zahlen ist gleich n(n + 1)/2<br />
Beweis:<br />
Der Induktionsanfang mit P(1) = 1 ist klar.<br />
IV: P(n) stimmt, d.h.<br />
Nun zeigen wir, dass auch P(n + 1) stimmt:<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
173
Exkurs: Beweis durch Induktion<br />
Manchmal gelingt es nicht, aus der Gültigkeit der Eigenschaft P(n) auf<br />
die Gültigkeit von P(n + 1) zu schliessen. Hier hilft zuweilen das<br />
allgemeine Induktionsprinzip, bei dem man im Induktionsschritt zeigt:<br />
Wenn P(n′), k ≤ n′ ≤n, gilt, dann gilt auch P(n + 1)<br />
Beweismethode der allgemeinen Induktion:<br />
� Induktionsanfang (IA, auch Anker genannt): Zeige P(k)<br />
� Induktionsschritt (IS): Zeige, dass für alle natürlichen Zahlen n > k gilt:<br />
P(k) ∧ P(k + 1) ∧ ···∧ P(n) � P(n + 1)<br />
© Xiaoyi Jiang Informatik I – Grundlagen der Programmierung<br />
174