Tentamen Programmeringsteknik II 2013-10-22 Skrivtid: 0800-1300 Hjälpmedel: Java-bok (vilken som helst) 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 att du har förstått dessa även om detaljer kan vara felaktiga. Programkod skall vara läslig dvs 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 så 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. Uppgifterna kommer inte i svårighetsordning. Betygsgränser: 15 ger säkert 3, 21 ger säkert 4, 26 ger säkert 5. Lycka till! 1
Uppgifter 1. Nedanstående figur visar noderna i ett binärt sökträd med heltal som nycklar. 12 3 23 1 7 17 31 5 20 42 a) Rita hur trädet ser ut efter inlägg av nycklarna 22, 19, 24, 18 och 15 (i den givna ordningen). (2p) 12 3 23 1 7 17 31 5 15 20 24 42 19 22 18 b) Skriv en klass Node som skall representera noderna i trädet samt en klass BST som representerar trädet. Klasserna skall innehålla nödvändiga instansvariabler och lämpliga konstruktorer. 2
Snyggast är att göra noden som en inre klass till BST-klassen. Då blir den dold för omvärlden men BST-klassen har full tillgång till den. Om man gör nodklassen som en egen självständig klass måste man ha get-metoder för dess attribut. BST-klassen behöver ingen konstruktor eftersom dess enda attribut root blir null automatiskt. public class BST { private Node root; private static class Node { private int key; private Node left; private Node right; private Node(int key, Node left, Node right) { this.key = key; this.left = left; this.right = right; c) Skriv en metod public int leaves() i klassen BST som returnerar antalet löv i trädet. (2p) Ett löv är alltså en nod som inte har några barn, public int leaves() { return leaves(root); private int leaves(node r) { if (r == null) { return 0; // inga löv else if (r.left == null && r.right == null){ return 1; // detta är ett löv else { // inte ett löv räkna på längre ner return leaves(r.left) + leaves(r.right); d) Skriv en metod void leveltraverse() i klassen BST som traverserar trädet i nivåordning och skriver ut värdet i varje nod som besöks. För trädet ovan skall alltså raden 12 3 23 1 7 17 31 5 20 42 skrivas ut. 3
Principen är att skriva en rekursiv metod som hittar allt på en viss nivå. När man har den kan man sen iterera över alla nivåer. // hjälpmetod, trädets höjd private int height(node r){ if(r == null) return 0; else return 1 + Math.max(height(r.left),height(r.right)); // fixa en angiven nivå public void leveltraverse(int n) { leveltraverse(root, n); private void leveltraverse(node r, int n) { if(r == null) return; // inget att göra if( n == 0) { // roten System.out.print(r.data + " "); else { // gå åt vänster, sedan åt höger leveltraverse(r.left, n - 1); leveltraverse(r.right, n - 1); // nu kan vi fixa allt, iterera över alla nivåer public void leveltraverse() { int n = height(); for (int i = 0; i < n; i++) leveltraverse(i); System.out.println(); Man kan också göra detta ickerekursivt, då behöver vi en kö. Algoritm: Köa rotnoden Så länge kön inte tom Ta ut första elementet i kön, skriv ut datadelen Köa vänsterbarnet följt av högerbarnet Klart. 2. Betrakta följande klasshierarki och testmetod: 4
public abstract class Animal { public String name; public final String s = "Animal"; public Animal(String name){ this.name = name; public String getname() { return name; public abstract String speak(); public class Dog extends Animal { public final String s = "Dog"; public String speak() { return getname() + ": " + s + " Woof "; public class Labrador extends Dog { public final String s = "Labrador "; public class Cat extends Animal { public final String s = "Cat"; public String speak() { return getname() + ": " + s + " Miaow "; public class Test { public static void main(string[] args) { Animal a1 = new Animal("Nisse"); System.out.println(a1.speak() + a1.s); Animal a2 = new Dog("Fido"); System.out.println(a2.speak() + a2.s); Dog a3 = new Labrador("Labbe"); System.out.println(a3.speak() + a3.s); Animal a4 = new Labrador("Lubbe"); System.out.println(a4.speak() + a4.s); Cat a5 = new Animal("Muppet"); System.out.println(a5.speak() + a5.s); Dog a6 = new Labrador("Ludde"); System.out.println(a6.speak() + a6.s); a) Det går inte att kompilera klassen Animal med dess underklasser som de är defininerade ovan, beroende på det sätt konstruktorerna är deklarerade. Förklara problemet och visa hur man ska rätta till det så att det fungerar på avsett sätt, dvs så att alla djur får ett namn när de skapas. Det går inte att kompilera Animal-hierarkin därför att det saknas konstruktorer i alla djurklasserna. Som det är nu så finns bara en parameterlös defaultkonstruktor där som anropar motsvarande parameterlösa konstruktor i Animal som saknas. Detta ger kompileringsfel. Detta gäller alldeles oavsett vad man gör i klassen Test. Vi måste alltså införa konstruktorer på något sätt. Exempelvis Public class Dog { public final String s = "Dog"; public Dog(String name) {... super(name); // anropa Animals konstruktor Detta gör också att klassen kan användas på det sätt man gör i Test Notera att det inte går att ersätta super med this.name = name; Då anropas fortfarande den icke existerande parameterlösa konstruktorn i Animal, vilket ger kompileringsfel Notera också att det går utmärkt att överskugga även final-deklarerade variabler. Man skulle kunna ta bort konstruktorn i Animal, då går det att kompilera eftersom vi då får en parameterlös defaultkonstruktor i klassen. Men då kan vi ju inte använda klassen på det sätt vi gör i Test för att namnge djuren. b) Klassen Test med sin main-metod går inte heller att kompilera. Stryk satser i 5
main-metoden som är illegala och ange sedan vad metoden skriver ut. Förklara också varför du tar bort de satser du strukit. Ta bort satserna Animal a1 = new Animal("Nisse"); System.out.println(a1.speak() + a1.s); Vi kan inte skapa objekt av typ Animal eftersom den är abstrakt Cat a5 = new Animal("Muppet"); System.out.println(a5.speak() + a5.s); Som ovan, dessutom kan inte ett objekt deklarerat som Animal tilldelas ett objekt deklarerat som en Cat. Utskrift: Fido: Dog Woof Animal Labbe: Dog Woof Dog Lubbe: Dog Woof Animal Ludde: Dog Woof Dog Notera att uttryck som a2.s binds av hur a2 är deklarerad, inte av vad a2 faktiskt är, vi får alltså Animal eftersom a2 är deklarerad som en sådan. Uttrycket a2.speak() (ett metodanrop) binds däremot av vad a2 faktiskt är, i detta fall av typen Dog. Det finns ingen speak i Labrador, alltså kan aldrig texten Labrador skrivas ut, utan det blir alltid speak i Dog för dessa och då används s i Dog. 3. Vi har följande klass som representerar en sorterad länkad lista: public class List { private Node first; private static class Node { private int value; private Node next; private Node(int value, Node next) { this.value = value; this.next = next; public List() { first = null; private List(Node n) { first = n; public String tostring() { String res = ""; for (Node n = first; n!= null; n = n.next) { res = res + n.value; if (n.next!= null) { res = res + ", "; return "{" + res + ""; private static Node insert(int k, Node n) { if (n==null k<n.value) { return new Node(k, n); else if (k>n.value) { n.next = insert(k, n.next); return n; public void insert(int k) { first = insert(k, first); a) Man skulle kunna tänka sig att implementera metoden insert på detta sätt: 6
public void insert(int k) { if (first == null first.value > k) first = new Node(k, first); else { // gör en sublista med alla noder // utom den första och lägg in i den List l = new List(first.next); l.insert(k); Förklara vad som händer och varför inte detta fungerar som tänkt. Metoden testar först om den nya noden ska in först i listan. Om det är så så läggs den in först i listan. Detta fungerar utmärkt. Annars skapas en ny lista och vi rekurserar med denna. Så småningom kommer vi att lägga in något först i denna lista. Men problemet är då att vi inte lägger in något i den ursrungliga listan utan i en annan lista, som dessutom bara är borta när vi är klara. Vår lista ändras alltså inte alls utom om vi lägger först i listan. b) Skriv en metod boolean isdisjunkt(list l) som testar om aktuell lista och listan l saknar datavärden som är lika. Om det är så ska metoden returnera true, om listorna innehåller någon nod som är lika ska false returneras. Metoden ska vara O(n), dvs linjärt beroende av listornas längd. (5p) public boolean isdisjunkt(list l){ return isdisjunkt(first, l.first); private static boolean isdisjunkt(node n1, Node n2) { if (n1 == null n2 == null) { return true; // om någon är tom så är det sant else if(n1.value == n2.value) { return false; // lika alltså falskt else if(n1.value < n2.value) { // stega fram listan som är minst return isdisjunkt(n1.next, n2); else { return isdisjunkt(n1, n2.next); 4. I bilagan finns kod som delvis implementerar en kalkylator. (Det är väsentligen samma kod som gavs till obligatorisk uppgift 5. ) Av utrymmesskäl är vissa delar som saknar betydelse för uppgiften utelämnade. a) Vi vill utöka kalkylatorn med en funktion power(x,n) som returnerar värdet av x upphöjt till n. Denna funktion har alltså två parametrar till skillnad från de övriga funktioner vi använt. Förklara hur arvshierarkin ska förändras för att införa sådana funktioner. Förklara vilka klasser som behövs och vilka ändringar i övrigt som behövs för att göra detta. Det hela ska göras på ett generellt sätt så att vi enkelt kan införa flera funktioner som har två parametrar. 7
Du behöver inte skriva någon kod för de klasser du inför, beskriv mer abstrakt hur du tänker. Vi ska alltså införa en ny typ Function med två parametrar. Den passar naturligt in i hiearkin under klassen Binary. Man skapar en hierarki som är lika som den till klassen Unary. Det betyder att vi får ett subträd för binära operatorer och ett annat för funktioner med två operander. Klassen Power görs sedan som en subklass till den senare klassen. Som en enklare lösning (fast mindre generell och därför inte så bra) kan man placera Power som en subklass direkt under Binary. Dessutom måste vi skriva den nya klasserna och förändra primary i Parsern så att den kan hantera denna funktion. Vi behöver också en ny metod i Symbolic som hanterar denna funktion på liknande sätt som övriga funktioner. b) De trigonometriska funktionerna (sin, cos... ) förutsätter att argumentet är givet i radianer. Ibland kan man ju vilja uttrycka sig i grader. För att kunna ange att man vill använda grader har kommandot set lagts in i metoden statement. Kommandot skall följas antingen av ordet degrees eller ordet radians. Exempel: Input : sin(0.5*pi) Parsed: sin(0.5*pi) Result: 1.0 Input : sin(90) Parsed: sin(90.0) Result: 0.8939966636005579 Input : set degrees Input : sin(90) Parsed: sin(90.0) Result: 1.0 Input : sin(30) Parsed: sin(30.0) Result: 0.49999999999999994 Input : set radians Input : sin(90) Parsed: sin(90.0) Result: 0.8939966636005579 Input : set nygrader *** Syntax error: Expected radians or degrees *** The error occurred at token nygrader just after token set Input : sin(0.5*pi) Parsed: sin(0.5*pi) Result: 1.0 Input : Skriv klart metoden performset() i klassen Calculator som skall se till att grader respektive radianer används enligt exemplet ovan. Skriv också de övriga kompletteringarna av koden som krävs! Tips: Grader (g) kan räknas om till radianer (r) med formeln r = g=180 8
Svårigheten i denna uppgift ligger i att överföra informationen från metoden performset i klassen Calculator till det ställe där beräkningarna faktiskt görs dvs metoderna sin och cos i klassen Symbolic. Det bästa sättet är att förse klassen Symbolic med en flagga som anger om grader eller radianer skall användas. Denna flagga sätts med en set-metod som alltså anropas från performset. Eftersom metoderna i klassen Symbolic alla är static måste såväl flaggan som dess set-metod deklareras static I Calculator: public void performset() { tokenizer.nexttoken(); if (tokenizer.gettoken().equals("radians")) { Symbolic.setRadians(true); else if (tokenizer.gettoken().equals("degrees")) { Symbolic.setRadians(false); else { throw new SyntaxException("Expected radians or degrees "); tokenizer.nexttoken(); I Symbolic private static boolean useradians = true; public static void setradians(boolean value) { useradians = value; public static Sexpr sin(sexpr operand) { if (!operand.isconstant()) { return new Sin(operand); else if (useradians) { return new Constant(Math.sin(operand.getValue())); else { return new Constant(Math.sin(operand.getValue()*Math.PI/180)); och på motsvarande sätt i cos 9