Lösningsförslag till tentamen i EDA011/EDA017 Programmeringsteknik för F, E, I, π och N 27 maj 2008 Christian 27 maj 2008 Uppgift 1 Flera av dem jag talade med efter tentan hade blivit förskräckta när de såg att man kunde få 64 poäng på uppgift 1 anledningen till det höga poängantalet är att det blir enklare att vara rättvis med en fingranulär skala. Vi sätter alltså poäng på nästan allt man kan göra (som framgår av poängsättningarna nedan). Klassen Room Vi börjar med klassen Room. Enligt uppgiftsformuleringen skulle vi använda minst två attribut: usedcomputers och requests. I båda fallen kunde vi välja mellan listor eller vektorer, det blir som vanligt enklare om vi använder listor. I denna uppgift blir det faktiskt betydligt enkare klasserna Computer och HelpRequest överskuggade båda equals(object)- operationen, och vi kan därför använda Collection-klassernas contains(object)- och remove(object)-operationer. Utöver dessa attribut behöver vi hålla reda på rummets namn, dess kapacitet, och dessutom namnet på den övningsledare som kan vara i rummet. Det var ganska många som frågade varför det inte fanns någon close-operation i klassen Room, och förklaringen är att det från början fanns en, men att jag tog bort den för att ni skulle få lite mindre att skriva på tentan. Ytterligare ett attribut kan kännas naturligt, och det är ett boolean-värde som håller reda på om rummet är öppet för övning eller inte. Om vi använder Session-klassen som det är tänkt kommer vi aldrig att anropa någon Room-operation annat än open på ett rum som inte är öppet, men det var naturligtvis långt ifrån självklart när ni skrev klassen Room. class Room { private String roomid; private int capacity; private List<HelpRequest> requests; private List<Computer> usedcomputers; private String assistant; private boolean isopen; // om man v i l l public Room (String roomid, int capacity) { this.roomid = roomid; this.capacity = capacity; requests = new LinkedList<HelpRequest>(); usedcomputers = new LinkedList<Computer>(); public String getid() { return roomid; public int getcapacity() { return capacity; Faktum är att det hade gått ändå, eftersom vi aldrig har några kopior av våra datorer eller förfrågningar, så == fungerar utmärkt (den equals som vi ärver från Object testar med ==). I efterhand är jag inte säker på att det var så smart av mig att plocka bort denna operation, kanske hade det varit lite enklare att förstå klassen Room om man även hade skrivit en close-operation. 1
... Den som använder vektorer behöver som vanligt även heltalsräknare som håller reda på antalet element som verkligen används i de båda vektorerna. Vi skulle ha kunnat deklarera requests som en Queue, men förfrågningarna tas inte nödvändigtvis ut ur denna kö i turordning, eftersom en student kan ta bort sig själv ur kön om hon lyckas lösa sitt problem innan övningsledaren hinner komma dit så en vanlig lista känns naturligare (även om vi faktiskt kan anropa remove(object)- operationen även på en kö). open I operationen open kan vi antingen tömma redan befintliga listor (som kan skapas i konstruktorn), eller skapa nya dessutom är det rätt plats att sätta isopen till true (om vi nu använder isopen man får inget poängavdrag om man inte gör det): public void open(string assistant) { this.assistant = assistant; requests.clear(); usedcomputers.clear(); isopen = true; // om man v i l l... Poäng (max 3 p): 1 p Sparar assistentens namn. 1 p Skapar eller tömmer kö med frågor. 1 p Skapar eller tömmer lista med datorer. participate Att testa om datorn står i rätt rum ger en bonuspoäng (även om det faktiskt är lite onödigt i vårt program om vi skriver operationen login i klassen Session på rätt sätt): public boolean participate(computer computer) { if (computer.getroom() == this // bonus &&!usedcomputers.contains(computer)) { usedcomputers.add(computer); Här skulle vi även kunna testa om isopen är true innan vi lät någon delta i övningen. Om vi inte använder list-paketets contains-operation (som vi alltså kan anropa, eftersom klassen Computer överskuggade equals(object)), så kan vi istället själva skriva en hjälpoperation isused(computer): private boolean isused(computer computer) { for (Computer c : usedcomputers) { if (c.equals(computer)) { Poäng (max 6 p): 2 p Testar att vi är i rätt rum (bonus). 2 p Testar att datorn inte redan används. 1 p Lägger in datorn bland använda datorer. 1 p Rätt returvärde. leave Operationen leave skall plocka bort datorn ur både hjälp-kö och listan med använda datorer. Om vi använder listpaketet blir det väldigt enkelt: public void leave(computer computer) { hasbeenhelped(computer); usedcomputers.remove(computer); Det går alldeles utmärkt att skriva en egen hjälpoperation remove som tar bort datorn ur listan med använda datorer, eller att göra en sökning och borttagning inuti leave. Poäng (max 4): 2 p Tar bort student ur kö. 2 p Tar bort dator ur lista. Bonuspoäng innebär att vi sänker gränsen för godkänt med motsvarande antal poäng. 2
askforhelp Operationen askforhelp skulle kontrollera att datorn verkligen deltog i övningen, och att datorn inte redan stod i kö. Det enklaste sättet att testa om datorn redan står i kö är att anropa en hjälpoperation: public boolean askforhelp(computer computer) { if (!usedcomputers.contains(computer) isaskingforhelp(computer)) { requests.add(new HelpRequest(computer, Time.now())); Här kan vi skriva isaskingforhelp så här: private boolean isaskingforhelp(computer computer) { return findrequest(computer)!= null; och denna behöver i sin tur hjälpoperationen findrequest: private HelpRequest findrequest(computer computer) { for (HelpRequest request : requests) { if (request.getcomputer().equals(computer)) { return request; return null; Den som gör sökningen efter förfrågningen direkt i asforhelp får inget poängavdrag (men måste skriva lite mer programkod strax...). Poäng (max 8): 1 p Testar att datorn används. 3 p Testar att datorn inte redan i kö. 2 p Skapar ny förfrågan. 1 p Lägger frågan i kö. 1 p Rätt returvärde. hasbeenhelped När vi skriver hasbeenhelped kan vi återanvända den findrequest som vi skrev nyss: public boolean hasbeenhelped(computer computer) { return requests.remove(findrequest(computer)); Det går naturligtvis utmärkt även att göra en sökning inuti hasbeenhelped. Vi kan även ta bort en HelpRequest ur en vektor, men måste vara lite försiktiga när vi gör det. För att frågekön inte skall komma i oordning måste vi flytta elementen i vektorn så att deras inbördes ordning inte kastas om. Om vi har en vektor för att lagra fråge-kön kan vi skriva hasbeenhelped-operationen ungefär så här: public boolean hasbeenhelped(computer computer) { int index = findrequestindex(requests, computer); if (index < 0) { for (int i = index; i < nbrofrequests-1; i++) { requests[i] = requests[i+1]; nbrofrequests--; requests[nbrofrequests] = null; // inte nödvändigt där vi återigen har använt en hjälpoperation: Poäng (max 6): private int findrequestindex(helprequest[] requests, Computer computer) { for (int i = 0; i < nbrofrequests; i++) { if (requests[i].getcomputer().equals(computer)) { return i; return -1; 3 p Letar upp förfrågan. 2 p Tar bort förfrågan så att de fortfarande ligger i rätt ordning. 1 p Rätt returvärde. 3
showstatus Operationen showstatus kräver två loopar, i den första loopen går vi igenom usedcomputers, och skriver ut namnen på de användare som är inloggade i den andra skriver vi ut de datorer som står i kö för hjälp: public void showstatus() { System.out.printf("Övningssal %s\n", roomid); System.out.printf("Övningsledare: %s\n", assistant); System.out.printf("%d inloggade (%d lediga platser)\n", usedcomputers.size(), capacity-usedcomputers.size()); for (Computer computer : usedcomputers) { System.out.println(" " + computer.getuser().getname()); if (requests.size() == 0) { System.out.println("Ingen kö"); else { System.out.println("Kö:"); for (HelpRequest request : requests) { System.out.printf(" %s (%s)\n", request.getcomputer().getname(), request.gettime().tostring()); Poäng (max 11): 1 p Skriver ut rums-id/namn. 1 p Skriver ut övningsledare. 2 p Skriver ut antal inloggade/lediga platser. 3 p Skriver ut namn på samtliga inloggade (1 + 2 för loop/namn). 1 p Kollar om det finns någon kö. 3 p Skriver ut alla köande (1 + 1 + 1 för loop, id och tid). Klassen Session I klassen Session behöver vi en referens till aktuell byggnad och grupp, och dessutom en lista eller vektor med öppnade rum enklast är att använda en lista, men ni får lika många poäng om ni istället har valt en vektor och ett heltal som håller räkning på antalet rum. Attribut och konstruktor kan skrivas som: class Session { private Campus campus; private Group group; private List<Room> openedrooms; public Session (Campus campus, Group group) { this.campus = campus; this.group = group; openedrooms = new LinkedList<Room>();... Vi behöver inte någon lista eller vektor med samtliga rum eller datorer, eftersom campus-objektet håller reda på dem. Eftersom attributen var givna delas inga poäng ut här. openroom I operationen openroom måste vi leta upp det aktuella rummet (med hjälp av dess namn/identitet), och därefter uppdatera både rum och openedrooms: Poäng (max 6): public boolean openroom(string roomid, String assistant) { Room room = campus.findroom(roomid); if (room == null) { System.out.println("Det finns inget sådant rum"); if (openedrooms.contains(room)) { System.out.println("Rummet är redan öppet"); room.open(assistant); openedrooms.add(room); 1 p Letar upp rum på rätt sätt. 1 p Testar om rummet finns. 1 p Ser om rummet redan öppet. 1 p Öppnar rummet. 4
1 p Lägger in rummet bland öppna rum 1 p Rätt returvärden. login Operationen login skall göra flera saker och ger därför lite fler poäng. Det var inte alldeles självklart i vilka fall operationen showstatus skulle anropas, så vi kommer att vara snälla vid bedömningen. public boolean login(string studentid, String computername) { Computer computer = campus.findcomputer(computername); if (computer == null) { System.out.println("Det finns ingen sådan dator!"); Student student = group.findstudent(studentid); if (student == null) { System.out.println("Du tillhör inte denna kursomgång."); if (!computer.login(student)) { System.out.println("Datorn är redan upptagen"); Room room = computer.getroom(); if (openedrooms.contains(room)) { room.participate(computer); showstatus(); else { System.out.println("Vi har inte övning i din sal nu."); showstatus(); Den som inte vill testa returvärdet på login-operationen i klassen Computer får gärna istället skriva något i stil med: if (!computer.getuser()!= null) { System.out.println("Datorn är redan upptagen"); computer.login(student); Flera av er ville lägga till en operation isopen() i klassen Room, och det är naturligtvis en god idé vi skulle i så fall kunna kontrollera om rummet var öppet utan att göra en sökning i openedrooms. Ni får full poäng oavsett om ni söker i openedrooms, eller om ni anropar en isopen()-operation. Poäng (max 10): 1 p Letar upp rätt dator. 1 p Testar om datorn finns. 1 p Testar om datorn är upptagen. 1 p Letar upp rätt student. 1 p Testar att studenten finns. 1 p Plockar fram rätt rum. 1 p Testar att rummet är öppet. 1 p Loggar in i rummet. 1 p Skriver ut status om ingen övning i rummet. 1 p Rätt returvärden. logout I logout kan man testa om datorn finns, och om någon är inloggad på datorn. Poäng (max 5): public void logout(string computername) { Computer computer = campus.findcomputer(computername); if (computer == null) { System.out.println("Felaktigt rumsnamn!"); return; if (computer.getuser() == null) { System.out.println("Ingen är inloggad på denna dator"); return; computer.getroom().leave(computer); computer.logout(); 1 p Letar upp rätt dator. Som jag egentligen hade tänkt att ni skulle göra inte för att det är smart, utan för att det kräver att ni implementerar en sökning. 5
1 p Testar om datorn finns. 1 p Plockar fram rätt rum. 1 p Gör logout i rum. 1 p Gör logout på dator. showstatus Operationen showstatus kan delegera det mesta av sitt arbete till showstatusoperationen i klassen Room: public void showstatus() { System.out.printf("Klockan är %s\n", Time.now()); if (openedrooms.size() == 0) { System.out.println("Just nu har vi ingen öppen sal"); Poäng (max 5): else { System.out.println("Vi har övning i följande salar:"); for (Room room : openedrooms) { room.showstatus(); 1 p Skriver ut rätt tid. 1 p Testar om det finns någon öppen sal. 3 p Skriver ut status för samtliga öppna salar Uppgift 2 Man kan lösa denna uppgift på otroligt många sätt (vilket gör att den antagligen blir rätt jobbig att rätta...). Det kanske mest grundläggande problemet är att plocka ut enstaka siffror ur ett tal, och man kan tänka sig många olika lösningar enklast är heltalsdivision och %- operatorn, men man kan exempelvis omvandla till en sträng och plocka ut enstaka tecken, som i sin tur kan omvandlas till ett ensiffrigt tal. Som ofta är det en god idé att skriva en funktion som löser problemet åt oss: int digit(int value, int digitnbr) { // plocka ut s i f f r a digitnbr (från höger) ur t a let value En lösning som baseras på heltalsdivision kan utnyttja att vi får den n:te siffran bakifrån i talet v om vi dividerar v med 10 n 1, och därefter tar resten vid division med 10: int digit(int value, int digitnbr) { return (value/tenraisedto(digitnbr-1)) % 10; där vi behöver ytterligare en hjälpoperation: int tenraisedto(int n) { return (int) Math.round(Math.exp(n*Math.log(10))); Vi kan för övrigt inte skriva 10^n för att få 10 n det går visserligen att kompilera, men ^ betyder inte upphöjt i i Java (detta ger dock inga avdrag). Om vi istället vill gå omvägen via strängar när vi skall plocka ut en siffra i vårt tal kan vi skriva: int digit(integer value, int digitnbr) { String str = value.tostring(); int pos = str.length() - digitnbr; if (pos < 0) { return 0; return str.charat(pos) - 0 ; Här måste vi deklarera parametern som ett Integer-objekt om vi vill kunna anropa tostring-operationen (dock inga poängavdrag för den som gör tostring på ett int-värde, eftersom man faktiskt får göra det i exempelvis Ruby, och det inte ingår i kursen att veta precis när autoboxingen gör att man slipper typa om sina värden). Ett alternativ är att låta value vara deklarerad som ett int, och sätta: String str = "" + value; I lösningsförslaget nedan använder jag inte någon funktion som plockar ut en enstaka siffra, istället bygger jag upp en faktor som växer med en faktor 10 för varje ny omgång vi kör, och dividerar ner talen direkt med detta värde. Detta är lite mer effektivt, men spelar ingen roll för poängsättningen på uppgiften. Ett annat problem är att välja datastruktur för att hålla reda på det som jag i upp- 6
giften kallar l-listorna. I problemtexten visar jag hur man kan skapa en vektor med köer (det är tyvärr ganska knepigt, eftersom vektorer med generiska typer är något av ett hack i Java) man kan även använda en lista av köer. Deklarationen av en vektor med köer fanns i texten, om vi vill ha en lista med köer istället kan vi skriva: List<Queue<Integer>> queues = new ArrayList<Queue<Integer>>(); Egentligen är det rent numeriskt inte särskilt smart att använda basen 10 när vi implementerar RADIX-SORT men det gör det mycket enklare att illustrera algoritmen. En bättre bas är 16, eller kanske 256 (vi får göra en avvägning mellan antalet iterationer och antalet listor som krävs). class RadixSorter { private final int base = 10; private Queue<Integer>[] queues = (LinkedList<Integer>[]) new LinkedList[base]; public RadixSorter () { for (int i = 0; i < base; i++) { queues[i] = new LinkedList<Integer>(); public void sort(list<integer> values) { int biggest = findbiggest(values); int factor = 1; while (biggest > 0) { orderbydigit(values, factor); factor *= base; biggest /= base; private int findbiggest(list<integer> values) { int biggest = Integer.MIN_VALUE; for (int value : values) { biggest = Math.max(value, biggest); return biggest; Jag trodde att det kanske skulle vara enklare för er att förstå idén med en vektor med köer. Om vi inte möjligen får tal med fler siffror att sortera. Som våra Comparator-objekt gör. private void orderbydigit(list<integer> values, int factor) { while (!values.isempty()) { int value = values.remove(0); // se kommentar nedan queues[(value/factor) % base].add(value); for (int i = 0; i < base; i++) { while (!queues[i].isempty()) { values.add(queues[i].remove()); En sak som är lurig är att vi här använder remove(int)-operationen på en List<Integer> det finns ibland risk att autoboxingen gör att vi tar bort fel värden av misstag (den skulle här ha kunnat få för sig att plocka bort värdet 0 istället för värdet på plats 0). I detta fall fungerar det precis som vi vill (det är värdet på plats 0 som tas bort). Man får poäng för följande (max 30): 2 p Korrekt yttre loop. 3 p Går inte fler varv än nödvändigt. 6 p Bestämmer index för rätt l-lista i varje varv. 4 p Flyttar värden till rätt l-lista i varje varv. 7 p Slår samman l-listorna till en ny lista på rätt sätt. 8 p Har förstått algoritmen/lämplig nedbrytning i underprogram/ snygg kod. Det är intressant att räkna på tidsåtgången för RADIX-SORT, om vi ökar antalet värden som vi skall sortera så ökar tidsåtgången linjärt (tiden att ta ut ur och sätta in i våra listor är direkt proportionellt mot antalet element i listorna, men vi behöver inte köra fler varv i den yttre loopen ) den är alltså en O(n)-operation. Detta kan jämföras med insättningssortering och urvalssortering, som båda är O(n 2 ). I själva verket finns det en teoretisk undre gräns för hur snabb en vanlig sorteringsalgoritm kan bli, och man kan intuitivt få en känsla för denna gräns genom följande resonemang. Vanliga sorteringsalgoritmer bygger på att vi jämför värden med varandra. Om vi har en samling med n värden a 1, a 2,..., a n, så kan de ordnas på n! olika sätt i 7
hälften av dessa permutationer kommer värdet a 1 att hamna före a 2. Om vi testar om a 1 > a 2 och upptäcker att det är sant så kan vi i princip utesluta alla de permutationer i vilka a 1 ligger före a 2 om vi vill sortera talen i växande ordning. På samma sätt kan vi efter varje ny test mellan två värden utesluta ungefär hälften av återstående permutationer. Detta är ingen praktiskt genomförbar sorteringsalgoritm, men den antyder att en jämförelse mellan två värden inte kan elminiera mer än ungefär hälften av de möjliga permutationer som finns. Det minsta antalet jämförelser vi måste göra innan vi har den permutation som innehåller våra värden i rätt ordning blir log 2 n! (detta motsvarar antalet jämförelser vi måste göra vid binärsökning, med skillnaden att vi nu har n! olika permutationer att söka bland). Stirlings formel säger att n! n n 2πn, e och om vi logaritmerar detta uttryck ser vi att det minsta antalet jämförelser vi måste göra är ungefär log( 2πn n n e n ). Den dominerande faktorn inuti logaritm-uttrycket är n n, och antalet jämförelser blir därför O(n log n). Detta är en teoretisk undre gräns för sorteringsalgorimer som baseras på jämförelse, och det finns flera algoritmer som faktiskt når ner till denna gräns (bland andra HEAP-SORT som vi hade på föregående tenta, och QUICK-SORT, som beskrivs i kompendiet). Det kan därför verka egendomligt att RADIX-SORT är O(n), vilket är bättre än den teoretiska undre gränsen för sorteringsalgoritmer men det beror på att den faktiskt inte gör några jämförelser alls! Istället utnyttjar den annan information om de värden som skall sorteras, vilket gör att den inte är lika generell som exempelvis HEAP-SORT och QUICK-SORT (vi kan dock använda den till att sortera exempelvis strängar, vilket flera av er som skrev tentan insåg). Det visar sig dessutom att konstanterna framför n log n-uttrycken i både HEAP-SORT och QUICK-SORT är så små att de i praktiken är snabbare än RADIX-SORT i de flesta fall. 8