LUNDS TEKNISKA HÖGSKOLA Institutionen för datavetenskap EDA690 Algoritmer och datastrukturer Programmeringsövningar, Algoritmer och datastrukturer Programmeringsövningarna i kursen Algoritmer och datastrukturer (EDA690) ger exempel på tillämpningar av det material som behandlas under kursen och ger träning i implementation av algoritmer i Java. Sex av övningarna (nr 2+3, 5+6 och 7+8) är datorövningar medan de övriga tre är ”papper- och penna”- övningar. Du kommer i samband med gruppindelningen att få uppgift om i vilken datorsal respektive lärosal dina övningar äger rum. Övningarna är obligatoriska, vilket innebär att du måste bli godkänd på uppgifterna under ordinarie övningstid. Om du skulle vara sjuk vid något övningstillfälle måste du meddela detta till kursansvarig. Du får då göra uppgifterna på egen hand och redovisa dem vid vid nästa övningstillfälle. Övningarna skall genomföras i grupper om två personer. I samband med anmälan till övningarna får du möjlighet att ange vem du vill samarbeta med. Övningarna kräver en hel del förberedelser. Före varje övning måste du studera de avsnitt som anges som förberedelse efter rubriken till varje övning. Med läroboken avses ”Data Structures & Problem Solving Using Java, Third Edition” av Mark Allen Weiss. Du måste också läsa igenom alla uppgifterna och försöka lösa dem. Det är inget krav att du kommer med helt färdiga lösningar, men du måste ha förberett dig så att du själv kan bedöma att det finns en rimlig möjlighet att bli klar under övningen. Normalt finns det två övningsledare och 1618 teknologer i varje övningsgrupp. Om du har problem med övningarna kan du före övningen vända dig till kursansvarig eller till den lärare som håller seminarierna på kursen. Du kan också under övningen be någon av dina övningsledare om hjälp. I samband med att du blir godkänd får du institutionens lösningsförslag till respektive övning (gäller enbart papper- och pennaövningarna). För att få tentera krävs det att du är godkänd på samtliga övningar. Kursen inrapporteras i två delar i Ladok. Den ena delen består av övningar och inlämningsuppgifter (2p) och den andra delen av tentamen (3p). Se till att din övningsledare noterar godkänd övning på nästa sida! Algoritmer och datastrukturer, godkända övningar Skriv ditt namn och din namnteckning nedan: Namn: ............................................................................ Namnteckning: .............................................................. Datum Godkänd övning 1 2 3 4 5 6 7 8 9 Övningsledarens namnteckning 1 Övning 1 (papper- och pennaövning) Innehåll:: Algoritmanalys: beräkning av tidskomplexitet. Användning och implementation av ArrayList. Förkunskaper: Kapitel 5 i läroboken. Läs också om klassen ArrayList på hemsidan http://www.cs.lth.se/EDA027/HT/ följ länken Kursmaterial, utdelat) __________________________________________________________________________________________ 1. Ange tidskomplexiteten (ordo-notation) för vart och ett av följande fyra programavsnitt. Motivera svaren. /* a */ for (int i=0; i<n; i++) { for (int j=0; j<n; j++) { sum++; } } /* b */ for (int i=0; i<n; i++) { sum++; } for (int j=0; j<n; j++) { sum++; } /* c */ for (int i=0; i<n; i++) { for (int j=0; j<n*n; j++) { sum++; } } /* d */ for (int i=0; i<n; i++) { for (int j=0; j<i; j++) { sum++; } } 2. Ett visst program kräver en minut för att klara av en indatamängd som svarar mot n = 1000. Om man investerar i en 100 gånger snabbare dator kan programmet naturligtvis klara av större problem på samma tid. Vilken mängd indata kan programmet klara av på den nya datorn om programmets tidskomplexitet T(n) är: a) T(n) = n b) T(n) = n2 c) T(n) = n3 d) T(n) = 10n 2 3. För att ta reda på hur många olika element det finns i en lista av typen ArrayList kan följande metod användas: public int nbrOfDifferent(ArrayList myList) { ArrayList newList = new ArrayList(); int sz = myList.size(); for (int i = 0; i < sz ; i++) { Object act = myList.get(i); if (!newList.contains(act)) { newList.add(act); } } return newList.size(); } Vad blir tidskomplexiteten i värsta fall för denna algoritm? Du kan utgå från att för en lista av typen ArrayList med n element så har operationen contains tidskomplexiteten n i värsta fall och add(Object elem)har konstant tidskomplexitet1. 4. I denna uppgift skall två av metoderna i klassen SimpleArrayList implementeras och analyseras med avseende på sin tidskomplexitet. I klassen representeras listan av en vektor (elementData). Man har dessutom ett attribut (size) som håller reda på hur många element som finns insatta i listan. Nedan skissas de delar av klassen som vi arbetar med i denna uppgift: public class SimpleArrayList { private Object[] elementData;// vektorn med element private int size; // antalet element public SimpleArrayList() { elementData = new Object[10]; size = 0; } /** Sätter in element på plats index i listan och skiftar det gamla elementet och alla element i positioner till höger om detta ett steg högerut. Om index är < 0 eller index > size kastas IndexOutOfBoundsException.*/ public void add(int index, Object element) {} /** Undersöker om objekt som matchar parametern element finns i listan. För jämförelse mellan objekt används metoden equals */ public boolean contains(Object element) {} /* Privat hjälpmetod som anropas när vektorn elementData blivit för liten. Metoden skapar en vektor som är dubbelt så stor som den gamla. Attributet elementData refererar efter anrop till den nya vektorn och alla element i den gamla vektorn har då flyttats till motsvarande positioner i den nya. */ private void doubleArray() {} } 1. Här står det att man får förutsätta att operationen add(Object elem) har tidskomplexiteten O(1) i klassen ArrayList. Detta kan förefalla felaktigt eftersom vektorn som används i implementationen kan bli full av element och att man därför i operationen add vid behov måste skapa en ny större vektor och flytta alla insatta element dit. Detta betyder att värstafallet för add egentligen är O(n) där n är antalet element som finns i vektorn. Dock utförs detta internt i klassen ArrayList på ett sådant sätt att operationen add i medeltal får tidskomplexitet O(1). För att garantera att add(Object elem) skall bli O(1) även i värsta fall så skulle vi kunna använda en annan konstruktor i klassen ArrayList när vi skapar newList. Vi kan ändra den första programsatsen i metoden till: ArrayList newList = new ArrayList(myList.size()); newList får då lika många platser som antalet element i myList. Därför kommer någon utökning av antal platser aldrig att behöva ske och operationen add blir O(1) för samtliga anrop som görs i metoden nbrOfDifferent. 3 a) Implementera metoderna add och contains. Observera att specifikationen av add innebär att de enda tillåtna positionerna för insättning är 0 .. size. Man får alltså inte skapa några ”hål” i listan vid insättning. Du får använda metoden doubleArray utan att implementera den. b) Vad blir tidskomplexiteten i värsta fall för metoderna add och contains? c) En programmerare använder klassen SimpleArrayList för att hålla reda på ett antal objekt av typen Person: public class Person { private String name; public Person(String name) { this.name = name; } public boolean equals(Person rhs) { return name.compareTo(rhs.name) == 0; } // övriga metoder i klassen } Följande programrader exekveras: SimpleArrayList personList = new SimpleArrayList(); Person p = new Person(”Kalle”); personList.add(0,p); ... if (personList.contains(new Person(”Kalle”))) { System.out.println(”Kalle found”) } else { System.out.println(”Kalle not found”); } Till sin förvåning finner programmeraren att utskriften blir ”Kalle not found” trots att raderna som markerats med ... inte innehåller några borttagningar ur listan. Förklara vari felet består och korrigera det. 4 Övning 2+3 (datorövning) Innehåll: Köer. Implementation av kö med hjälp av (generisk) länkad datastruktur. Testning av klasser med JUnit. Förkunskaper: Läroboken avsnitt 6.1-6.4, kap 15 och 16.1-16.3. OH-bilder om Java 5.0. __________________________________________________________________________________________ Inled alla datorövningar med att utföra kommandot z:\ad\init så att du får korrekt sökväg. 1. Läs igenom utdelat PM om testning med JUnit. 2. I denna uppgift skall vi utgå från följande generiska interface för den abstrakta datatypen kö: public interface Queue<E> { void enqueue(E x) ; // E getFront(); // E dequeue() ; // boolean isEmpty(); // void makeEmpty(); // } Lägg in x i kön Returnera äldsta elementet i kön. Returnera och tag bort äldsta elementet. Undersök om kön är tom Töm kön Metoderna getFront och dequeue skall generera NoSuchElementException när kön är tom. I kapitel 16 diskuteras två implementationer av köer, en som baseras på vektor (avsnitt 16.1.2), en baserad på en länkad struktur (avsnitt 16.2.2). I den senare implementationen representeras kön av två referenser, en till den första noden i listan och en till den sista. I avsnitt 16.3 diskuteras för- och nackdelar hos de båda implementationerna. I denna uppgift skall Queue<E> implementeras på ett tredje sätt. En enkellänkad lista skall användas för elementen i kön, men kön representeras nu enbart av en referens (last) till det sist insatta elementet. Listan skall vara cirkulär, dvs i det sista elementet är inte referensen (next) till efterföljaren null utan i stället refererar den till det äldsta (första) elementet i listan. I en tom kö har attributet last värdet null. Se figur 1. I filen z:\ad\adovn\generic_fifo\FifoQueue.java finns ett skelett till en klass som implementerar kön på detta sätt. En skiss av filens innehåll finns här: : import java.util.NoSuchElementException; import se.lth.cs.ad.queue.Queue; // för att få tillgång till Queue-interfacet public class FifoQueue<E> implements Queue<E> { private ListNode last;// Refererar till sista noden i listan // alla metodrubriker enligt ovan /** (Inre) klass som beskriver en nod i en enkellänkad lista. */ class ListNode { E element; // nodens innehåll ListNode next; // referens till nästa nod i listan /** Konstruktor: skapar en nod med innehållet x och next = null */ ListNode(E x) { element = x; next = null; } } } 5 last Cirkulär lista att använda för att implementera kö. Next-referenser och referenser till i listan insatta objekt är utritade. Noden längst till vänster innehåller det äldsta elementet och noden som last refererar till innehåller det senast insatta elementet. last Kö med ett enda element Fig 1: Representation av kö med enkellänkad cirkulär lista. Kopiera filen till egen katalog. Kopiera även filen z:\ad\adovn\generic_fifo\TestFifoQueue.java till din egen katalog. Titta på filens innehåll. Den innehåller en rad testmetoder avsedda att testa användning av FifoQueue i olika situationer. Dels väldigt enkla dels mer komplicerade fall. Det är viktigt att man speciellt tänker på de specialfall som kan uppträda. För klasser som hanterar samlingar av element (som i detta fall) är ett specialfall den tomma samlingen. Något test bör undersöka om man klarar att tömma en kö helt och om isEmpty därefter ger det förväntade resultatet true. En rekommendation är att skriva testfall innan man implementerar det som skall testas. Man blir då medveten om vilka specialfall som finns och blir uppmärksam på att implementationen sedan tar hänsyn till dessa. Implementera konstruktorn samt metoderna enqueue, getFront, dequeue, isEmpty och makeEmpty i klassen FifoQueue. (Operationen append skall inte implementeras förrän i nästa deluppgift.) Kompilera sedan FifoQueue och TestFifoQueue och kör testprogrammet genom . Om någon test inte går igenom får du ”rött ljus” samt felmeddelanden om vilka test som misslyckades. Titta på de felmeddelanden som ges och försök åtgärda felen i FifoQueue. Testa på nytt och fortsätt tills alla test går igenom. Lägg eventuellt till egna testmetoder i klassen TestFifoQueue om du tycker att något saknas. 3. Ibland behöver man slå samman (konkatenera) två köer q1 och q2 till en kö bestående av alla element i q1 följda av alla element i q2. Man kan successivt ta ut elementen ur q2 med dequeue-metoden och sätta in dem i q1 med enqueue-metoden. Om det finns n element i q2 anropas alltså båda metoderna n gånger. Båda har tidskomplexitet O(1) och algoritmen blir därför O(n). Implementationen kan göras effektivare om den i stället utförs i en metod i klassen FifoQueue. Då kan man utnyttja den interna datastrukturen hos FifoQueue genom att attribut som inte är tillgängliga utanför klassen nu kan användas. Lägg till följande metod i FifoQueue: /** Slå samman denna kö med kön q. Efter anrop är q tom */ public void append(FifoQueue<E> q); Kopiera filen z:\ad\adovn\generic_fifo\TestAppendFifoQueue.java till din egen katalog. Filen innehåller fyra metoder avsedda att testa append-metoden. 6 Implementera metoden append i klassen FifoQueue. Kompilera FifoQueue samt TestAppendFifoQueue, kör testen och korrigera eventuella fel i append-metoden tills alla test lyckas. 4. Det finns en sorteringsmetod, positionssortering (eng. Radix sort) som sorterar en mängd icke-negativa heltal med ett visst känt maximalt antal siffror mycket snabbt genom att använda köer. (Egentligen är metoden inte begränsad till att sortera tal, den klarar även följder av annat slag t ex strängar.) Algoritmen använder sig av 10 köer, en för varje siffra 0..9, samt en kö som från början innehåller de osorterade talen och som i slutet av algoritmen innehåller talen sorterade i växande ordning. Den arbetar enligt följande, där vi använder FifoQueue-klassen för att representera köerna: Placera talen i en kö numberQ av typ FifoQueue<Integer> Skapa en vektor subQ med 10 tomma köer // en kö för varje siffra for (int i=1; i<=d; i++) {// d är antalet siffror i det längsta talet så länge numberQ inte är tom tag ut det första talet ur numberQ; j = i:e siffran bakifrån i talet; placera talet i kön subQ[j]; for (int j=0; j<10; j++) // konkatenera (den nu tomma) numberQ numberQ.append(subQ[j]); // med de 10 köerna i subQ } I algoritmen distribuerar man först talen på de tio köerna med avseende på den sista siffran, sedan med avseende på den näst sista, osv. Efter varje sådan distribution konkateneras de tio köerna. Försök övertyga dig om att algoritmen är korrekt genom att studera exemplet som finns på sista sidan i denna övning. Kopiera sedan filen z:\ad\adovn\generic_fifo\RadixSort.java till din egen katalog och implementera där metoden radixSort, som har följande rubrik: /** Sorterar talen i vektorn a med positionssortering. d anger maximalt antal siffror i talen.*/ public static void radixSort(int[] a, int d); Tips: Siffror ur ett heltal kan extraheras med hjälp av divisionsoperatorn / och modulo-operatorn %. När ett heltal a divideras med ett annat heltal b med operatorn / sker heltalsdivision vilket innebär att decimalerna stryks efter divisionen. Om a är ett heltal blir a/10 därför det tal som består av alla siffror i a utom den sista. Operatorn % står för rest vid heltalsdivision. Sista siffran i ett tal a får man därför ur a%10. Näst sista siffran i a är den sista i talet a/10 dvs vi får den genom att bilda a/10%10. Allmänt gäller att i-te siffran från slutet kan erhållas ur a/10i–1%10. Filen z:\ad\adovn\generic_fifo\TestRadixSort.java innehåller några test för sorteringsmetoden. Kopiera den till din katalog. Kompilera RadixSort och TestRadixSort och kör testprogrammet. Korrigera tills alla test går igenom. Lägg eventuellt till egna test. 7 Anm 1: I filen RadixSort.java finns följande programsatser för att skapa de tio köerna i subQ: FifoQueue<Integer>[] subQ = (FifoQueue<Integer>[]) new FifoQueue[10]; for (int i = 0; i < 10; i++) subQ[i] = new FifoQueue<Integer>(); Den första satsen skapar en vektor av typ FifoQueue<Integer>[]. I for-satsen initieras de tio elementen i vektorn som tomma köer av typen FifoQueue<Integer>. Man förväntar sig kanske att den första programsatsen skulle kunna skrivas enklare: FifoQueue<Integer>[] subQ = new FifoQueue<Integer>[10]; //fel! Det är emellertid inte tillåtet att i Java 5.0 skapa vektorer vars element är av en parametriserad typ. För att kunna skapa vår vektor tvingas vi därför att först skapa en vektor FifoQueue[] (utan att ange typen av element) och sedan göra typkonvertering till den typ vi önskar. Detta gör att vi kommer att få en varning under kompileringen: RadixSort.java uses unchecked or unsafe operations. Anm 2: Om du vill prova på möjligheten att kombinera test av flera klasser till en testsvit så kan du kopiera filen z:\ad\adovn\generic_fifo\TestAll.java. Titta på innehållet. I TestAll läggs de tre testklasserna TestFifoQueue, TestAppendFifoQueue och TestRadixSort in i samma testsvit. Om du kompilerar TestAll och kör kommer samtliga testfall i de tre testklasserna att köras. 8 Exempel på användning av RadixSort (bilaga till övning 2, uppgift 4) Nedan ges ett exempel på hur en följd av heltal sorteras med metoden RadixSort. Talen som sorteras är {721, 10, 51, 122, 674, 96, 109, 44, 236, 178, 1, 567, 674}. Vi börjar med att distribuera talen med avseende på den sista siffran på 10 köer numrerade 0,1,..9. Resultatet av detta blir att köerna får följande innehåll: 0: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10 721 51 1 122 674 44 674 96 236 567 178 109 Därefter konkateneras dessa köer (i ordningen 0, 1,.. 9) och vi får den resulterande kön: 10 721 51 1 122 67 44 674 96 236 567 178 109 Det vi nu åstadkommit är att talen är sorterade med avseende på sin sista siffra. Talen i denna kö distribueras nu med avseende på den andra siffran från slutet på 10 köer på samma sätt. Observera att ensiffriga tal då hamnar i kö nummer 0. Den andra siffran från slutet i dessa motsvarar alltså en inledande nolla i talet, vilket också ges av den formel för att räkna fram i:e siffran från slutet som anges i uppgiften: 0: 1: 2: 3: 4: 5: 6: 7: 8: 9: 1 109 10 721 122 236 44 51 567 674 674 178 96 Dessa köer konkateneras nu med resultatet: 1 109 10 721 122 236 44 51 567 674 674 178 96 Lägg märke till att om vi enbart betraktar de tal som består av de två sista siffrorna så utgör dessa nu en sorterad följd. Eftersom det maximala antalet siffror i talen är tre behövs ett sista tredje pass i vilket talen igen distribueras, nu med avseende på den tredje siffran från slutet. (Nu hamnar alla tal som har en eller två siffror i kö nummer 0): 0: 1: 2: 3: 4: 5: 6: 7: 8: 9: 1 10 44 51 96 109 122 178 236 567 674 674 721 Efter konkatenering får vi nu det sorterade slutresultatet: 1 10 44 51 96 109 122 178 236 567 674 674 721 9 Övning 4 (papper- och pennaövning) Innehåll: Binära träd, rekursion, binära sökträd. Förkunskaper: Läroboken avsnitt 7.1-7.3, 18.1-18.3, 19.1. __________________________________________________________________________________________ 1. Man kan definiera potensfunktionen xn (n heltal>=0) rekursivt enligt: x0 = 1 xn = x*xn-1 a) Skriv en rekursiv metod public double power(double x, int n); som beräknar xn enligt denna definition. b) Beräkningen kan göras effektivare om man i stället gör följande definition: x0 = 1 xn = (xn/2)2 xn = x*(xn/2)2 om n>0 och jämnt om n>0 och udda Gör en ny implementation av metoden i a enligt denna definition. c) Vad blir tidskomplexiteten för beräkning av xn med implementationerna i a resp. b? 2. En palindrom är ett ord eller en mening som kan läsas på samma sätt både fram- och baklänges. Exempel: DALLASSALLAD, NITALARBRALATIN. a) Skriv en metod med rubriken public boolean isPalindrome(String str); som med rekursiv teknik undersöker om en sträng är en palindrom. Ledning: Låt isPalindrome anropa en annan rekursiv metod som förutom strängen har två heltalsparametrar som anger index för första och sista tecken i den delsträng som undersöks. b) Vad blir tidskomplexiteten i värsta fall för algoritmen i a? 3. Följande rekursiva algoritm förelås för att undersöka om de size första talen i en vektor a med heltal är sorterad i växande ordning. Vi skall alltså undersöka talen på platserna 0..size-1 i vektorn: public boolean isSorted(int[] a, int size){ if (size == 1) { return true; } else if (size == 2) { if (a[1] < a[0]) { return false; } else { return true; } } else { if (a[size-1] < a[size-2]) { return false; } else { return isSorted(a,size-1); } } } Kommentera lösningsförslaget. Är det korrekt? I så fall: går det att förbättra? 10 4. I denna uppgift används följande klasser för att representera binära träd där noderna innehåller heltal: class BinaryNode { int value; BinaryNode left; BinaryNode right; } // noderna innehåller heltal // refererar till vänster barn // refererar till höger barn class BinaryTree { protected BinaryNode root; // refererar till rot-noden // metoder } Implementera i klassen BinaryTree en metod public int levelSum(); som beräknar trädets nivåsumma, vilket definieras som summan av (nodens nivå)*(heltalsvärde i noden) över alla noder i trädet. Roten har nivå ett och ett barn har nivå ett mer än sin förälder. Trädet i figuren nedan har nivåsumman 1*2+2*5+3*1+2*7+3*4+4*3 = 53. Metoden imple-menteras enklast rekursivt med hjälp av en metod i klassen BinaryNode som beräknar motsvarande summa för det delträd i vilket noden är rot. Denna senare metod kan ha nodens nivå som parameter. 2 5 7 1 4 3 5. För att kunna lösa uppgiften behöver man känna till definitionen av ett binärt sökträd (BST): Ett binärt träd där elementen i noderna går att jämföra med varandra t ex är av typen Comparable och där det för varje nod gäller att dess element är större än samtliga element i dess vänstra underträd och mindre än samtliga element i dess högra underträd. Dubbletter tillåts normalt ej. I läroboken kap 19.1 finns implementerat en klass för hantering av binära sökträd. Tillfoga en metod public void printPart(AnyType min, AnyType max); som med rekursiv teknik skriver ut innehållet i de noder som är större än eller lika med min och mindre än eller lika med max i växande ordning. Gör det utan att besöka onödigt många noder i trädet. Låt din metod anropa en annan metod som har ytterligare en parameter (roten i det underträd som metoden skall skriva ut berörda delar av). Ledning: Genomgång av ett binärt sökträd i inorder (först vänster underträd, sedan roten och till sist höger underträd) ger elementen i växande ordning. Man bör för effektivitetens skull i denna uppgift inte alltid gå igenom hela trädet. Om vi t ex befinner oss vid en nod vars element är mindre än min är det inte meningsfullt att gå vidare ned i dess vänstra underträd. 11 Övning 5+6 (datorövning) Innehåll: Binära sökträd, rekursion. Förkunskaper: Läroboken 19.1-19.4. __________________________________________________________________________________________ Inled alla datorövningar med att utföra kommandot z:\ad\init så att du får korrekt sökväg. Under denna datorövning skall några metoder i en enkel klass för binära sökträd implementeras och dessutom skall en klass för visualisering av trädet skrivas. Trädets noder beskrivs av en färdigimplementerad klass BinaryNode med följande publika metoder: BinaryNode(E e); E getElement(); BinaryNode<E> getLeft(); BinaryNode<E> getRight(); // // // // Skapar en nod med innehållet e Returnerar innehållet i noden Returnerar referens till vänster barn Returnerar referens till höger barn Klassen finns i z:\ad\adovn\generic_bst\BinaryNode.java. Kopiera den till din egen katalog. Trädet beskrivs av en klass BinarySearchTree med följande publika metoder implementerade: BinarySearchTree(); BinaryNode getRoot(); int height(); void printTree(); // // // // Konstruktor; skapar ett tomt träd Returnerar en referens till roten Beräknar trädets höjd Skriver ut innehållet i inorder Klassen finns i z:\ad\adovn\generic_bst\BinarySearchTree.java. Kopiera även denna fil till din egen katalog. 1. Först skall en insättningsoperation tillfogas i trädklassen. Den skall ha följande signatur: public void insert(E x); Trädet skall tillåta dubbletter. Om det när man skall sätta in objektet x i trädet redan finns en nod n som innehåller ett element y för vilket gäller y.equals(x) (eller ekvivalent y.compareTo(x) == 0) så skall man välja att sätta in x i n:s högra underträd. D.v.s. följande algoritm används: om x.compareTo(n.element) < 0 sätt in x i n:s vänstra underträd annars sätt in x i n:s högra underträd Du kan för att kontrollera resultatet till en början skriva en main-metod som sätter in några element (t ex Integer-objekt) och sedan använder metoden printTree() som finns färdigskriven enligt ovan. Du får då elementen utskrivna i växande ordning om du gjort rätt. 2. I denna uppgift skall en klass för visualisering av trädet skrivas. För själva ritandet kommer en helt färdigskriven klass DrawingArea att användas. Denna klass finns i paketet se.lth.cs.ad.drawing och dess viktigaste publika metoder är: /** Skapa en rityta med önskad titel, storlek och bakgrundsfärg. */ DrawingArea(String title, int width, int heigth, Color bgColor) /** Rita en cirkel fyllt med färgen col, centrum i (x,y) och diameter size */ void fillCircle(Color col, int x, int y, int size) /** Rita en (svart) linje mellan (x1,y1) och (x2,y2) */ void drawLine(int x1, int y1, int x2, int y2); 12 /** Skriv strängen text med start i positionen (x,y). */ void drawString(String text, int x, int y); /** Fyll hela ritytan med bakgrundsfärgen */ void erase(); /** Fördröj anropande program ms millisekunder. Ett lämpligt sätt att pausa mellan ritoperationer om man t ex vill se mellanresultat */ void wait(int ms); Er uppgift är att utnyttja klassen DrawingArea för att implementera en klass BSTVisualizer med följande specifikation: /** Skapa en rityta med angiven titel och storlek på vilken drawTree kan rita. Använd vit bakgrund. */ public BSTVisualizer(String title, int width, int height); /** Rita trädet bst på ritytan enligt beskrivning nedan. */ public void drawTree(BinarySearchTree bst); Kopiera filen z:\ad\adovn\generic_bst\BSTVisualizer.java till din egen katalog. I filen finns metodrubriker för konstruktorn och metoden drawTree. Det finns också ett antal konstanter definierade som kan användas vid uppritningen samt ett par andra enkla hjälpmetoder som nämns i slutet av denna uppgift. Metoden drawTree skall rita trädet enligt principer som följer: För ett träd som på alla sina nivåer har så många noder som det är möjligt kan man lätt bestämma koordinater för noderna så att trädet ritas symmetriskt och så att det för alla undertäd gäller att noderna i vänster underträd ritas till vänster om roten som i sin tur ritas till vänster om noderna i det högra underträdet. Betrakta följande exempel på visualisering av ett sådant binärt träd: 0 1 2 3 4 5 6 7 0 x-axel 1 2 3 y-axel Fig 1: Layout för ett träd med höjden 2 och maximalt antal noder på varje nivå. Nivåer i trädet definieras på följande sätt • Roten har nivå ett. • Barn till en nod med nivå k har nivån k+1. Lägg märke till följande: • Nodernas y-koordinat bestäms av deras nivå i trädet. • Nodernas x-koordinat bestäms av deras nummer i en uppräkning av noderna i inorder. För binära träd med höjden h och med maximalt antal noder på alla sina nivåer gäller följande samband (observera att vi här använder bokens definition av höjd d.v.s. ett träd som bara består av roten har höjden 0 och ett tomt träd anses ha höjden –1): • Antalet noder är 2h+1 – 1. Vänster repektive höger underträd har 2h – 1 noder. Om vi betecknar antalet noder i trädet med n så har alltså underträden (n – 1)/2 noder vardera. Av dessa samband följer följande: • • • Roten i hela trädet har inordernummer 2h (ty det finns 2h – 1 noder i dess vänstra underträd som kommer före vid inorderuppräkning. Om roten i ett underträd med höjden k har inordernummer inbr så har roten i dess vänstra underträd nummer inbr–2k-1 och roten i det högra underträdet har nummer inbr+2k-1 13 Ex: I följande träd med höjden 3 visas inordernummer för alla noder: 8 4 2 1 12 6 3 5 10 7 9 14 11 13 15 Fig 2: Inordernumrering av noderna i ett träd med höjden 3 och maximalt antal noder på alla nivåer. Dessa samband kan användas för att implementera en rekursiv metod för att rita trädet. Om trädet inte har maximalt antal noder på alla nivåer kan fortfarande samma strategi användas för att placera ut noderna i koordinatsystemet. Man tänker sig att noderna har det inordernummer de skulle ha haft om man kompletterat trädet så att det fyllts på maximalt på alla sina nivåer. Ett träd med höjden 2 som bara innehåller 4 noder enligt nedanstående figur (där existerande noder ritats svarta och tomma platser som vita noder) får då sina noder ritade på samma platser i koordinatsystemet som motsvarande noder i det maximalt fyllda trädet i figur 1. 0 1 2 3 4 5 0 6 7 x-axel 1 2 3 y-axel Fig 3: Layout för ett träd med höjden 2 Implementera med ledning av det ovannämnda metoden drawTree(BinarySearchTree bst). Observera att endast existerande noder (d.v.s. de som motsvarar de svarta noderna i figuren) skall ritas. Förutom noder och bågar skall metoden skriva ut nodernas innehåll i anslutning till dessa. Det kan vara lämpligt att låta metoden anropa en rekursiv hjälpmetod som har följande signatur: private void drawTree(BinaryNode n, int level, int h, int rootNbr); där n refererar till roten i det underträd som skall ritas, level är n:s nivå, h är underträdets höjd samt rootNbr är inordernumret för roten n. I klassen BSTVisualizer finns implementerat en enkel metod som givet en nivå returnerar y-koordinat och en som givet ett inordernummer returnerar x-koordinat. Testa ritning av träd genom att modifiera din main-metod från uppgift 1 så att den även ritar det/de träd som skapas. 3. Ett träd kan bli snett (obalanserat) när man gör många insättningar och borttagningar. Ett sätt att undvika detta är att balansera trädet i samband med varje insättning/borttagning enligt den metod som vi gått igenom på föreläsningarna (s.k. AVL-träd). Ett annat sätt kan vara att ”bygga om” trädet då och då när det blivit alltför snett förutsatt att detta inte sker alltför ofta. En algoritm som bygger om ett träd till ett komplett träd (d.v.s. ett träd som har maximalt antal noder på alla nivåer utom möjligen den som ligger längst bort från roten) när det inte finns dubbletter i trädet skall implementeras i denna uppgift. 14 Om man placerar alla element från trädet i växande ordning i en vektor är det sedan enkelt att bygga ett träd där antalet noder i vänster respektive höger underträd aldrig skiljer sig med mer än ett och som därför är komplett. Algoritmen är följande: Skapa en nod som innehåller mittelelementet i vektorn. Bygg (rekursivt) ett komplett träd som innehåller elementen tillvänster om mittelementet och ett komplett träd som innehåller elementen till höger om mittelementet. Låt dessa båda träd bli vänster respektive höger barn till roten. Antag t ex att vektorn innehåller heltalen 1,3,5,7,9,11,13. Trädet som byggs får då utseendet:. 7 11 3 1 5 9 13 Fig 4: Binärt sökträd med nycklar 1,3,5,7,9,11,13 byggt enligt algoritmen ovan. I klassen BinarySearchTree finns en metod public void rebuild(); som är avsedd att använda för att bygga om trädet på detta sätt. Metoden är implementerad så att den går igenom trädet i inorder och bildar en vektor med innehållet. Sedan anropas följande hjälpmetod som skall bygga trädet enligt den givna algoritmen: /* Bygg ett jämnt binärt sökträd av elementen a[first] .. a[last]. Returnera roten i trädet. */ private BinaryNode<E> buildTree(E[] a, int first, int last); Implementera metoden buildTree med den beskrivna rekursiva tekniken. Tänk på att införa lämpligt basfall. Testa genom att skriva en main-metod som bygger ett snett träd genom successiva insert-anrop och som sedan anropar rebuild(). Låt sedan main-metoden rita trädet med hjälp av klassen i uppgift 2 och kontrollera att det blivit ett komplett träd. 15 Övning 7+8 (datorövning) Innehåll: Hashtabeller, grafer. Träning i att läsa dokumentation och välja bland Javas standardklasser. Förkunskaper: Läroboken 6.8, kap. 20, 14.1-14.3, 14.5. __________________________________________________________________________________________ Inled alla datorövningar med att utföra kommandot z:\ad\init så att du får korrekt sökväg. 1. I denna uppgift skall en klass för hantering av en telefonkatalog implementeras med hjälp av en av Javas hashtabellklasser. Det finns tre klasser bland Javas standardklasser som hanterar hashtabeller nämligen Hashtable, HashMap och HashSet. De två första är identiska sånär som på att alla metoder i Hashtable är synchronized, vilket innebär att klassen kan användas i sammanhang där man har flera exekverande processer som kan avbryta varandra. HashSet skiljer sig från de båda andra genom att den implementerar det gränssnitt som gäller för mängder (Set) och därmed t ex inte har någon metod för sökning. Eftersom vi i denna uppgift bl.a. vill kunna söka upp personer som lagrats i tabellen och eftersom vi inte har behov av synkronisering kommer vi att använda klassen HashMap. a) Läs igenom dokumentationen av HashMap bland Javas standardklasser. Man kan använda vilka objekt som helst som nyckel i en hashtabell eftersom klassen Object innehåller metoden hashCode(). Många klasser omdefinierar denna metod. Vi kommer att använda nycklar som är av typen String i tabellen. Läs dokumentationen av hashCode-metoden i denna klass. b) Följande klass skall implementeras: public class PhoneRegister { private HashMap<String, Integer> reg; // telefonkatalogen /** Skapa en tom katalog. */ public PhoneRegister(); /** Registrera name med telefonnummer nbr i katalogen. Om detta namn inte redan finns i katalogen är det en insättning som sker, annars är det en uppdatering av telefonnumret. */ public void add(String name, int nbr); /** Tag reda på telefonnumret för namnet name. Returnerar negativt tal om namnet ej finns i katalogen. */ public int findNbr(String name); /** Tag bort element med namnet name ur katalogen. Om namnet ej finns returneras false, annars true. */ public boolean remove(String name); /** Undersök om registret är tomt*/ public boolean isEmpty(); } En fil med ovanstående specifikation finns i katalogen z:\ad\adovn\generic_phone\PhoneRegister.java. Kopiera den till egen katalog och implementera metoderna. Kopiera även filen z:\ad\adovn\generic_phone\TestRegister.java till din katalog. Den innehåller testmetoder skrivna i ramverket JUnit. Kompilera och exekvera testprogrammet och gör korrigeringar i PhoneRegister tills alla test går bra. c) Tillfoga följande metod i klassen PhoneRegister: /** Skriver ut innehållet i registret (namn och nummer) i alfabetisk ordning. */ public void print(); 16 Tips: Det finns en metod keySet() i HashMap som returnerar mängden av nycklar i tabellen. Denna kan sedan sorteras på något sätt, t ex genom att alla nycklarna sätts in i ett objekt av klassen TreeSet. Botanisera gärna bland Javas klasser för att hitta alternativ. I vilket fall som helst blir detta en ganska dyr operation eftersom hashtabeller inte stödjer operationer som bygger på ordning mellan elementen. Skälet till att ändå använda en hashtabell kan vara att sökningen är den operation som används oftast i en telefonkatalog och att denna skall vara så effektiv som möjligt. Att skriva ut hela katalogen görs förhoppningsvis sällan och då kan den operationen få kosta mera. Det finns en metod i TestRegister som testar att sätta in några element i registret och sedan skriver ut det. Testet skriver ut namnen på skärmen och du får själv visuellt kontrollera resultatet. 2. I denna övning används de klasser som presenterades och användes under seminarium 5 för att representera grafer. Klasserna finns också som bilaga sist i denna övning. En oriktad graf är bipartit om dess noder kan delas in i två grupper så att ingen båge i grafen har sina båda ändpunkter i samma grupp av noder. Ett alternativt sätt att uttrycka definitionen är att det är möjligt att färga grafens noder med två färger så att grannar aldrig har samma färg. Exempel: En bipartit graf och en graf som inte är bipartit Uppgiften går ut på att skriva en metod som undersöker om en graf är bipartit. Följande subklasser finns färdiga i paketet se.lth.cs.ad.twocolor: /** Beskriver en nod, som kan besökas och färgas. När noder skapas är de obesökta och har vit färg */ public class ColorVertex extends Vertex { /** Tag reda på nodens färg */ public Color getColor(); /** Ge noden färgen col */ public void setColor(Color col); /** Tag reda på om noden är besökt */ public boolean isVisited(); /** Markera noden besökt eller obesökt enligt parameter visit */ public void setVisited(boolean visit); } /** Beskriver en graf som kan ritas och där noderna kan färgas */ public abstract class ColorGraph extends DiGraph { /** Rita grafen och gör därefter en paus på 2 sekunder */ protected void redraw(); /** Undersök om grafen är bipartit */ public abstract boolean twoColorable(); } När noder av typ ColorVertex skapas är de obesökta och har färgen Color.white. Inför en subklass till ColorGraph i vilken den abstrakta metoden twoColorable() implementeras. För att få tillgång till färger skall du importera java.awt.Color. Dessutom måste du importera paketen digraph och twocolor. Din klass skall alltså inledas med importsatserna 17 import se.lth.cs.ad.digraph.*; import se.lth.cs.ad.twocolor.*; import java.awt.Color; Försök (t ex) färga noderna med de två färgerna Color.red och Color.blue. Använd inte Color.white eftersom vit färg redan reserverats för ofärgade noder. Rita om grafen varje gång du färgat en nod genom att anropa metoden redraw(). Tänk på att grafen inte nödvändigtvis är sammanhängande. För att testa metoden twoColorable och samtidigt illustrera hur grafen färgas nod för nod vid de rekursiva anropen har vi skrivit en rad testmetoder i klassen ColorTest. Var och en av dem bygger en graf, ritar upp den i ett fönster (ofärgad), och anropar twoColorable, som genom sina anrop av redraw kommer att visa när noderna färgas, samt jämför resultatet med korrekt svar för den aktuella grafen. Vi hade kunnat utföra dessa test inom ramen för det verktyg vi tidigare använt, JUnit. Eftersom testmetoderna nu i sig själva grafiskt illustrerar resultatet tillför det dock knappast något att köra dem inifrån verktyget (annat än att man då får ytterligare ett fönster med grönt/rött ljus för korrekt/felaktigt resultat). I paketet se.lth.cs.ad.twocolor finns klassen ColorTest med följande specifikation: public class ColorTest { /** Sätter namnet på den klass som skall testas till className */ public static setNameOfClass(String className); /* Följande fem metoder bygger vardera en graf, ritar den, anropar twoColorable() i klassen och jämför resultatet med korrekt svar.*/ public static void test1(); public static void test2(); public static void test3(); public static void test4(); public static void test5(); } Skriv själv ett testprogram enligt följande mönster: import se.lth.cs.ad.twocolor.*; public class MyTest { public static void main(String[] args) { ColorTest.setNameOfClass("..."); ColorTest.test1(); ColorTest.test2(); ... } } Strängen som är inparameter till metoden setNameOfClass skall vara namnet på den klass där du implementerat metoden twoColorable. Om t ex klassen heter TwoColorGraph skall du utföra anropet setNameOfClass("TwoColorGraph"). Testklassen ColorTest behöver namnet för att kunna skapa objekt (grafer) av denna typ och anropa metoden twoColorable på dem. Observera att klassen måste vara public för att testprogrammet skall kunna anropa dess metoder. Exekvera testprogrammet och gör ändringar i din implementation tills alla testerna ger rätt resultat. Anm: Då man kompilerar en klass görs normalt omkompilering av alla de klasser i samma katalog som utnyttjas av klassen om man gjort ändringar sedan föregående kompilering. I detta program finns det inget synligt beroende mellan er klass MyTest och den klass där ni implementerar operationen twoColorable. Automatisk omkompilering av den senare klassen kommer därför inte att ske då man kompilerar MyTest. Gör därför explicit omkompilering av var och en av era egna båda klasser vid behov. 18 Bilaga till övning 7+8 (uppgift 2) /** Beskriver en nod i en graf */ public class Vertex { /** Tag reda på nästa nod i grafen */ public Vertex nextVertex(); /** Tag reda på första bågen från denna nod */ public Edge firstEdge(); } /** Beskriver en båge i en graf */ public class Edge /** Tag reda på noden i bågens slut */ public Vertex endPoint(); /** Tag reda på nästa båge (från samma nod) */ public Edge nextEdge(); } /** Beskriver en graf */ public class DiGraph { /** Lägg in noden v i grafen */ public void insertVertex(Vertex v); /** Lägg in bågen e i grafen från nod v till nod w */ public void insertEdge(Vertex v, Vertex w, Edge e); /** Tag reda på första noden i grafen */ public Vertex firstVertex(); } 19 Övning 9 (papper- och pennaövning) Innehåll: Grafer. Hashtabeller. Prioritetsköer. Sortering. Förkunskaper: Läroboken 6.7-6.9, 14.3, kap. 20, 21.1-21.5, 8.1-8.3, 8.5-8.6. __________________________________________________________________________________________ 1. Givet följande graf. A 10 100 B 30 D 50 60 E 20 10 C Bestäm kortaste vägar från noden A till alla de noder till vilka det finns en väg från A genom att för hand använda Dijkstras algoritm. Visa steg för steg hur algoritmen konstruerar kortaste vägar, som på föreläsningsbilder. 2. Ett antal objekt med ett heltalsattribut nbr skall sättas in i en hashtabell av storlek 10. Objekten har följande värden på nbr: 4371, 1323, 6173, 4199, 4344, 9679, 1989. Använd hashfunktionen h(x) = (x.nbr) % 10. Visa tabellens utseende efter insättningarna i följande tre fall genom. Ange alltså för varje upptagen plats i tabellen nbr-värdet för det objekt som placerats där. a) Linjär teknik används vid kollisioner b) Kvadratisk teknik används vid kollisioner c) En öppen hashtabell används (separate chaining) 3. En implementation av en prioritetskö anses vara stabil om element med samma prioritet plockas ut ur kön med deleteMin i samma ordning som de sattes in. Är en binär heap en stabil implementation? Tips: Undersök hur det blir om man sätter in några element med samma prioritet och därpå gör ett antal anrop av deleteMin. 4. Sortera talen 8 1 4 1 5 9 2 6 5 med Quicksort, bokens variant med CUTOFF=3. Du behöver inte visa sortering av de små delvektorer som sorteras med insättningssortering. 5. Vad blir tidskomplexiteten om en vektor med n lika tal sorteras med a) Insättningssortering b) Mergesort c) Heapsort d) Quicksort, pivotelement = median av tre 20 6. I ett visst programspråk måste programmeraren numrera raderna. Numreringen skall vara i växande ordning. Det förekommer i språket satser av typen goto x, som innebär att exekveringen fortsätter med den programrad som har nummer x. Ex: 3 5 11 12 17 23 27 sats sats if ... goto 27 sats sats goto 3 sats Ibland vill man numrera om raderna så att första raden får ett visst nummer och så att numret för två på varandra följande rader har en viss fix differens. Om vi i exemplet ovan vill att första raden skall ha nummer 10 och skillnaden i nummer mellan två rader skall vara 5 skulle programmet efter omnumreringen bli: 10 15 20 25 30 35 40 sats sats if ... goto 40 sats sats goto 10 sats Formulera en algoritm som gör omnumreringen av ett program med n rader på i medelfall linjär tid genom att utnyttja en hashtabell. Ange vilken storlek tabellen bör ha. Algoritmen har som inparametrar antal programrader, det nya numret för den första raden samt den differens som skall gälla för numret på två på varandra följande rader i det omnumrerade programmet. Man kan inte förutsätta något om storleksordningen på dessa tal annat än att alla radnummer kan representeras som vanliga heltal i datorn. Beskriv din algoritm i ord eller pseudokod och motivera att tidskomplexiteten blir O(n). Förutsätt att det finns operationer för att läsa och skriva rader (och radnummer) i ett program sekventiellt och för att känna igen satser av typen goto x.
© Copyright 2024