Fachrichtung 6.2 — Informatik Universität des Saarlandes Tutorenteam der Vorlesung Programmierung 1 Programmierung 1 (Wintersemester 2015/16) Lösungsblatt: Aufgaben für die Übungsgruppen: 1 (Kapitel 1) Hinweis: Dieses Übungsblatt enthält von den Tutoren für die Übungsgruppe erstellte Aufgaben. Die Aufgaben und die damit abgedeckten Themenbereiche sind für die Klausur weder relevant noch irrelevant. Aufgabe 1.1 Schreiben Sie eine Prozedur comp: (int*int)*bool → bool, die als Argument zwei Ganzzahlen und einen Wahrheitswert bekommt und überprüft, ob das Ergebnis des Vergleiches der beiden Zahlen mit dem übergebenen Wahrheitswert übereinstimmt. Schreiben Sie die Prozedur auf drei unterschiedliche Arten: (a) mit kartesischem Argumentmuster, (b) mit einer lokalen Deklaration, (c) mit Projektionen. Lösung 1.1: (a) fun comp (( x : int , y : int ) , b : bool ) = ( x = y ) = b (b) fun comp ( p : ( int * int )* bool ) = let val (( x , y ) , b ) = p in (x = y) = b end (c) fun comp ( p : ( int * int )* bool ) = (#1 (#1 p ) = #2 (#1 p )) = #2 p Aufgabe 1.2 (a) Schreiben Sie eine Prozedur add: int*int → int, welche zwei Zahlen x ∈ N und y ∈ Z addiert, aber nur mittels Addition und Subtraktion von 1. Geben Sie zunächst unbedingt die Rekursionsgleichungen an. (b) Schreiben Sie eine Prozedur mult: int*int → int, welche die Multiplikation zweier Zahlen x ∈ N und y ∈ Z implementiert, ohne dabei den Multiplikationsoperator ∗ zu benutzen. Geben Sie zunächst unbedingt die Rekursionsgleichungen an. (c) Schreiben Sie eine Prozedur pot: int*int → int, welche zu einer Basis a ∈ Z und einem Exponent c ∈ N die Potenz ac berechnet. Sie dürfen dabei weder den Multiplikationsoperator *, noch einen Potenzoperator oder eine Standardstruktur verwenden. Geben Sie zunächst unbedingt die Rekursionsgleichungen an. (d) Schreiben Sie nun die oben implementierten Prozeduren endrekursiv, falls Sie dies nicht bereits getan haben. Geben Sie auch hier die Rekursionsgleichungen vorher an. Lösung 1.2: (a) add (0, y) = y add (x, y) = 1 + add (x − 1, y) für x 6= 0 1 fun add ( x : int , y : int ) : int = if x = 0 then y else 1 + add ( x−1 , y ) (b) mult (0, y) = 0 mult (x, y) = y + mult(x − 1, y) für x 6= 0 fun mult ( x : int , y : int ) : int = if x = 0 then 0 else y + mult ( x−1 , y ) (c) pot (x, 0) = 1 pot (x, n) = x ∗ pot (x, n − 1) für n 6= 0 fun pot ( x : int , n : int ) : int = if n = 0 then 1 else mult (x , pot (x , n−1)) (d) • add (0, y) = y add (x, y) = add (x − 1, y + 1) für x 6= 0 fun add (x , y ) = if x = 0 then y else add ( x−1 , y+1) • mul0 (0, y, a) = a mul0 (x, y, a) = mul0 (x − 1, y, a + y) für x 6= 0 fun mul ’( x ,y , a ) = if x = 0 then a else mul ’( x−1 ,y , a+y ) fun mul (x , y ) = mul ’( x ,y ,0) • pot0 (x, 0, a) = a pot0 (x, n, a) = pot (x, n − 1, a ∗ x) für n 6= 0 fun pot ’( x ,n , a ) = if n = 0 then a else pot ’( x , n−1 , mul (a , x )) fun pot (x , n ) = pot ’( x ,n ,1) Aufgabe 1.3 Wir wollen die Fakultät einer natürlichen Zahl n rekursiv berechnen. (a) Stellen Sie Rekursionsgleichungen auf, die die Berechnung beschreiben. (b) Überführen Sie diese Rekursionsgleichungen nun in ein SML-Programm, sodass Sie eine Prozedur fac: int → int erhalten. (c) Schreiben Sie nun eine Prozedur facend: int → int mit einer endrekursiven Hilfsprozedur, stellen Sie dazu zunächst erneut die Rekursionsgleichungen auf. Lösung 1.3: (a) f ac 0 = 1 f ac n = n · f ac (n − 1) für n ≥ 1 (b) fun fac ( n : int ) : int = if n=0 then 1 else n * fac ( n−1) (c) f ac0 (0, a) = a f ac0 (n, a) = f ac0 (n − 1, n · a) für n ≥ 1 f acend n = f ac0 (n, 1) fun fac ’ ( n : int , a : int ) : int = if n = 0 then a else fac ’( n−1 , n * a ) fun facend ( n : int ) : int = fac ’( n , 1) Aufgabe 1.4 Diskutieren Sie mit Ihren Kommilitonen die Unterschiede einer Funktion gegenüber einer Prozedur. Erinnern Sie sich dabei an den Begriff der Berechnungsvorschrift. Schlagen Sie gegebenenfalls Begriffe nach oder fragen Sie 2 Ihren Tutor und beantworten Sie danach die folgenden Fragen: (a) Sind folgende Funktionen gleich? f (x) = x + x f 0 (x) = 2 · x (b) Sind folgende Prozeduren gleich? fun f ( x : int ) : int = x+x fun f ’ ( x : int ) : int = 2* x Lösung 1.4: Die Prozeduren sind nicht gleich, man könnte höchstens davon sprechen, dass sie die selbe Ergebnisfunktion berechnen, aber wir werden in späteren Kapiteln mehr darüber lernen. Die Funktionen hingegen sind gleich, da sie nicht aktiv einer Berechnungsvorschrift folgen, sondern uns direkt das Ergebnis ausgeben. Aufgabe 1.5 Gegeben ist folgende Prozedur: fun add ( x : int , y : int ) : int = if y = 0 then x else if y < 0 then add ( x−1 , y+1) else add ( x+1 , y−1) Schreiben Sie ein vollständiges Ausführungsprotokoll des Prozeduraufrufs add(4, ∼2). Lösung 1.5: add (4 , ∼2) = if ∼2 = 0 then 4 else if ∼2 < 0 then add (4−1 , ∼2+1) else add (4+1 , ∼2−1) = if false then 4 else if ∼2 < 0 then add (4−1 , ∼2+1) else add (4+1 , ∼2−1) = if ∼2 < 0 then add (4−1 , ∼2+1) else add (4+1 , ∼2−1) = if true then add (4−1 , ∼2+1) else add (4+1 , ∼2−1) = add (4−1 , ∼2+1) = add (3 , ∼2+1) = add (3 , ∼1) = if ∼1 = 0 then 3 else if ∼1 < 0 then add (3−1 , ∼1+1) else add (3+1 , ∼1−1) = if false then 3 else if ∼1 < 0 then add (3−1 , ∼1+1) else add (3+1 , ∼1−1) = if ∼1 < 0 then add (3−1 , ∼1+1) else add (3+1 , ∼1−1) = if true then add (3−1 , ∼1+1) else add (3+1 , ∼1−1) 3 = add (3−1 , ∼1+1) = add (2 , ∼1+1) = add (2 , 0) = if 0 = 0 then 2 else if 0 < 0 then add (2−1 , 0+1) else add (2+1 , 0−1) = if true then 2 else if 0 < 0 then add (2−1 , 0+1) else add (2+1 , 0−1) = 2 Aufgabe 1.6 Gegeben sind nun folgende Prozeduren, die die Fibonacci-Zahlen endrekursiv berechnen: fun fib ’ ( n : int , a : int , b : int ) : int = if n = 0 then a else fib ’( n − 1 , b , a + b ) fun fib ( n : int ) : int = fib ’( n , 0 , 1) Schreiben Sie ein vollständiges Ausführungsprotokoll des Prozeduraufrufs fib(5). Lösung 1.6: fib (5) = = = = = = = = = = = = = = = = = = = = = = = = = = = = = fib ’(5 , 0 , 1) if 5 = 0 then 0 else fib ’(5 − 1 , 1 , 0 + 1) if false then 0 else fib ’(5 − 1 , 1 , 0 + 1) fib ’(5 − 1 , 1 , 0 + 1) fib ’(4 , 1 , 0 + 1) fib ’(4 , 1 , 1) if 4 = 0 then 1 else fib ’(4 − 1 , 1 , 1 + 1) if false then 1 else fib ’(4 − 1 , 1 , 1 + 1) fib ’(4 − 1 , 1 , 1 + 1) fib ’(3 , 1 , 1 + 1) fib ’(3 , 1 , 2) if 3 = 0 then 1 else fib ’(3 − 1 , 2 , 1 + 2) if false then 1 else fib ’(3 − 1 , 2 , 1 + 2) fib ’(3 − 1 , 2 , 1 + 2) fib ’(2 , 2 , 1 + 2) fib ’(2 , 2 , 3) if 2 = 0 then 2 else fib ’(2 − 1 , 3 , 2 + 3) if false then 2 else fib ’(2 − 1 , 3 , 2 + 3) fib ’(2 − 1 , 3 , 2 + 3) fib ’(1 , 3 , 2 + 3) fib ’(1 , 3 , 5) if 1 = 0 then 3 else fib ’(1 − 1 , 5 , 3 + 5) if false then 3 else fib ’(1 − 1 , 5 , 3 + 5) fib ’(1 − 1 , 5 , 3 + 5) fib ’(0 , 5 , 3 + 5) fib ’(0 , 5 , 8) if 0 = 0 then 5 else fib ’(0 − 1 , 8 , 5 + 8) if true then 5 else fib ’(0 − 1 , 8 , 5 + 8) 5 4 Aufgabe 1.7 (a) Schreiben Sie eine Prozedur equal: int*int → bool, die testet, ob zwei positive Zahlen gleich sind. Sie dürfen dabei nur auf die Gleichheit mit 0 testen. (b) Können Sie auch eine Prozedur equal’: int*int → bool schreiben, die als Argumente sowohl positive als auch negative Zahlen nimmt und testet, ob diese gleich sind? Auch hier dürfen Sie nur mit =, <, >, <= oder >= auf 0 testen. Lösung 1.7: (a) fun equal ( x : int , y : int ) : bool = if x = 0 then y = 0 else equal ( x−1 , y−1) (b) fun equal ’ ( x : int , y : int ) : bool = let fun selbesvorzeichen ( x : int , y : int ) : bool = if x < 0 then y < 0 else y >= 0 fun equalpos ( x : int , y : int ) : bool = if x = 0 then y = 0 else equalpos ( x−1 , y−1) fun equalneg ( x : int , y : int ) : bool = if x = 0 then y = 0 else equalneg ( x+1 , y+1) in if selbesvorzeichen (x , y ) then if x < 0 then equalneg (x , y ) else equalpos (x , y ) else false end Aufgabe 1.8 (Größte Ziffer einer Zahl) Implementieren Sie eine Prozedur maxziffer: int → int, die zu einer natürlichen Zahl die größte in der Zahl enthaltene Ziffer bestimmt. Hinweis: Erleichtert Ihnen eine Hilfsprozedur maxziffer’: int*int → int die Aufgabe? Lösung 1.8: fun max ( x : int , y : int ) : int = if x > y then x else y fun maxziffer ’ ( x : int , n : int ) : int = if ( x div 10) = 0 then max (x , n ) else if ( x mod 10) > n then maxziffer ’( x div 10 , x mod 10) else maxziffer ’( x div 10 , n ) fun maxziffer ( x : int ) : int = maxziffer ’( x ,0) Aufgabe 1.9 Schreiben Sie eine Prozedur, die prüft, ob eine natürliche Zahl gerade ist: 5 (a) unter Verwendung des Modulo-Operators. (b) ohne Verwendung des Modulo-Operators. Geben Sie zunächst die Rekursionsgleichungen der Prozedur an. (c) Schreiben Sie nun mithilfe Ihrer gerade geschriebenen Prozedur aus Teil (b) eine Prozedur, die prüft, ob eine ganze Zahl ungerade ist. Lösung 1.9: (a) fun gerade ( x : int ) : bool = if ( x mod 2) = 0 then true else false Alternativ: fun gerade ( x : int ) : bool = x mod 2 = 0 (b) gerade0 0 = true gerade0 1 = f alse gerade0 x = gerade0 (x − 2) für x > 1 fun gerade ’ ( x : int ) : bool = if x = 0 then true else if x = 1 then false else gerade ’( x−2) (c) fun ungerade ( x : int ) : bool = if x < 0 then ungerade (∼x ) else if gerade ’( x ) then false else true Aufgabe 1.10 Schreiben Sie eine Prozedur nextprim: int → int, die zu einer gegebenen natürlichen Zahl n die nächstgrößere Primzahl liefert. Sie dürfen davon ausgehen, dass eine Hilfsprozedur istprim: int → bool gegeben ist, die überprüft, ob eine Zahl Primzahl ist. Der Aufruf von nprim 7 würde 11 liefern. Lösung 1.10: fun nextprim ( n : int ) : bool = if istprim ( n+1) then n+1 else nextprim ( n+1) Aufgabe 1.11 Sei folgender Code gegeben: fun verdoppler ’ ( n : int , k : int , i : int ) : int = ... fun verdoppler ( n : int ) : int = verdoppler ’ (n ,n , n ) Es soll gelten, dass verdoppler n = 2n. Geben Sie Rekursionsgleichungen für verdoppler’ an, sodass beim Verdoppeln von n ∈ N genau 2n Rekursionaufrufe nötig sind. Sie dürfen die Argumente nur inkrementieren (um eins erhöhen) bzw. dekrementieren (um eins verringern)! Füllen Sie anschließend die Lücke im obigen Programm. Ist Ihre Lösung endrekursiv? Lösung 1.11: verdoppler0 (n, 0, 0) = n verdoppler0 (n, 0, i) = verdoppler0 (n, 0, i − 1) 0 für i > 0 0 verdoppler (n, k, i) = verdoppler (n + 1, k − 1, i) für k > 0 Welchen Vorteil hat es, dass im letzten Fall nicht verdoppler0 (n, k, n) steht, obwohl wir den letzten Fall nur so verwenden? fun verdoppler ’ ( n : int , k : int , i : int ) : int = if k > 0 then verdoppler ’ ( n + 1 , k − 1 , i ) else if i > 0 then verdoppler ’ (n , 0 , i − 1) 6 else n fun verdoppler ( n : int ) : int = verdoppler ’ (n ,n , n ) Die vorgestellte Lösung ist endrekursiv. Aufgabe 1.12 Schreiben Sie eine Prozedur istprim: int → bool, die entscheidet, ob eine Zahl n ≥ 2 eine Primzahl ist. Hinweise: • Durch welche Zahlen ist eine Primzahl teilbar? j nk • Wann gilt n = · k? k Lösung 1.12: Der zweite Hinweis ist ein Test auf Teilbarkeit. Gleichheit gilt genau dann, wenn n durch k teilbar ist. fun istprim ’ ( n : int , k : int ) : bool = if k = n then true else if ( n div k ) * k = n then false else istprim ’ (n , k + 1) fun istprim ( n : int ) : bool = istprim ’ (n , 2) Standard ML – keine imperative Programmiersprache Hinweis: Die folgenden Aufgaben sollten Sie insbesondere dann bearbeiten, wenn Sie schon Erfahrungen in imperativen Programmiersprachen1 haben, Standard ML aber Ihre erste funktionale Programmiersprache2 ist. Aufgabe 1.13 Beantworten Sie die folgenden Fragen (für den Teil von Standard ML, das Sie bisher kennen). Ignorieren Sie dabei den Ergebnisbezeichner it. (a) Was ist die Aufgabe eines let-Ausdrucks? (b) Können Sie eine Prozedur f : unit → int schreiben, so dass f () = f () manchmal, aber nicht immer zu true auswertet? (c) Können Sie die Funktionsweise einer Prozedur nach ihrer Deklaration beeinflussen, ohne sie erneut zu deklarieren? (d) Warum dürfen Sie bei Projektionen keine Bezeichner benutzen? (#2 (1,2) ist gültig, #x (1,2) nicht) Lösung 1.13: (a) Ein let-Ausdruck dient dazu, Deklarationen an einer Stelle durchzuführen, an der ein Ausdruck stehen müsste. (b) Nein. Dies ist in funktionalen Programmiersprachen genauso unmöglich wie in der Mathematik. (c) Nein. Dies ist in funktionalen Programmiersprachen genauso unmöglich wie in der Mathematik. (d) Dann ließen sich die Typen nicht bestimmen: #2 (1,true) hat eindeutig den Typ bool, bei #x (1,true) hinge der Typ von der Belegung des Bezeichners ab. Aufgabe 1.14 Betrachten Sie den folgenden Algorithmus in Pseudocode. procedure exploder (n , a , b ): do while ( n > 0) do n := n − 1 tmp := a 1 Imperative Programmiersprachen sind beispielsweise C und alle ähnlichen Sprachen, Java, Basic, Python, Pascal, Delphi, . . . Programmiersprachen sind beispielsweise Haskell, OCaml, F#, . . . 2 Funktionale 7 a := a ∗ b b := tmp end return a end Geben Sie eine Standard-ML-Prozedur an, die diesen Algorithmus möglichst genau umsetzt. Lösung 1.14: fun exploder ( n : int , a : int , b : int ) = if n <= 0 then a else exploder ( n−1 , a *b , a ) Aufgabe 1.15 Welche Funktion berechnet folgende Prozedur? val s = 1; fun while_schleife ( n : int ) : int = let val s = 2 * s in if n <= 0 then s else while_schleife ( n−1) end Warum berechnet sie nicht die Funktion 2n ? Lösung 1.15: Die Prozedur berechnet einfach immer die konstante Funktion 2. Das liegt daran, dass das s in der lokalen Deklaration nichts mit dem zuerst deklarierten Bezeichner s zu tun hat. Insbesondere wird der Wert des lokalen s immer nur an den Wert des äußeren Bezeichners s gebunden. Weil sich in SML der Wert eines Bezeichners niemals ändern kann wird also immer 2 zurückgegeben. Man sieht das besser wenn man das lokale s einfach umbenennt: val s = 1; fun while_schleife ( n : int ) : int = let val x = 2 * s in if n<=0 then x else while_schleife ( n−1) end 8