Algorithmen und Programmieren 1 Funktionale Programmierung - Musterlösung zu Übung 8 - Dozent: Prof. Dr. G. Rote Tutoren: J. Fleischer, T. Haimberger, N. Lehmann, C. Pockrandt, A. Steen 03.01.2012 Ziele dieser Übung Mit Ein- und Ausgabe in Haskell sicher umgehen und das Konzept der primitiv-rekursiven Funktionen verstehen, sowie das Arbeiten mit Suchbäumen sicher beherrschen. In dieser Übung sollte man folgende Konzepte geübt und verstanden haben: Ein- und Ausgabe Bisektionsalgorithmus Fibonacci-Folge Arbeiten mit Suchbäumen Primitive Rekursion Aufgabe 48: Ein- und Ausgabe Schreiben Sie ein Programm für folgende Aufgabe: Der Benutzer wird nach dem Namen eine Eingabedatei und einer Ausgabedatei gefragt; dann wird die Eingabedatei auf die Ausgabedatei kopiert, wobei Folgen von Leerzeichen am Zeilenende weggelassen werden. Lösung zu Aufgabe 48 copy :: IO () copy = do putStr "Eingabedatei? " inFile <- getLine putStr "Ausgabedatei? " outFile <- getLine input <- readFile inFile writeFile outFile (format input) -- Entfernt Leerzeichen am Zeilenende. format :: String -> String format = unlines . (map prune) . lines where prune [] = [] prune xs | last xs /= ' ' = xs | otherwise = prune (init xs) 1 Aufgabe 49: Bäume dekodieren Schreiben Sie ein Programm für die Umkehrfunktion zu Aufgabe 41. Lösung zu Aufgabe 49 data Show a => Baum a = Blatt a | Knoten' a (Baum a) (Baum a) deriving Show -- Teile die Zeichenkette so auf, dass im linken Teil eine komplette Zahl steht. -- Bsp: "-15D3UD27U" -> value = "-15" und rest = "D3UD27U" -- Ist die zweite Zeichenkette leer, so handelt es sich um ein Blatt; -- sonst, teile die zweite Zeichenkette wieder auf, so dass der erste Teil -- dem linken Teilbaum und der zweite Teil dem rechten Teilbaum entspricht. -- Bsp: "D3UD27U" -> left = "D3U" und right = "D27U" -- Entferne das erste D und das letzte U von left und right, so dass wieder -- gültige Kodes entstehen. -- Dekodiere rekursiv den linken und den rechten Teilbaum. decode :: String -> Baum Int decode xs | rest == [] = Blatt (read value) | otherwise = Knoten' (read value) lTree rTree where (value,rest) = span (`notElem` ['U','D']) xs lTree = decode ((init.tail) left) rTree = decode ((init.tail) right) (left,right) = splitCode [] rest 0 -- Teilt die Zeichenkette so auf, dass der erste Teil dem linken Teilbaum -- und der zweite Teil dem rechten Teilbaum entspricht. Dabei wird ein -- Zähler benutzt, der anfangs 0 ist. Der Zähler wird um 1 erhöht, wenn -- wir eine Ebene tiefer gehen, und um 1 verringert, wenn wir eine Ebene -- höher gehen. Wir sind fertig, wenn der Zähler wieder 0 erreicht. splitCode :: String -> String -> Int -> (String,String) splitCode (l:ls) rs 0 = (l:ls,rs) splitCode ls (r:rs) n | r == 'D' = splitCode (ls ++ [r]) rs (n+1) | r == 'U' = splitCode (ls ++ [r]) rs (n-1) | otherwise = splitCode (ls ++ [r]) rs n encode :: Baum Int -> String encode (Blatt x) = show x encode (Knoten' x l r) = show x ++ "D" ++ encode l ++ "UD" ++ encode r ++ "U" bspCode :: String bspCode = "27D5D-2UD7UUD44D8UD5D7UD9UUU" bspBaum :: Baum Int bspBaum = Knoten' 27 (Knoten' 5 (Blatt (-2)) (Blatt 7)) (Knoten' 44 (Blatt 8) (Knoten' 5 (Blatt 7) (Blatt 9))) 2 Aufgabe 50: Intervallhalbierung, binäre Suche nullStelle:: (Floating a, Ord a) => (a -> a) -> a -> a -> a -> a zum Finden einer Näherungslösung x der Gleichung f (x) = 0 für eine stetige reelle Funktion f : [a, b] → R, die auf einem Intervall [a, b] deniert ist. Die Funktion nullStelle f a b epsilon setzt voraus, dass f (a) und f (b) verschiedenes Vorzeichen haben. Wegen der Stetigkeit von f folgt daraus, dass f im Intervall [a, b] mindestens eine Nullstelle hat. Das Verfahren der Intervallhalbierung bestimmt zunächst das Vorzeichen von f ((a + b)/2) und Schreiben Sie ein Programm grenzt dadurch das Intervall auf die linke oder die rechte Hälfte ein. Dieser Vorgang wird iteriert, bis die Länge des Lösungsintervalls unter ε reduziert ist. Finden Sie mit Ihrem Programm die Lösung von xx = 2, und von ex = sin x, x > −4, exp x x jeweils auf 6 Nachkommastellen genau. Die Exponentialfunktion e wird in Haskell als geschrieben. Lösung zu Aufgabe 50 -- Voraussetzungen: a < b, signum (f a) /= signum (f b), epsilon > 0 -- (In der dritten Zeile wurde die signum-Funktion benutzt, weil wir -- nach der gegebenen Signatur nicht voraussetzen können, dass der -- Typ t zur Typklasse Ord gehört.) nullStelle :: Floating t => (t -> t) -> t -> t -> t -> t nullStelle f a b epsilon | signum (b - a - epsilon) == -1 = c -- falls b - a < epsilon | f a == 0 = a | f b == 0 = b | signum (f c) == signum (f b) = nullStelle f a c epsilon | otherwise = nullStelle f c b epsilon where c = (a+b)/2 -f1 f1 n1 --f2 f2 n2 Intervall: [1,2] :: (Floating a) => a -> a x = x**x - 2 = nullStelle f1 1 2 1e-7 -- Lösung = 1.559610 ----------------------------Intervall: [-4,0] :: (Floating a) => a -> a x = exp x - sin x = nullStelle f2 (-4) 0 1e-7 -- Lösung = -3.183063 Aufgabe 51: Die Fibonacci-Folge Man kann die Fibonacci-Folge auch bei fibo:: fibo 0 fibo 1 fibo n F0 = 0 beginnen lassen: Integer -> Integer = 0 = 1 | n>1 = fibo (n-1) + fibo (n-2) Wie oft wird beim Aufruf von fibo n die letzte Zeile der Denition ausgeführt? Wie oft wird die erste Zeile ausgeführt? Wie oft die zweite Zeile? Beweisen Sie Ihre Aussagen durch Induktion nach n. 3 Lösung zu Aufgabe 51 n 0 1 2 3 4 5 6 7 8 9 10 11 12 # 1. Def. Zeile 1 0 1 1 2 3 5 8 13 21 34 55 89 # 2. Def. Zeile 0 1 1 2 3 5 8 13 21 34 55 89 144 # 3. Def. Zeile 0 0 1 2 4 7 12 20 33 54 88 143 232 T1 (0) = 1, T1 (1) = 0, ∀n ≥ 2 : T1 (n) = T1 (n − 1) + T1 (n − 2) T2 (0) = 0, T2 (1) = 1, ∀n ≥ 2 : T2 (n) = T2 (n − 1) + T2 (n − 2) T3 (0) = 0, T3 (1) = 0, ∀n ≥ 2 : T3 (n) = T3 (n − 1) + T3 (n − 2) + 1 Behauptung T1 (0) = 1 und ∀n ≥ 1 : T1 (n) = Fn−1 II. T2 (n) = Fn III. T3 (n) = Fn+1 − 1 I. Es folgt ein Beweis mit vollständiger Induktion über n. Induktionsbasis T1 (0) = 1, T1 (1) = 0 = F0 = F1−1 und T1 (2) = 1 = F1 = F2−1 T2 (0) = 0 = F0 und T2 (1) = 1 = F1 III. T3 (0) = 0 = 1 − 1 = F1 − 1 = F0+1 − 1 und T3 (1) = 0 = 1 − 1 = F2 − 1 = F1+1 − 1 I. II. Induktionsannahme Für ein beliebiges, aber festes n ∈ N gilt: T1 (n) = Fn−1 und T1 (n − 1) = F(n−1)−1 = Fn−2 II. T2 (n) = Fn und T2 (n − 1) = Fn−1 III. T3 (n) = Fn+1 − 1 und T3 (n − 1) = Fn − 1 = F(n−1)+1 − 1 I. Induktionsbehauptung Dann gilt auch: T1 (n + 1) = Fn = F(n+1)−1 II. T2 (n + 1) = Fn+1 III. T3 (n + 1) = Fn+2 − 1 = F(n+1)+1 − 1 I. Induktionsschritt n −→ n + 1 I. IA T1 (n + 1) = T1 (n) + T1 (n − 1) = Fn−1 + Fn−2 = Fn = F(n+1)−1 II. IA T2 (n + 1) = T2 (n) + T2 (n − 1) = Fn + Fn−1 = Fn+1 III. T3 (n + 1) = T3 (n) + T3 (n − 1) + 1 IA = Fn+1 − 1 + Fn − 1 + 1 = Fn+1 + Fn − 1 = Fn+2 − 1 = F(n+1)+1 − 1 Es wurde mit vollständiger Induktion gezeigt, dass die Behauptung gilt. 4 Aufgabe 52: Suchen und Einfügen in einem Suchbaum data Ord a => Suchbaum a b = Leer | Knoten a b (Suchbaum a b) (Suchbaum a b) 1. Schreiben Sie eine Funktion finde:: Ord a => a -> Suchbaum a b -> Maybe b zum Suchen eines Elements in einem Suchbaum (Vorlesung vom 28. 11.) einf:: Ord a => a -> b -> Suchbaum a b -> Suchbaum a b zum Einfügen eines Elements in einen Suchbaum. Das Ergebnis von einf s t b ist ein neuer Suchbaum, in dem ein zusätzliches Element mit Schlüssel s vom Typ a und Wert t vom Typ b enthalten ist. Sie dürfen annehmen, dass der Schlüssel s vorher nicht 2. Programmieren Sie eine Funktion im Baum vorkommt. Lösung zu Aufgabe 52 data (Ord a) => Suchbaum a b = Leer | Knoten a b (Suchbaum a b) (Suchbaum a b) -- a ist der Typ der Schlüssel -- b ist der Typ der Nutzdaten -- Achtung: Die Signaturen aus der Aufgabenstellung wurden geändert, -- damit wir die Schlüssel vergleichen können. finde :: (Ord a) => a -> Suchbaum a b -> Maybe b finde _ Leer = Nothing finde x (Knoten a b links rechts) | x == a = Just b | x < a = finde x links -- links weitersuchen | x > a = finde x rechts -- rechts weitersuchen einf :: (Ord a) => a -> b -> Suchbaum a b -> Suchbaum a b einf s t Leer = Knoten s t Leer Leer einf s t (Knoten a b links rechts) | s < a = Knoten a b (einf s t links) rechts -- links einfügen | s > a = Knoten a b links (einf s t rechts) -- rechts einfügen bspSuchbaum = Knoten 10 "xy" (Knoten 4 "ab" Leer Leer) Leer Aufgabe 53: Bedingte Ausdrücke in primitiv-rekursiven Funktionen f, g, h : Nk → N drei k -stellige primitiv-rekursive folgende Funktion a eine primitiv-rekursive Funktion: Beweisen Sie: Wenn ist auch die Funktionen sind, dann a(x1 , . . . , xk ) := if f (x1 , . . . , xk ) = 0 then g(x1 , . . . , xk ) else h(x1 , . . . , xk ) Beweisen Sie dieselbe Schlussfolgerung auch für Bedingungen der Form f1 (x1 , . . . , xk ) < f2 (x1 , . . . , xk ), f1 (x1 , . . . , xk ) ≤ f2 (x1 , . . . , xk ) und f1 (x1 , . . . , xk ) = f2 (x1 , . . . , xk ), falls f1 und f2 primitiv-rekursiv sind. 5 Lösung zu Aufgabe 53 Sei ifelse(b, x, y) := if b = 0 then x else y . Dann ist ifelse nach dem Rekursionssche- ma eine primitiv-rekursive Funktion: ifelse(0, x, y) = P12 (x, y) = x (siehe Aufgabe 55) ifelse(n, x, y) = P44 (ifelse(n − 1), n, x, y) = y , Nach der Einsetzungsregel ist auch a für n>0 primitiv-rekursiv: a(x1 , ..., xk ) = ifelse(f (x1 , ..., xk ), g(x1 , ..., xk ), h(x1 , ..., xk )) minus und plus primitiv-rekursive Funktionen sind. (Zur minus(a, b) = 0; sonst ist minus(a, b) = a − b > 0.) Nach der In der Vorlesung wurde gezeigt, dass Erinnerung: Ist a ≤ b, so ist Einsetzungsregel sind also die folgenden Funktionen primitiv-rekursiv: f< (x1 , ..., xk ) := minus(nachf(f1 (x1 , ..., xk )), f2 (x1 , ..., xk )) f≤ (x1 , ..., xk ) := minus(f1 (x1 , ..., xk ), f2 (x1 , ..., xk )) f≥ (x1 , ..., xk ) := minus(f2 (x1 , ..., xk ), f1 (x1 , ..., xk )) f= (x1 , ..., xk ) := plus(f≤ (x1 , ..., xk ), f≥ (x1 , ..., xk )) Die obigen Funktionen liefern genau dann 0, wenn die jeweilige Bedingung erfüllt ist. Es gilt also z.B. f≤ (x1 , ..., xk ) = 0 ⇐⇒ f1 (x1 , ..., xk ) ≤ f2 (x1 , ..., xk ). Wir können nun wieder durch Einsetzen die gewünschten Funktionen denieren: a< (x1 , ..., xk ) := ifelse(f< (x1 , ..., xk ), g(x1 , ..., xk ), h(x1 , ..., xk )) a≤ (x1 , ..., xk ) := ifelse(f≤ (x1 , ..., xk ), g(x1 , ..., xk ), h(x1 , ..., xk )) a= (x1 , ..., xk ) := ifelse(f= (x1 , ..., xk ), g(x1 , ..., xk ), h(x1 , ..., xk )) Aufgabe 54: Gröÿte enthaltene Zweierpotenz Zeigen Sie, dass die Funktion p2 : N → N p2 (n) := max{ i ≥ 0 | 2i mit ist ein Teiler von n} für n > 0, p2 (0) := 0 primitiv-rekursiv ist. Tipp: Konstruieren Sie zuerst eine Hilfsfunktion h(m, n) := max{ i ≥ 0 | 2i ≤ m + 1 und 2i ist ein Teiler von n} Sie dürfen unter anderem voraussetzen, dass die modulo-Funktion und die Potenzfunktion primitiv-rekursiv ist. Lösung zu Aufgabe 54 Wir setzen voraus, dass die modulo-Funktion modulo und die Potenzfunktion power primitivg primitiv-rekursiv (nach Einsetzungsregel). rekursiv sind. Dann ist auch die folgende Funktion g(x, y, z) : = if modulo(z, 2y+1 ) = 0 then y + 1 else x = ifelse(modulo(z, power(2, nachf(y))), nachf(y), x) Wir denieren eine Hilfsfunktion h0 fast genau so wie in der Aufgabenstellung vorgeschlagen: h0 (m, n) := max{ i ≥ 0 | i ≤ m 6 und 2i ist ein Teiler von n} Die Funktion h0 ist nach dem Rekursionsschema primitiv-rekursiv. h0 (0, n) = 0 h0 (m, n) = g(h0 (m − 1, n), m − 1, n) = if modulo(n, 2m ) = 0 then m else h0 (m − 1, n), Nach der Einsetzungsregel ergibt sich, dass p2 für m>0 primitiv-rekursiv ist: p2 (n) = h0 (n, n) Für n>0 ergibt sich nämlich h0 (n, n) = max{ i ≥ 0 | i ≤ n = max{ i ≥ 0 | 2i weil 2i > i ist, und für n=0 ist und 2i ist ein Teiler von ist ein Teiler von n} n }, p2 (0) = h0 (0, 0) = 0. Aufgabe 55: Einsetzung Klassischerweise wird die Einsetzungsregel bei der Denition der primitiv-rekursiven Funktionen strenger formuliert: Wenn eine k -stellige primitiv-rekursive Funktionen die n-stellige f : Nk → N und k n-stellige g1 , . . . , gk : Nk → N gegeben sind, dann ist auch primitiv-rekursive Funktion Funktion h(x1 , . . . , xn ) := f g1 (x1 , . . . , xn ), g2 (x1 , . . . , xn ), . . . gk (x1 , . . . , xn ) primitiv-rekursiv. Zusätzlich wird dabei deniert, dass alle Projektionsfunktionen für 1≤i≤k Pik (x1 , . . . , xk ) := xi primitiv-rekursiv sind. Zeigen Sie mit dieser Einsetzungsregel und mit Hilfe geeigneter Projektionsfunktionen, dass die Funktionen p(x, y, z) := h(x, h(y, z)) q(x, y, z) := g(h(x, y), y, g(z, z, y)) primitiv-rekursiv sind, falls die Funktionen g und h als primitiv-rekursiv vorausgesetzt werden. Lösung zu Aufgabe 55 Wir denieren zuerst drei primitiv-rekursive Hilfsfunktionen: h1 (x, y, z) := h(P23 (x, y, z), P33 (x, y, z)) = h(x, z) h2 (x, y, z) := h(P13 (x, y, z), P23 (x, y, z)) = h(x, y) g1 (x, y, z) := g(P33 (x, y, z), P33 (x, y, z), P23 (x, y, z)) = g(z, z, y) Dann setzen wir die Hilfsfunktionen und geeignete Projektionsfunktionen ein: p(x, y, z) = h(P13 (x, y, z), h1 (x, y, z)) q(x, y, z) = g(h2 (x, y, z), P23 (x, y, z), g1 (x, y, z)) 7 Aufgabe 56: Suchbäume Schreiben Sie eine Funktion, die überprüft, ob ein Suchbaum richtig sortiert ist. (Das heiÿt, ob die Suchbaumeigenschaft erfüllt ist: Für jeden Knoten mit Schlüssel die Knoten im linken Teilbaum kleinere Schlüssel als s s müssen haben und die Knoten im rechten Teilbaum gröÿere Schlüssel.) Lösung zu Aufgabe 56 -- Alle Knoten im linken Teilbaum sollen kleinere Schlüssel als s -- haben und alle Knoten im rechten Teilbaum gröÿere Schlüssel. -- Um diese Eigenschaft zu überprüfen, können wir einfach den -- rechtesten Wert des linken Teilbaums und den linkesten Wert des -- rechten Teilbaums mit s vergleichen, solange wir auch noch überprüfen, -- ob die Teilbaume selbst richtig sortiert sind. check :: (Ord a) => Suchbaum a b -> Bool check Leer = True check (Knoten s _ links rechts) = checkMax links && checkMin rechts && check links && check rechts where -- Ist der rechteste Wert im linken Teilbaum kleiner als a? checkMax Leer = True checkMax (Knoten x _ _ Leer) = x <= s checkMax (Knoten _ _ _ rechts) = checkMax rechts -- Ist der linkeste Wert im rechten Teilbaum gröÿer als a? checkMin Leer = True checkMin (Knoten x _ Leer _) = x >= s checkMin (Knoten _ _ links _) = checkMin links bspRichtig = Knoten 10 "xy" (Knoten 4 "ab" Leer (Knoten 6 "richtig" Leer Leer)) Leer bspFalsch = Knoten 10 "xy" (Knoten 4 "ab" Leer (Knoten 12 "fehler" Leer Leer)) Leer Aufgabe 57: Verschmelzen Scheiben Sie eine Funktion verschmelze:: (Ord a) => [a] -> [a] -> [a], die zwei auf- steigend sortierte Listen zu einer sortierten Gesamtliste vereinigt. Zum Beispiel: verschmelze [1,3,4,7] [2,3,7,8,10] = [1,2,3,3,4,7,7,8,10] Lösung zu Aufgabe 57 verschmelze verschmelze verschmelze verschmelze :: (Ord a) => [a] -> [a] -> [a] [] ys = ys xs [] = xs (x:xs) (y:ys) -- "Reiÿverschluss" | x <= y = x : verschmelze xs (y:ys) | otherwise = y : verschmelze (x:xs) ys 8