Die Natur rekursiver Funktionen

Werbung
Funktionale Programmierung
ALP I
Die Natur rekursiver Funktionen
SS 2011
Prof. Dr. Margarita Esponda
Prof. Dr. Margarita Esponda
Funktionale Programmierung
Die Natur rekursiver Funktionen
Rekursive Funktionen haben oft folgende allgemeine Form:
f :: a -> a
f 0 =c
f (n+1) = h (f n )
Diese Art der Definitionen wird oft als Strukturelle
Rekursion über die natürlichen Zahlen bezeichnet.
Prof. Dr. Margarita Esponda
Funktionale Programmierung
Die Natur rekursiver Funktionen
Eine Funktionsdefinition dieser Form über die natürlichen
Zahlen sieht aus wie folgt:
Sei
1+1+1+…+1+0 = die natürliche Zahl n.
Wenn wir die 0 mit c und (1+) mit h ersetzen, bekommen wir
folgenden Ausdruck
h(h(…(h(c)…),
in dem h n-mal auf c = f(0) angewendet wird.
Prof. Dr. Margarita Esponda
Funktionale Programmierung
Die Natur rekursiver Funktionen
Folgende Faltungsfunktion stellt eine Verallgemeinerung
der Funktionen mit dieser einfachen Grundform dar:
data Natural = Zero | S Natural
deriving (Eq, Show)
fold :: (a->a) -> a -> Natural -> a
fold h c Zero = c
fold h c (S n) = h (fold h c n)
Prof. Dr. Margarita Esponda
Funktionale Programmierung
Rekursionsarten
Lineare Rekursion
Rekursive Funktionen, die in jedem Zweig ihrer Definition maximal
einen rekursiven Aufruf beinhalten, werden als linear rekursiv
bezeichnet.
Beispiel:
f 0
= 1
f n | n<0
= 2 * (f (n+1))
| otherwise = 3 * (f (n-1))
Prof. Dr. Margarita Esponda
Funktionale Programmierung
Rekursionsarten
Endrekursion (tail recursion)
Linear rekursive Funktionen werden als endrekursive
Funktionen klassifiziert, wenn der rekursive Aufruf in jedem
Zweig der Definition die letzte Aktion zur Berechnung der
Funktion ist.
Das bedeutet, dass keine weiteren Operationen nach der
Auswertung der Rekursion berechnet werden müssen .
Prof. Dr. Margarita Esponda
Endrekursion
Beispiel: Eine nicht endrekursive Funktion ist folgende Definition der
Fakultätsfunktion:
factorial 0 = 1
factorial n = n * factorial (n-1)
Ablauf einer Berechnung:
factorial 6 =>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
Prof. Dr. Margarita Esponda
Der Ausführungsstapel wächst
bei jedem rekursiven Aufruf und
Teilausdrücke müssen ständig
zwischengespeichert werden.
6 * factorial 5
6 * (5 * factorial 4)
6 * (5 * (4 * factorial 3))
6 * (5 * (4 * (3 * factorial 2)))
6 * (5 * (4 * (3 * (2 * factorial 1))))
6 * (5 * (4 * (3 * (2 * (1 * factorial 0)))))
6 * (5 * (4 * (3 * (2 * (1 * 1)))))
6 * (5 * (4 * (3 * (2 * 1))))
6 * (5 * (4 * (3 * 2)))
6 * (5 * (4 * 6))
Die Endberechnungen finden erst
6 * (5 * 24)
beim Abbau des Ausführungsstapels
6 * 120
statt.
720
Endrekursion
Beispiel einer endrekursiven Definition der Fakultätsfunktion
quickFactorial n = factorial_helper 1 n
where
factorial_helper a 0 = a
factorial_helper a n = factorial_helper (a*n) (n-1)
Ablauf einer Berechnung:
quickFactorial 6
=>
=>
=>
=>
=>
=>
=>
=>
factorial_helper
factorial_helper
factorial_helper
factorial_helper
factorial_helper
factorial_helper
factorial_helper
720
1 6
6 5
30 4
120 3
360 2
720 1
720 0
Es müssen keine
Zwischenausdrücke
gespeichert werden.
Endrekursive Funktionen
können aus diesem Grund oft
vom Übersetzer (Compiler)
optimiert werden, indem diese
in einfache Schleifen
verwandelt werden.
Funktionale Programmierung
Beispiele endrekursiver Funktionen
Klassisches Beispiel einer nicht endrekursiven Definition ist:
Die Standarddefinition der reverse-Funktion
rev :: [a] -> [a]
rev [] = []
rev (x:xs) = rev xs ++ [x]
Berechnungsaufwand von rev:
Reduktionen
rev [x1, x2, …, xn] => rev [x2, …, xn] ++ [x1]
=> rev [x3, …, xn] ++ [x2] ++ [x1]
1
1
...
=> [xn] ++ [xn-1] ++ [x2] ++ [x1]
1
=> [] ++ [xn] ++ … ++ [x2] ++ [x1]
1
bis hier (n+1) Reduktionen!
Prof. Dr. Margarita Esponda
Funktionale Programmierung
Berechnungsaufwand von rev
bis hier (n+1) Reduktionen!
(++) :: [a] -> [a] -> [a]
(++) [] ys
= ys
(++) (x:xs) ys = x:(xs ++ ys)
Reduktionen
=> [] ++ [xn] ++ [xn-1] ++ … ++ [x2] ++ [x1]
=> [xn] ++ [xn-1] ++ … ++ [x2] ++ [x1]
1
=> [xn, xn-1] ++ … ++ [x2] ++ [x1]
2
=> [xn, xn-1 ,xn-2] ++ … ++ [x2] ++ [x1]
3
=> . . .
.
=> [xn, xn-1 , … ,x1]
n
Die gesamte Anzahl der Reduktionen ist:
Prof. Dr. Margarita Esponda
Quadratischer
Ausführungsaufwand!
Funktionale Programmierung
Eine effizientere Version von rev
quickRev xs = rev_helper xs []
where
rev_helper [] ys = ys
rev_helper (x:xs) ys = rev_helper xs (x:ys)
Berechnungsaufwand:
Reduktionen
quickRev [x1, x2, …, xn] => rev_helper [x1,…,xn] []
1
=> rev_helper [x2,…,xn] (x1:[])
1
n
=> rev_helper [x3,…,xn] (x2:x1:[]) 1
...
=> (xn:, … ,x2:x1:[])
…
1
=> (xn:, … ,x2:[x1])
1
...
=> (xn:, … , x3:[x2,x1])
…
1
lineare Komplexität
Prof. Dr. Margarita Esponda
2n = O(n)
n
Funktionale Programmierung
Wichtiges Beispiel von Endrekursion
foldl-Funktion:
foldl :: (b -> a -> b) -> b -> [a] -> b
foldl f z []
= z
foldl.1
foldl f z (x:xs) = foldl f (f z x) xs
foldl.2
Hier werden Zwischenergebnisse
akkumuliert und weitergeleitet.
Mit Hilfe von Faltungs-Operatoren können sehr leicht
endrekursive Funktionen definiert werden.
Beispiel:
reverse_reloaded = foldl (flip (:)) []
flip
:: (a -> b -> c) -> b -> a -> c
flip f x y
Prof. Dr. Margarita Esponda
= fyx
Funktionale Programmierung
Berechnungsverlauf
reverse_reloaded [x1, x2, … , xn]
foldl.2
=> foldl (flip (:)) [] [x1, x2,…, xn]
=> foldl (flip (:)) ((flip (:)) [] x1) [x2,x3,…, xn]
=> foldl (flip (:)) ((:) x1 []) [x2,x3,…, xn]
=> foldl (flip (:)) (x1:[]) [x2,x3,…, xn]
=> foldl (flip (:)) [x1] [x2,x3,…, xn]
foldl.2
=> foldl (flip (:)) ((flip (:)) [x1] x2) [x3,…, xn]
=> foldl (flip (:)) ((:) x2 [x1]) [x3,…, xn]
=> foldl (flip (:)) (x2:[x1]) [x3,…, xn]
foldl.2
Prof. Dr. Margarita Esponda
=> foldl (flip (:)) [x2, x1] [x3,…, xn]
=> . . .
Funktionale Programmierung
Berechnung der Fibonacci-Zahlen
Wie viele Reduktionsschritte brauchen wir, um fib n zu berechnen?
fib 0
1
fib 1
+
1
1
=
+
fib 2
2
2
2
fib 3 fib 4
5
3
=
+
fib 5
fib 6
8
13
Reduktionen
3
3
3
=
+
5
5
3
=
+
8
5
=
13
Die Anzahl der Reduktionen für fib n ist gleich fib (n+1)
Die rekursive Berechnung der FibonacciZahlen hat eine exponentielle Komplexität
n
O( (1,618...) )
Prof. Dr. Margarita Esponda
Funktionale Programmierung
Berechnung der Fibonacci-Zahlen
2. Lösung
Endrekursive Funktion
quickFib funktioniert nur, wenn diese mit
den ersten zwei Fibonacci-Zahlen
gestartet wird.
fib' n = quickFib 0 1 n
where
quickFib a b 0 = a
Zähler
quickFib.1
quickFib a b n = quickFib b (a+b) (n-1) quickFib.2
Innerhalb jedes rekursiven Aufrufs wird eine neue Fibonacci-Zahl berechnet und
der Zähler verkleinert. Die neue Zahl und ihr Vorgänger werden beim nächsten
rekursiven Aufruf als Parameter weitergegeben.
Anzahl der Reduktionen
Für die Berechnung von quickFib n benötigen wir n Reduktionen,
d.h. wir haben nur einen linearen Aufwand O(n)
Prof. Dr. Margarita Esponda
Funktionale Programmierung
Eine effiziente tree2list-Funktion
Noch ein Beispiel für die Verbesserung der Effizienz einer
Funktion ist die Funktion tree2List.
data Tree = Nil | Leaf Int | Node Int Tree Tree
tree2list :: Tree -> [Int]
tree2list Nil
= []
tree2list (Leaf n)
= [n]
tree2list (Node n l r)
= tree2list l ++ [n] ++ tree2list r
Die Verwendung der (++)-Funktion verursacht wieder einen
quadratischen Berechnungsaufwand.
Prof. Dr. Margarita Esponda
Funktionale Programmierung
Eine effiziente tree2list-Funktion
Hier benutzen wir die gleiche Technik wie bei der rev-Funktion,
indem wir eine Art Akkumulator für Zwischenergebnisse einbauen:
data Tree = Nil | Leaf Int | Node Int Tree Tree
tree2list' :: Tree -> [Int]
tree2list' t = tree2liste' t []
tree2liste' :: Tree -> [Int] -> [Int]
tree2liste' Nil ns
= ns
tree2liste' (Leaf n) ns = n:ns
tree2liste' (Node n l r) ns = (tree2liste' l (n:(tree2liste' r ns)))
Prof. Dr. Margarita Esponda
Herunterladen