1 - Backus-Naur-Form (a) Ableitung des Wortes /a*(a b)b/ aus der gegebenen BNF: R Regel R -> '/' S '/' => '/' S '/' Regel S -> E S => '/' E S '/' Regel E -> E '*' => '/' E '*' S '/' Regel E -> D => '/' D '*' S '/' Regel D -> 'a' => '/' 'a' '*' S '/' Regel S -> E S => '/' 'a' '*' E S '/' Regel E -> '(' E ' ' E ')' => '/' 'a' '*' '(' E ' ' E ')' S '/' Regel E -> D (2x) => '/' 'a' '*' '(' D ' ' D ')' S '/' Regel D -> 'a' => '/' 'a' '*' '(' 'a' ' ' D ')' S '/' Regel D -> 'b' => '/' 'a' '*' '(' 'a' ' ' 'b' ')' S '/' Regel S -> E => '/' 'a' '*' '(' 'a' ' ' 'b' ')' E '/' Regel E -> D => '/' 'a' '*' '(' 'a' ' ' 'b' ')' D '/' Regel D -> 'b' => '/' 'a' '*' '(' 'a' ' ' 'b' ')' 'b' '/' Ableitungsbaum für das Wort /a*(a b)b/: R +-------------------+---------------+ '/' S '/' +-------------+-----+ E S +-+-+ +-----+-----+ E '*' E S +--+---+---+---+ D '(' E ' ' E ')' E 'a' D D D 'a' 'b' 'b' (b) Definition einer BNF für Zeichenfolgen aus n Buchstaben gefolgt von n Ziffern (n > 0): Z ::= A B A Z B A ::= '0'... '9' B ::= 'a'... 'z' 1
2 - (6/8 ECTS) Termauswertung (a) Umwandlung von 5 ** 2 < 4 + val / 3 in Term-Baum-Darstellung: < / \ ** + / \ / \ 5 2 4 / / \ val 3 (b) Umwandlung des Ausdrucks 5 ** 2 < 4 + val / 3 in Postfix-Darstellung: 5 2 ** 4 val 3 / + < Auswertung des Ausdrucks in Postfix-Darstellung mittels Stack-Maschine, wobei val durch 7 ersetzt wurde: 5 2 ** 4 7 3 / + < => 5 2 ** 4 7 3 / + < => 5 2 ** 4 7 3 / + < => 25 4 7 3 / + < => 25 4 7 3 / + < => 25 4 7 3 / + < => 25 4 7 3 / + < => 25 4 2 + < => 25 6 < => false Das Ergebnis des Ausdrucks ist also der boolesche Wert false. 3 - (5 ECTS) Programmpunkttabelle PP n m r i j x Ausgabe # 1 2 # 2 3 # 3 1 # 4 1 # 5 0 # 6 1 # 7 1 # 6 2 # 7 2 # 8 # 9 2 # 4 2 # 5 0 # 6 1 # 7 2 # 6 2 # 7 4 # 8 2
PP n m r i j x Ausgabe # 9 4 #10 #11 4 (b) Das Programm berechnet n m in der Variablen r. In jedem Schritt der äußeren for-schleife berechnet die innere for-schleife x := n r und setzt dann r := x. Vor der Schleife (bei #3) gilt r = 1. Nach dem ersten Durchlauf (bei #9) gilt r = n 1 = n, nach dem zweiten Durchlauf r = n n = n 2 und nach dem m-ten entsprechend r = n n m 1 = n m. 4 - Methodenschreibweise 1. Indeed, z. B. für a = "Hallo", b ="Welt" 2. Ganz genau, z. B. für a = 38, b = 4 3. Nein, die Operation + ist auf booleschen Werten nicht definiert. 4. Nein, man kann auch einen String vervielfachen; * ist also auch eine Methode für Strings und Ganzzahlen, z. B. für a = "Ho", b = 3 5. Nein, c[2] ist ein Ausdruck; genau genommen ist es der Wert, der an Indexposition 2 im Array c oder im String c steht. 6. Richtig, hier wird das Element an Indexposition 2 von c (wobei c ein Array oder String ist) durch den Wert 42 ersetzt; genau genommen wird die mutierende Methode []=(x, y) auf einem Array/String aufrufen, die das Element an der Indexposition x durch den Wert von y ersetzt, in Methodenschreibweise: c.[]=(2, 42). 7. Ja, man kann die Zuweisung auch schreiben als a.[](1).[]=(3, 73): Der erste Methodenaufruf selektiert das erste Element des Arrays a. In diesem Fall muss dieser Wert wieder ein Array sein, damit die zweistellige Methode []=(x, y) aufgerufen werden kann, die dem Array an der Indexposition x den Wert y durch einen Seiteneffekt zuweist. Damit ist der zweite Methodenaufruf in der Tat mutierend, der erste jedoch nicht. 8. Ja, z. B. für b = [true, [0, 1, 2, 42]] 9. Nein, es ist natürlich genau anders herum; mutierende Funktionen werden durch ein Ausrufezeichen am Ende des Namens signalisiert. 10. Genau, der **-Operator ist rechtsassoziativ. 5 - Tabellensumme # Aufgabe 5 (a) # Iterative Variante def array_sum_odd(a) sum = 0; for index in 0.. a.size-1 do if a[index] % 2!= 0 then sum = sum + a[index]; return sum; 3
# Rekursive Variante def array_sum_odd(a) sum = 0; if a.size > 0 then if a[0] % 2!= 0 then sum = a[0]; sum = sum + array_sum_odd(a[1, a.size-1]); return sum; # Aufgabe 5 (b) # Iterative Variante def row_sum_odd(a) result = []; for index in 0.. a.size-1 do result[index] = array_sum_odd(a[index]); # Rekursive Variante def row_sum_odd(a) result = []; if a.size > 0 then result = [array_sum_odd(a[0])] + row_sum_odd(a[1, a.size-1]); # Aufgabe 5 (c) # Mutierende Variante def row_sum_odd!(a) for index in 0.. a.size-1 do a[index] = array_sum_odd(a[index]); 6 - Objektidentität Würde die *-Methode für i=1 keine Kopie erzeugen, sondern einfach dasselbe Objekt zurückliefern, so würde beim destruktiven Update y[0,1] = "b" dasjenige Objekt, auf das auch x zeigt, verändert. Das Programm würde sich also völlig anders verhalten, als für i=0 oder i>1, wo ja neue Objekte angelegt werden müssen, da diese nicht identisch aussehen. Beispiel: Für die Variablenbelegung i = 1 erhielte man, wenn * keine Kopie erzeugen würde: Zeile x y ------+--------------- 1 Obj1 2 Obj1 3 4
# Obj1 # # --------- # # String # # # # "a" PP 1 # # "b" PP 3 # Für die Variablenbelegung i = 2 erhält man dagegen (* erzeugt Kopie): Zeile x y ------+---------- 1 Obj1 2 Obj2 3 # Obj1 # # --------- # # String # # # # "a" (PP 1)# # Obj2 # # --------- # # String # # # # "aa" PP 2 # # "ba" PP 3 # 7 - (6/8 ECTS) Klassen und Objekte (a) Um von außen den Wert des Attributs @time zu lesen, wird eine Methode get_time hinzugefügt, die den Wert von @time zurückgibt: def get_time() return @time; (b) Die Methode to_s wird um eine Fallunterscheidung für die Stunden-Anzeige ergänzt. def to_s() time = @time; hours = 0; minutes = 0; seconds = 0; if time >= 3600 then hours = time / 3600; time = time % 3600; if time >= 60 then minutes = time / 60; 5
time = time % 60; if time > 0 then seconds = time; return hours.to_s + ":" + minutes.to_s + ":" + seconds.to_s; (c) Um eine echte Kopie eines Timer-Objekts zu erzeugen, wird zunächst ein neues Objekt erzeugt. Jetzt muss der Wert des @time-attribut des neuen Objekts copy auf denselben Wert gesetzt werden wie das @time-attribut des erzeugenden Objekts. Schreibzugriff von außen ist aber nicht erlaubt! Daher muss die Methode tick! verwenden werden, um den Wert des @count-attributs zu setzen. Die Methode wird solange für das Objekt copy aufgerufen, bis der gewünschte @time-wert erreicht ist (also @time mal): def clone() copy = Timer.new(); for i in 1.. @time copy.tick!(); return copy; 8 - (5 ECTS) Aufzählen und Überprüfen Eine Möglichkeit ist es, mit drei ineinander verschachtelten for-schleifen alle möglichen Werte für i, j und k durchzulaufen und jeweils zu prüfen, ob n = 2 i 3 j 5 k gilt. Die Werte laufen dabei jeweils von 0 bis m, wobei m = n vorgegeben ist: def is_hamming(n) result = false; m = Math.sqrt(n); for i in 0.. m do for j in 0.. m do for k in 0.. m do if 2**i * 3**j * 5**k == n then result = true; Eine weitere Möglichkeit besteht darin, mit drei ineinander verschachtelten while-schleifen die Variablen i, j und k aufzuzählen und die Aufzählung abzubrechen, sobald n = 2 i 3 j 5 k (n ist eine Hamming-Zahl) gilt. Diese Variante hat den Vorteil, dass nicht alle möglichen Werte aufgezählt werden müssen. def is_hamming(n) result = false; i = 0; while result == false && 2**i <= n do j = 0; while result == false && 2**i * 3*j <= n do k = 0; while 2**i * 3*j * 5**k < n do k = k + 1; 6
if 2**i * 3**j * 5**k == n then result = true; j = j + 1; i = i + 1; Die Variante mit der while-schleife lässt sich auch effizienter implementieren, indem nicht die Exponenten hochgezählt werden, sondern die potentielle Hamming-Zahl selbst (bzw. deren Faktoren x = 2 i, y = 2 i 3 j = x 3 j, z = 2 i 3 j 5 k = y 5 k ), also: def is_hamming(n) result = false; x = 1; while result == false && x <= n do y = x; while result == false && y <= n do z = y; while z < n do z = z * 5; if z == n then result = true; y = y * 3; x = x * 2; Anmerkung: Die Aufzählungsgrenze m = n in der Version mit for liefert für n = 8 das falsche Ergebnis. Besser wäre z.b. m = n (in Ruby: m = Math.sqrt(n).ceil) oder m = log 2 (n) (in Ruby: m = Math.log(n, 2).floor) zu verwenden, obwohl in beiden Fällen meist viel zu viele Zahlen getestet werden. Die optimimale Lösung wäre, für i von 0 bis log 2 (n) zu zählen, für j von 0 bis log 3 ( n 2 ) und für k von 0 i bis log 5 ( n 2 i 3 ) (in Ruby: Math.log(n, 2).floor, Math.log(n/(2**i), 3).floor und Math.log(n/(2**i j * 3**j), 5).floor). 7