Tentamen Programmeringsteknik II 2015-05-26 Skrivtid: 0800 1300 Tänk på följande Skriv läsligt! Använd inte rödpenna! Skriv bara på framsidan av varje papper. Börja alltid ny uppgift på nytt papper. Lägg uppgifterna i ordning. Skriv uppgiftsnummer och din kod på alla papper. Skriv inte längst upp i vänstra hörnet - det går inte att läsa där efter sammanhäftning. Fyll i försättssidan fullständigt. Det är principer och idéer som är viktiga. Skriv så att du övertygar rättaren om att du har förstått dessa även om detaljer kan vara felaktiga. Programkod skall vara läslig, d.v.s. den skall vara vettigt strukturerad och indenterad. Namn på variabler, metoder, funktioner, klasser etc. skall vara beskrivande men kan ändå hållas ganska korta. Såvida inget annat anges, får man bygga på lösningar till föregående uppgifter även om dessa inte har lösts. Det är tillåtet att införa hjälpmetoder och hjälpklasser. Uttrycket skriv en metod som skall alltså inte tolkas så att lösningen inte får struktureras med hjälp av flera metoder. Alla uppgifter gäller programmeringsspråket Java och programkod skall skrivas i Java. Koden skall vara läslig dvs den skall vara vettigt strukturerad och indenterad. Namn på variabler, metoder, klasser etc skall vara beskrivande men kan ändå hållas ganska korta. Lycka till! Observera att poängavdrag bland annat kan göras för icke-privata eller onödiga instansvariabler, dålig läslighet, upprepning av identisk kod, underlåtenhet att utnyttja given eller egen tidigare skriven metod, underlåtenhet att utnyttja arv och lösningar som har väsentligt sämre komplexitet än nödvändigt t ex Θ(n 2 ) i stället för Θ(n). 1
Uppgifter 1. Uppgiften finns på bifogad svarsblankett. Ringa in rätta svaren och lämna blanketten tillsammans med dina övriga svar. 2. I en bilaga finns en klass BinaryTree som representerar binära träd (observera att det inte är sökträd). a) Skriv en metod (eventuellt med en hjälpmetod) som returnerar summan av data-attributen i noderna. För nedanstående trädexempel är summa 1 + 2 + 3 + 4 + 5 = 15. public int sum() { return sum(root); private static int sum(node r) { if (r==null) return 0; else return r.data + sum(r.left) + sum(r.right); b) Skriv klart den rekursiva hjälpmetoden int numberofleaves(node r) som returnerar antalet löv i (sub-)trädet som har r som rot. Ett löv är en nod som saknar barn. Antalet lövnoder i nedanstående exempelträd är 2. private static int numberofleaves(node r) { if (r==null) return 0; else if (r.left==null && r.right==null) return 1; else return numberofleaves(r.left) + numberofleaves(r.right); c) I koden finns en metod write som skriver ut trädet i preorder på en fil. Utskriften görs med hjälp av tostring-metoderna. För varje nod skrivs tre heltal. Det första är nodens dataattribut. De två följande anger huruvida noden har vänster- respektive högerbarn. Talet 1 anger att barnet finns, 0 att det inte finns. Exempel: Vidstående träd (som också används i körexemplen) representeras av följden 1 1 1 2 0 0 3 1 0 4 0 1 5 0 0 2 1 4 3 5 Metoden read(string filename) används för att läsa in träd lagrade på det sättet från en fil. Skriv den rekursiva hjälpmetoden read(scanner scan)! Tips: Om uppgiften verkar svår sparas den lämpligen till sist! 2
private static Node read(scanner scan) { int data = scan.nextint(); int lft = scan.nextint(); int rgt = scan.nextint(); Node left = null; if (lft==1) left = read(scan); Node right = null; if (rgt==1) right = read(scan); return new Node(data, left, right); 3. I bilagan finns kod för en enkel kalkylator. Kalkylatorn är implementerad med recursive descent. Så här kan en körning se ut: Input : 1+2 Result: 3.0 Input : 1+2=x Result: 3.0 Input : x Result: 3.0 Input : 3*x Result: 9.0 Input : y Error: Undefined variable: y Input : 3*(2*x = y) = z Result: 18.0 Input : y Result: 6.0 Input : z Result: 18.0 Input : 1 Result: 1.0 Input : -1 Result: 1.0 Input : -2-3 Result: 5.0 Input : 6-2*y-z Result: 0.0 Input : 6 - (2*y-z) Result: 12.0 Input : -2 - -y Result: 4.0 Input : -5 = x = y Result: 5.0 Input : x Result: -5.0 Kalkylatorn kan alltså hantera konstanter, variabler, de fyra räknesätten och parenteser. Dessutom kan den hantera absolutbelopp med tecknet på vanligt sätt. a) Skriv den sats som fattas i konstruktorn. variables = new TreeMap<String,Double>(); b) Skriv de satser i factor som hanterar variabler. Om variabeln inte finns definierad i variables ska ett undantag av typen CalculatorException kastas. I annat fall skall värdet hämtas och returneras. if (variables.containskey(w)) { //<<<<<< result = variables.get(w); // tokenizer.nexttoken(); // else { // throw new CalculatorException("Undefined variable: " + w); // 3
c) Skriv de kompletteringar som behövs (vilka satser och var) för att hantera absolutbelopp enligt körexemplet. I factor: else if ( tokenizer.getchar() == ) { tokenizer.nexttoken(); result = Math.abs(assignment()); if (tokenizer.getchar() == ) { tokenizer.nexttoken(); else { throw new CalculatorException("Expected but found: " + tokenizer.gettoken()); 4. Hashtabeller är en effektiv struktur för att lägga in, lagra och söka poster med nycklar (t ex strängar, personnummer,... ). Idén är att man använder ett stort antal listor och en funktion ( hashfunktionen ) som väljer vilken lista en given nyckel skall lagras/sökas i. En array ( hashtabellen ) används för att hålla reda på listorna. Hashtabeller kan bland annat användas för avbildningar som t ex variabler med värden som i de obligatoriska uppgifterna. Nedanstående figur exemplifierar en hashtabell med strängar som nycklar och numeriska värden som data (dvs som i den numeriska kalkylatorn). 0 "pi" 3.14159 1 "y" 5.0 "z" 7.0 "x" 2 hash 3 4 "x" Node 3.0 "e" 2.71828 nodes Noderna i listorna innehåller nyckel, värde och en referens till nästa nod i listan. Hashfunktionen (hash i figuren) gör magiska beräkningar på nyckeln som resulterar i ett heltal som används som index i arrayen nodes. En bra hashfunktion ska sprida nycklarna någorlunda jämnt över hela tabellen. I detta exempel returnerar tydligen hash värdet 0 för strängen "pi", 1 för strängarna "y" och "z" samt 4 för strängarna "x" och "e". Poängen med hashtabeller är alltså att man snabbt kan beräkna vilken av de många listorna man ska söka/lagra en given nyckel i. Om tabellens storlek väljs i proportion till det förväntade antalet element blir listorna mycket korta. Därför blir operationerna inlägg, sökning och urtag alla i genomsnitt O(1). I en bilaga finns klassen VariableHashMap som implementerar denna struktur. Uppgifter: 4
a) Skriv klart metoden containskey(string key) som returnerar true om nyckeln key finns i hashtabellen annars false. public boolean containskey(string key) { int hashvalue = hash(key, nodes.length); return containskey(key, nodes[hashvalue]); private boolean containskey(string key, Node n) { if(n == null) return false; if(n.key.equals(key)) return true; else return containskey(key, n.next); b) Skriv klart metoden double get(string key) som letar upp värdet för strängen key och returnerar det. Om key inte finns så skall ett undantag av typen VariableHashMapException kastas. public double get(string key) { int hashvalue = hash(key, nodes.length); Node n = nodes[hashvalue]; while(n!= null && n.key.equals(key) == false) n = n.next; if(n == null) throw new VariableHashMapException("Key " + key + " not found."); return n.value; c) Skriv klart den rekursiva hjälpmetoden Node putrecursive(node n, String key, double value) Om nyckeln key redan finns i den (del)lista som börjar med noden n ska värdet i noden med nyckel key ändras till value. Om nyckeln inte finns så skall en ny nod med denna nyckel och värdet value läggas in i listan. I båda fallen ska en referens till första noden i den modifierade (del)listan returneras. private static Node putrecursive(node n, String key, double value) { if(n == null) return new Node(key, value, null); if(n.key.equals(key)) { n.value = value; return n; else { n.next = putrecursive(n.next, key, value); return n; Dina lösningar ska givetvis bevara O(1)-egenskapen! 5. I bilagan Measurements finns en klass som lagrar en följd av mätvärden i tidsordning (inläggningsordning). 5
Demoprogram: Utskrifter: public static void main(string[] args) { Measurements m = new Measurements(); m.add(1.); m.add(5.); m.add(3.); m.add(4.); m.add(5.); m.add(3.); m.smooth(); [1.0, 5.0, 3.0, 4.0, 5.0, 3.0] [1.0, 3.0, 4.0, 4.0, 4.0, 3.0] Din uppgift är att skriva en klass SortedMeasurements som ärver från Measurements men som lagrar värdena värdena i storleksordning i stället för i ankomstordning. Eftersom metoden smooth nu är meningslös så klassen skall förhindra att den används. Så här ska det se ut med samma demonstrationsprogram men med ett objekt av typen SortedMeasurements Demoprogram: public static void main(string[] args) { Measurements m = new SortedMeasurements(); m.add(1.); m.add(5.); m.add(3.); m.add(4.); m.add(5.); m.add(3.); m.smooth(); Utskrifter: [1.0, 3.0, 3.0, 4.0, 5.0, 5.0] SortedMeasurements$SortedMeasurementsException: Smooth may not be applied to SortedMeasurements at SortedMeasurements.smooth(SortedMeasurements.java:23) Klassen skall utnyttja arv för allt som går och bara implementera det som är nödvändigt. public class SortedMeasurements extends Measurements { public static class SortedMeasurementsException extends RuntimeException { public SortedMeasurementsException(String msg) { super(msg); @Override public void add(double x) { int i = 0; while (i<thedata.size() && x>=thedata.get(i)) { i++; thedata.add(i, x); @Override public void smooth() { throw new SortedMeasurementsException("Smooth may not " + "be applied to SortedMeasurements"); 6