LUNDS TEKNISKA HÖGSKOLA Institutionen för datavetenskap Algoritmer och datastrukturer, EDA690 Seminarieövningar, Algoritmer och datastrukturer Seminarieövningarna i kursen Algoritmer och datastrukturer ger exempel på tillämpningar av det material som behandlas under kursen. För att seminarierna skall ge bäst utbyte måste du ha läst på uppgifterna i förväg och gärna försökt lösa dem. Du måste också läsa de avsnitt som anges under förberedelser efter rubriken på varje seminarium. Aktivt deltagande i form av frågor, synpunkter och förslag till lösningar välkomnas. Alla seminarier innehåller uppgifter att lösa. Uppgifterna, som ibland är svårare än de som behandlas på programmeringsövningarna, löses av lärare vid tavlan. En del seminarier innehåller dessutom en hel del text som utgör en sammanfattning av eller ett komplement till de avsnitt i läroboken som anges under rubriken Litteratur för respektive seminarium. I något enstaka fall behandlas material som inte finns i läroboken. Med läroboken avses ”Data Structures & Problem Solving Using Java” av Mark Allen Weiss. Lösningsförslag delas ut i samband med seminarierna. Seminarieövning 1 Mål Att ge förståelse för hur man beräknar tidskomplexiteten för enkla algoritmer och belysa sambandet mellan teoretisk tidskomplexitet och verklig tidsåtgång. Att introducera interface och ge insikt om hur dessa används. Innehåll Seminariet består av två delar. Den första (uppgifterna 1-3) består av problem i anslutning till de första två föreläsningarna och handlar om tidskomplexitet. Den andra delen (uppgifterna 4-6) består av en text där interface i Java behandlas. Speciellt behandlas interfacet Comparable med flera exempel. Dessutom ges en introduktion till en del av nyheterna i Java 5.0. Litteratur Tidskomplexitet behandlas i läroboken kap 5. Texten om interface i detta seminarium. Interface behandlas även i läroboken 4.4. OH-bilder om Java 5.0 finns på hemsidan. __________________________________________________________________________________________ Uppgift 1 I en vektor a av typ int[] finns n heltal på platserna 0..n–1. Man vill ta bort alla dubbletter av det tal som finns på första platsen i vektorn. Följande algoritm används: int index = 1; int size = n; while (index < size) { if (a[index] == a[0]) { // dubblett funnen, skifta a[index+1]..a[size-1] ett steg vänster for (int i = index; i < size-1; i++) { a[i] = a[i+1]; } size--;// antal platser i a med tal insatta har nu minskat med ett } else { // ingen dubblett, gå vidare till nästa plats */ index++; } } När inträffar värsta fallet och vad blir tidskomplexiteten då för denna algoritm? Finns det något effektivare sätt att lösa problemet? Uppgift 2 Det tar 0.5 ms för en algoritm att beräkna lösningen till ett visst problem av storlek 100. Hur lång tid skulle det ta att lösa samma problem av storlek 500 om algoritmens tidskomplexitet är a) linjär b) O(n log n) c) kvadratisk d) kubisk Förutsätt i samtliga fall att lägre ordningens termer i det exakta uttrycket för tidskomplexiteten kan försummas. Uppgift 3 Följande metoder kan användas för att beräkna den geometriska summan 1 + a + a2 + a3 + ... + an public double computeGeometricSum(double a, int n) { double sum = 1; for (int i = 1; i <= n ; i++) { sum += power(a,i); } return sum; } private double power(double a, int n) { double pow = 1; for (int i = 1 ; i <= n; i++) { pow *= a; } return pow; } a) Vad blir tidskomplexiteten för ett anrop computeGeometricSum(a,n)? b) Ge en förbättrad algoritm som utnyttjar att an = a*an-1. Vad blir tidskomplexiteten nu? __________________________________________________________________________________________ Definition av interface (gränssnitt) i Java Ett interface i Java består uteslutande av abstrakta, publika metoder. Det får inte finnas några konstruktorer eller attribut i ett interface. Konstanter får finnas med. /** Ett interface med två metoder p och q. */ public interface SomeInterface { void p(); int q(int x); } Notera att vi inte behöver (eller bör) specificera metoderna public eller abstract, de blir de ändå definitionsmässigt i ett interface. I Java finns också abstrakta klasser En klass är abstrakt om den innehåller en eller flera abstrakta metoder, dvs metoder som inte implementeras i klassen. Man kan inte heller skapa några objekt av en abstrakt klass. De metoder som är abstrakta måste implementeras i subklasser till den abstrakta klassen. En abstrakt klass skiljer sig från ett interface genom att den kan innehålla såväl abstrakta metoder som ickeabstrakta (implementerade) metoder samt attribut. Det är dock tillåtet att en abstrakt klass enbart innehåller abstrakta metoder. En klass som skall implementera metoder från en abstrakt klass utnyttjar ärvning: public class SomeClass extends SomeAbstractClass { // Här implementeras de metoder som är abstrakta // i SomeAbstractClass (och ev ytterligare metoder) } En klass implementerar ett interface genom att deklarera detta i sin rubrik och genom att implementera alla metoderna i interfacet. Klassen innehåller ofta också flera egna metoder: public class SomeClass implements SomeInterface { public void p() { // implementation av metoden } public int q(int x) { // implementation av metoden } // eventuellt ytterligare metoder } I klassen som implementerar interfacet måste interface-metoderna vara public och de måste ha samma signatur (samma namn och samma antal, typ och ordning på parametrarna) som i inter-facet. Det är tillåtet att en klass implementerar flera interface: public class SomeClass implements SomeInterface, SomeOtherInterface { // här måste alla metoder från SomeInterface och // SomeOtherInterface implementeras. En klass kan bara ärva från en klass och därmed bara vara subklass till en abstrakt klass men kan implementera flera interface. Det är även tillåtet för en klass att ärva från en klass och implementera ett eller flera interface: public class SomeClass extends SomeOtherClass implements SomeInterface {...} Även om klassen SomeOtherClass och interfacet SomeInterface båda har en metod p med samma signatur leder det inte till konflikter. Klassen SomeClass behöver då inte implementera p, den ärvs från SomeOtherClass. SomeClass kan emellertid fortfarande implementera p. Denna implementation omdefinierar i så fall den som finns i superklassen SomeOtherClass. En klass som ärver från en annan ärver också implementation av de metoder som finns i superklassen. Det är detta som kan leda till olika slags konflikter om man har multipel ärvning (vilket Java inte har) . En klass som implementerar ett eller flera interface ärver inte någon implementation från dessa (eftersom ingen sådan finns). Det klassen ”ärver” är ett protokoll eller gränssnitt (bestående av metodrubriker) från de interface den implementerar. Det är tillåtet att variabler och formella parametrar har en typ som är ett interface. Om vi deklarerar en variabel SomeInterface x; så får x referera till objekt av klasser som implementerar interfacet SomeInterface. I detta fall kan därför x referera till objekt av klasserna SomeClass och även till subklasser till denna. Följande tilldelning är alltså korrekt: x = new SomeClass(...); Om vi har en metod med någon formell parameter av typen SomeInterface: public void doWork(SomeInterface s); så kan vi anropa metoden med aktuell parameter vars typ är SomeInterface eller en klass som implementerar detta interface: SomeClass sc= new SomeClass(...); SomeInterface si = new SomeClass(...); doWork(sc); doWork(si); Användning av interface I Java har man förutsett att en del allmänna egenskaper kan behövas för alla klasser och placerat motsvarande metoder i klassen Object. Här finns exempelvis metoden toString() som ger alla objekt egenskapen att kunna skrivas ut, metoden hashCode() som ger alla objekt egenskapen att kunna hashkodas och sättas in i hashtabeller (kommer vi till senare i kursen). När vi själv implementerar klasser skulle vi kunna göra på liknande sätt. För att t ex ge ett antal klasser egenskapen att ”kunna ritas” kan man ge klasserna en gemensam abstrakt superklass med en metod draw(). Därmed skapar man dock mycket märkliga ärvningshierarkier för klasser som ur modelleringssynpunkt inte har något med varandra att göra Oftast väljer man att använda interface i stället för abstrakta klasser när man vill beskriva en egenskap som kan innehas av många olika klasser inom olika ärvningshierarkier. I vårt exempel ovan skulle vi alltså kunna införa ett interface som innehåller en metod draw() och sedan låta alla klasser vars objekt skall ha egenskapen att kunna ritas implementera detta interface. Det finns en rad interface i Javas standardbibliotek, speciellt i grafikpaketen, där allmänna egenskaper såsom att kunna lyssna på mushändelser modelleras som interface. Man kan naturligtvis också själv definiera nya interface vid behov. Vi kommer här att behandla en viktig egenskap som utmärkt låter sig beskrivas med hjälp av interface, nämligen egenskapen att kunna jämföras med andra objekt av samma typ. Antag t ex att vi vill implementera en metod som söker upp största element i en vektor a med n element (n>=1). Algoritmskiss: maxElement = a[0]; for (int i = 1; i < a.length; i++) om a[i] är större än maxElement maxElement = a[i]; Ingenting i logiken i denna metod begränsar den till en viss slags element. Den går att använda på heltal, flyttal, strängar etc. För att återspegla denna generella karaktär skulle ett första förslag till metodrubrik kunna vara: public Object findMax(Object[] a); Försöker vi nu implementera metoden råkar vi ut för en svårighet. Det finns ingen ”större-än-operation” för objekt av klassen Object. Vi måste för att kunna implementera algoritmen ha en sådan metod och måste inskränka vår metod till att arbeta med en vektor av denna typ. I Java har man redan förutsett behovet av att kunna utöka klasser med denna egenskap genom att införa ett interface Comparable i java.lang. Detta behandlas i nästa avsnitt. Interfacet Comparable I Java 5.0 har man infört s.k. generiska klasser och interface. Mer om detta finns att läsa på kursens hemsida. Vi skall här inskränka oss till att nämna lite om generiska interface, speciellt interfacet Comparable. Ett generiskt interface kan ha en eller flera typparametrar. Ex: public interface Comparable<E> {...} Typparametrar (i detta fall E) anges inom tecknen < och >. Typparametern kan användas i interfacet för att ange typ på inparametrar till metoder och för att ange typ för vad metodernas eventuellt returnerar. Ex: public interface Comparable<E> { public int compareTo(E rhs); } Antag att vi har ett program där vi gör statistik på förekomster av ord ur texter där följande klass förekommer: public class WordCount { private String theWord; private int count; ... } // ordet // antal förekomster För att objekt av typen WordCount skall kunna jämföras med varandra med avseende på antalet förekomster kan vi låta klassen implementera interfacet Comparable enligt: public class WordCount implements Comparable<WordCount> { private String theWord; // ordet private int count; // antal förekomster public int compareTo(WordCount rhs) { return count - rhs.count; } ... } Flera av Javas standardklasser implementerar redan det generiska interfacet Comparable<E>, t ex implementeras Comparable<Integer> av klassen Integer och Comparable<String> av klassen String. Detta utnyttjas i följande exempel där vi inför en klass Person som beskriver en person och vill att jämförelse skall avse alfabetisk ordning av personernas namn: public class Person implements Comparable<Person> { private String name; // övriga attribut public int compareTo(Person rhs) { return name.compareTo(rhs.name); } // övriga metoder i klassen } Här har vi i implementationen av metoden compareTo delegerat jämförelsen till att utföras av motsvarande operation i klassen String. Vi har alltså utnyttjat att klassen String har en metod med signaturen int compareTo(String rhs). Metoden equals Det är viktigt att känna till att det finns en operation för jämförelse avseende likhet i klassen Object: public boolean equals(Object rhs); Metoden är implementerad så att resultatet av ett anrop x.equals(y) blir true enbart om x == y, d.v.s. om x och y refererar till samma objekt. Meningen är att man vid behov omdefinierar metoden i egna klasser. Det är speciellt viktigt för klasser som implementerar interfacet Comparable. Här implementeras ju metoden compareTo så att resultatet blir 0 då man anser att två objekt av den aktuella typen är lika. För våra klasser i föregående avsnitt skulle detta innebära att två objekt av klassen WordCount anses lika om de har samma frekvens och att två personer anses lika om de har samma namn. Om man emellertid utför följande: Person p1 = new Person("Kalle"); // Vi antar det finns en sådan konstruktor Person p2 = new Person("Kalle"); if (p1.compareTo(p2) == 0) { System.out.println("Equal!") } else { System.out.println("Not equal!"); } if (p1.equals(p2)) { System.out.println("Equal!"); } else { System.out.println("Not equal!"); } får man utskriften: Equal! Not equal! Resultatet beror på att metoden equals, som ju är definierad i superkalssen Object, jämför om objekten p1 och p2 är identiska. Det är en mycket stark rekommendation att varje klass som implementerar interfacet Comparable också omdefinierar metoden equals så att equals ger resultatet true precis när compareTo ger reslutatet 0. För våra två klasser skulle det se ut så här (vi visar nu bara metoden equals): public class WordCount implements Comparable<WordCount>{ // allt som fanns förut public boolean equals(Object rhs) { return compareTo((WordCount) rhs) == 0; } } public class Person implements Comparable<Person>{ // allt som fanns förut public boolean equals(Object rhs) { return compareTo((Person) rhs) == 0; } } Lägg märke till att equals har samma signatur som i klassen Object, annars skulle det inte bli omdefinition av metoden utan överlagring. Lägg också märke till hur implementationen av equals använder sig av compareTo och därmed uttrycker det samband som skall råda mellan de båda metoderna. Notera dock att vi i anropet måste göra en typomvandling eftersom parametern till compareTo måste vara av typ WordCount respektive Person. __________________________________________________________________________________________ Uppgift 4 Vad tror du anledningen är att det anses viktigt att metoderna compareTo och equals är konsistenta? Uppgift 5 Vår implementation av equals ovan är egentligen för enkel. Det ”kontrakt” för metoden equals som finns i Java innebär att: • Om x inte är null skall x.equals(x) returnera true. • Om x och y är objekt av olika klasser skall x.equals(y) returnera false. • Om x inte är null skall x.equals(null) returnera false. Vilka av dessa regler bryter vår implementation emot? Korrigera implementationen så att vi uppfyller alla regler. Ledning: Man kan ta reda på vilken klass x är ett objekt av (runttime-type) genom anropet x.getClass(). Uppgift 6 Antag att vi i klassen Person har två namnattribut, ett för efternamn och ett för förnamn enligt följande: public class Person implements Comparable<Person> { private String familyName; private String firstName; public int compareTo(Person rhs) { ... } public boolean equals(Object rhs) { ... } } Implementera compareTo och equals så att jämförelse innebär alfabetisk ordning på i första hand efternamn och i andra hand förnamn. Dvs om två personer har samma efternamn är det förnamnen som avgör vem som kommer alfabetiskt först. __________________________________________________________________________________________ Generiska metoder Låt oss återgå till problemet som tidigare behandlats under seminariet: att skriva en metod findMax som tar reda på största elementet i en vektor. Vi har kontaterat att vi inte kan utforma metoden så att den har en vektor av Object som inparameter eftersom vi måste förutsätta att objekten i vektorn går att jämföra med varandra. Med utnyttjande av interfacet Comparable kan vi utforma metoden på följande sätt i Java 5.0: public static <E extends Comparable<E>> E findMax(E[] a) {...} Metoden är ett exempel på en av nyheterna i Java 5.0 en s.k. generisk metod. Inparametern är en vektor där elementen är av typen E och metoden returnerar ett element av denna typ. För att tala om att E skall uppfattas som en typparameter anges detta (före uppgiften om vad metoden returnerar) inom tecknen < och >. Man kan också ange s.k. övre eller undre gränser (eng: upper/lower bounds) för typparametrar. I detta fall anger vi Comparable<E> som övre gräns för E genom att skriva <E extends Comparable<E>>. Vi anger därmed att parametern E måste vara en typ som implementerar interfacet Comparable<E>. Man förväntar sig kanske att detta skulle anges genom att skriva E implements Comparable<E>. I Java 5.0 har man dock bestämt sig för att använda ordet extends i detta sammanhang. Implementation av den generiska metoden findMax: public static <E extends Comparable<E>> E findMax(E[] a) { if (a.length == 0) { return null; } E maxElement = a[0]; for (int i = 1; i < a.length; i++) { if (a[i].compareTo(maxElement) > 0) { maxElement = a[i]; } } return maxElement; } Metoden är nu generell och kan användas för att söka det största elementet i alla vektorer vars element är av en typ E som implementerar Comparable<E>, t ex Person eller WordCount: Person[] persons = new Person[10]; WordCount[] words = new WordCount[1000]; ... Person maxPerson = findMax(persons); WordCount maxCount = findMax(words); Mera om användning av interface Under denna kurs kommer vi att presentera i datalogin vanligt förekommande abstrakta datatyper (ADT). En abstrakt datatyp är en abstrakt modell med ett antal metoder för att manipulera modellen. Ett exempel som finns redan i grundkursen är lista. En lista är en abstrakt modell. Den definieras som en ordnad följd av element. Med ordning avses inte att listan på något sätt är sorterad med avseende på elementens innehåll, utan att man kan identifiera första element, sista element, efterföljare och föregångare till element. Listan kan manipuleras med metoder för insättning av nya element och borttagning av element ur listan. Man behöver också metoder för att positionera sig i listan. När man har bestämt sig för vilka metoder som skall finnas i en ADT kan man skriva en specifikation bestående av alla metodrubriker med tillhörande kommentarer. Implementationen är sedan möjlig att genomföra på flera olika sätt. Det första valet gäller vilken datastruktur som skall representera modellen, i detta fall listan. Vektor eller olika slags länkade strukturer är möjliga kandidater. När datastruktur valts kan man implementera metoderna. Det kan ibland vara lämpligt att ge användare möjlighet att välja mellan flera olika implementationer. I Javas klassbibliotek, liksom i läroboken, väljer man i flera fall att först specificera en abstrakt datatyp som ett interface. Antag att man vill ha en ADT för hanteringeng av tid. Specifikationen ges av följande interface· public interface Time { public void setTime(int hours, int minutes); public void addTime(int hours, int minutes); public int getHours(); public int getMinutes(); public Time timeDiff(Time otherTime); ... } Vi kan implementera interfacet på olika sätt t ex genom att ha två heltalsattribut för timmar respektive minuter: public class ConcreteTime1 implements Time { int hours; int minutes; ... konstruktor och impl av alla metoder i interfacet Time... } Ett annat alternativ är att låta tid representeras av enbart antal minuter: public class ConcreteTime2 implements Time { int minutes; ... konstruktor och impl av alla metoder i interfacet Time... } En programmerare som behöver hantera tider i ett program gör klokt i att i sina deklarationer använda interfacet Time enligt följande mönster: Time t1 = new ConcreteTime1(); t1.setTime(3,58); Time t2 = new ConcretTime1(); t2.setTime(2,45); Time t3 = t1.timeDiff(t2); ... Genom att man deklarerat alla tider med typnamnet Time så kan man bara använda de operationer som finns i interfacet Time, inte eventuella ytterligare operationer som kan finnas i olika implementationer som ConcretTime1 eller ConcretTime2. Om man skulle ångra sig och vilja använda den andra implementationen är det bara på de ställen där man skapar tidsobjekt man behöver göra ändringar.. En fördel med att specificera abstrakta datatyper som interface är alltså att de då kan manipuleras i program utan att man binder sig till en speciell implementation. Om man vill skriva en metod som har en tid som inparameter gör man också klokt i att deklarera att parameterna är av typ Time, t ex public void printTime(Time t) { int hours = t.getHours(); int minutes = t.getMinutes(); System.out.println(hours + ":" + minutes); } Därmed kan man som aktuell parameter överföra objekt av alla klasser som implementerar detta interface. __________________________________________________________________________________________ Uppgift 7 Om man ur modelleringssynpunkt tycker att det kan vara lämpligt att införa ett interface med flera olika metoder kan det ibland av andra skäl vara motiverat att i stället välja en abstrakt klass. Kan du tänka ut något sådant skäl? Tips: ett interface får inte innehålla någon kod men en abstrakt klass kan innehålla både abstrakta metoder och metoder med implementation. Seminarieövning 2 Mål Att ge förståelse för behov, utformning och användning av iteratorer för samlingar av objekt (collections). Att ge kunskaper om olika sätt att utforma listhanteringspaket. Innehåll Interfacet Iterator i Java. Genomgång av de väsentliga delarna av klassen LinkedList i Java. Interfacet ListIterator. Litteratur Läroboken 6.1-6.3, 6.5. Dokumentation av LinkedList och Iterator på nätet. För jämförelse med andra sätt att utforma/implementera listor och iteratorer finns avsnitten 15.1-15.4 och 17.117.3 __________________________________________________________________________________________ Interfacet Iterator För klasser som representerar samlingar av element behöver man oftast något sätt att gå igenom (traversera, iterera över) alla element i samlingen. Det kan t ex röra sig om att man vill söka sig fram till en viss position i en lista som innehåller ett visst element för att sedan där sätta in ett ytterligare element. Eller det kan röra sig om att man vill gå igenom hela samlingen för att skriva ut någon uppgift om de ingående elementen. Denna egenskap, att kunna traverseras, är så allmän att man infört ett speciellt interface för detta i Java. Interfacet heter Iterator och har följande specifikation: public interface Iterator<E> { /** Return true if there are more items in this iteration */ boolean hasNext(); /** Returns a reference to the next object not yet seen by this iterator. The object becomes seen, and thus advances the iterator. */ E next(); /** Removes the last item viewed by the operation next. This method can be called only once between calls to next. Optional operation. */ void remove(); } Kommentarer: Metoden remove anges vara optional d.v.s man skall inte behöva implementera den. Naturligtvis måste den finnas med i alla klasser som anger att de implementerar interfacet Iterator, annars skulle kompilatorn klaga. Vill man inte stödja operationen så implementerar man den så att den alltid genererar UnsupportedOperationException. Metoden next skall generera NoSuchElementException om det inte finns fler element. Varken UnsupportedOperationException eller NoSuchElementException behöver fångas. Alla klasser i Javas klassbibliotek som beskriver samlingar (implementerar interfacet Collection) har en metod med följande signatur: /** Returns an iterator over the elements in this collection. */ public Iterator<E> iterator(); Metoden returnerar ett objekt av en klass som implementerar interfacet Iterator<E>. Genom att anropa metoden får alltså en användare möjlighet att iterera över samlingen enligt följande mönster, där vi antar att SomeCollection<E> är en klass som implementerar interfacet Collection<E> och därmed har metoden iterator(): SomeCollection<Person> sc = new SomeCollection<Person>(); // Här sätts Person-objekt in i samlingen. ... Iterator<Person> itr = sc.iterator(); while (itr.hasNext()) { Person actPerson = itr.next(); // behandla actPerson } Märk att vi aldrig riskerar att få en NoSuchElementException när vi itererar på detta sätt. Vi kontrollerar ju först med hasNext att det är korrekt att ytterligare en gång anropa next. Ett alternativt (och enklare) sätt att iterera genom samlingen är att använda den nya for-satsen i Java 5.0 enligt följande mönster: for (Person actPerson: sc) { // behandla actPerson } Denna typ av for-sats kan användas för klasser som implementerar interfacet Iterable. Detta interface har följande specifikation: public interface Iterable<E> { public Iterator<E> iterator(); } Det har alltså en enda metod vilken returnerar en iterator för en mängd objekt av typen E. Interfacet Collection ärver från interfacet Iterable: public interface Collection<E> extends Iterable<E> {...} Klasser som implementerar interfacet Collection<E> måste alltså ha en metod iterator med den signatur som anges i interfacet Iterable. Det blir därmed möjligt att använda den nya for-satsen för alla sådana klasser. __________________________________________________________________________________________ Uppgift 1 I stället för att låta klassen SomeCollection implementera Iterable skulle man kunna låta den implementera Iterator: public class SomeCollection<E> implements Iterator<E> { // impl av alla metoder för samlingen + alla metoder i Iterator } Vad är nackdelen med denna utformning jämfört med den tidigare där klassen implementerade interfacet Iterable<E>? __________________________________________________________________________________________ Implementation av iteratorer De flesta typer av samlingar finns redan färdigimplementerade i Java. Ibland kan man dock behöva skriva en egen klass (ADT) som beskriver en samling element och då behöver man antagligen också ha en operation som returnerar en iterator. Låt oss anta att vi implementerar en egen klass MySet enligt följande public class MySet<E> implements Iterable<E> { E[] elements = (E[]) new Object[..]; // Insatta element finns i denna // vektor som initialt ges en viss storlek. int size; // Antal element i mängden. // Elementen finns på platserna 0..size-1 i vektorn elements. /** Sätt in x i mängden om det inte redan finns. Returnera false om x redan fanns, annars true. */ public boolean add(E x) {...} /** Undersök om x finns i mängden. */ public boolean contains(E x) {...} /** Tag bort x ur mängden om det finns med */ public void remove(E x) {...} /** Skapa och returnera en iterator för mängden. */ public Iterator<E> iterator() {...} } För att implementera den sista metoden måste vi kunna skapa objekt av en klass som beskriver en iterator för en mängd av typen MySet. Vi måste alltså först skriva en klass som implementerar interfacet Iterator<E>. Förutom metoderna hasNext, next och remove måste vi ha en konstruktor så att vi kan skapa objekt av klassen: public class MySetIterator<E> implements Iterator<E> { public MySetIterator(MySet<E> s) {...} public boolean hasNext() {...} public E next() {...} public void remove() {...} } Konstruktorn har som parameter den mängd till vilken iteratorn skall knytas. För att hålla reda på aktuell position för iteratorn är det i detta fall enklast att införa ett heltalsattribut index som anger platsen för det element som skall returneras vid nästa anrop av metoden next. Dessutom måste vi ha ett attribut som refererar till mängden över vilken vi itererar. Den fullständiga implementationen blir därför: public class MySetIterator<E> implements Iterator<E> { private int index; private MySet<E> theSet; public MySetIterator(MySet<E> s) { theSet = s; index = 0; } public boolean hasNext() { return index < theSet.size; } public E next() { if (index >= theSet.size) { throw new NoSuchElementException(); } return theSet.elements[index++]; } public void remove() { throw new UnsupportedOperationException(); } } Vi har här valt att låta remove alltid generera en exception. Operationen iterator() i klassen MySet<E> får nu följande enkla implementation: public Iterator<E> iterator() { return new MySetIterator<E>(this); } __________________________________________________________________________________________ Uppgift 2 a) Attributen elements och size i MySet är inte privata utan saknar skyddskod, vilket innebär att de kan nås med punktnotation i klasser i samma paket. Har vi utnyttjat detta i ovanstående implementation? Skulle vi kunna göra dem privata? b) Skulle man kunna sätta index = –1 initialt i stället för 0? Vilka ändringar måste i så fall göras i metoderna? Uppgift 3 Antag att MySet<E> i stället för en vektor använde sig av en enkellänkad lista för att lagra elementen enligt följande: class ListNode<E> { E element; ListNode<E> next; // Innehållet i listnoden. // Referens till efterföljande listnod. //... konstruktor } public class MySet<E> implements Iterable<E> { ListNode<E> first; // Referens till första noden, null om tom. // ... metoder som ovan } Implementera en ny iteratorklass för denna representation av mängden. __________________________________________________________________________________________ Ett citat från dokumentationen av Iterator på nätet, närmare bestämt i anslutning till metoden remove: The behavior of an iterator is unspecified if the underlying collection is modified while the iteration is in progress in any way other than by calling this method. Iteratorer kräver i allmänhet en stabil samling element att iterera över för att inte riskera att ge konstiga resultat om man utifrån tar bort eller lägger till något i den position iteratorn befinner sig. Det är oftast svårt att implementera en iteratorklass för en samling så att den ger vettiga resultat även i sådana fall. I Javas implementationer av Iterator-interfacet har man intagit en ännu striktare hållning. Man gör det omöjligt att fortsätta använda en iterator på en mängd som förändras. Det som händer är att man får ett exekveringsfel nästa gång man anropar iteratorns hasNext, next eller remove. Man brukar i dokumentationen av iteratormetoden i collection-klasserna använda ett namn på detta fenomen: iteratorn är fail-fast. Det är en stark rekommendation att vi när vi implementerar iteratorer för egna klasser gör på samma sätt. Åtminstone bör man annars klart och tydligt i specifikationen ange att man inte kan garantera att resutatet av en iteration blir det en användare förväntar sig om man modifierar mängden av element under iterationens gång. Förklaringen till att iteratorinterfacet innehåller en metod remove är just att man intagit en så strikt ståndpunkt avseende förändringar utifrån. Antag t ex att vi skulle vilja iterera oss igenom en lista för att ta bort alla element som uppfyller något visst villkor. Vi använder en iterator och rör oss framåt tills vi hittar det första elementet som skall tas bort. Därefter anropar vi listans remove-metod (vi antar att det finns en sådan som tar bort elementet i en viss position). Nu kan vi inte längre använda vår iterator eftersom mängden förändrats. Vi får skapa en ny och börja röra oss från början av listan fram till en position där vi eventuellt hittar ett nytt element som skall bort o.s.v. Detta blir klart ineffektivt. Genom att införa en remove-operation i iteratorinterfacet och ha ett strikt protokoll för denna kan man tillåta borttagningar under iterationens gång. Protokollet för remove i interfacet Iterator är: Removes from the underlying collection the last element returned by the iterator (optional operation). This method can be called only once per call to next. D.v.s. efter ett anrop av remove måste det komma minst ett anrop av next innan det är tillåtet att göra en ny borttagning. Om vi granskar den iteratorklass MySetIterator<E> som vi implementerade tidigare ser vi att denna inte uppfyller de krav som nämnts ovan. Det finns ingen som helst kontroll av om mängden förändrats sedan föregående metodanrop. Den teknik som brukar användas för att kunna göra dessa kontroller är följande: Inför i den collection-klass vi skall iterera över (i vårt fall MySet<E>) ett heltalsattribut modCount, som initialt när ett objekt av klassen skapas är 0 och som ökas varje gång en metod som förändrar mängden anropas. När man skapar ett iterator-objekt (i vårt fall ett objekt av typen MySetIterator<E>) ser man till att överföra värdet av modCount till iteratorobjektet. I metoderna hasNext och next kontrollerar man sedan att mängdens modCount-värde överensstämmer med det värde det hade när iteratorn skapades. Den förbättrade implementationen blir då: public class MySet<E> implements Iterable<E> { E[] elements = (E[]) new Object[..]; / int size; int modCount = 0; // Ökas varje gång mängden förändras. /** Sätt in x i mängden om det inte redan finns. Returnera false om x redan fanns, annars true. */ public boolean add(E x) {.. om x sätts in så utför modCount++ ..} /** Undersök om x finns i mängden. */ public boolean contains(Object x) {...} /** Tag bort x ur mängden om det finns med */ public void remove(Object x) {.. om x tas bort så utför modCount++ ..} /** Skapa och returnera en iterator för mängden. */ public Iterator<E> iterator() { return new MySetIterator<E>(this); } } public class MySetIterator<E> implements Iterator<E> { private int index; private MySet<E> theSet; private int expectedModCount; // Används för att kontrollera om mängden // förändrats sedan denna iterator skapades. public MySetIterator(MySet<E> s) { theSet = s; index = 0; expectedModCount = s.modCount; } public boolean hasNext() { if (expectedModCount != theSet.modCount) { throw new ConcurrentModificationException(); } return index < theSet.size; } public E next() { if (!hasNext()) { // Observera att i hasNext kontrolleras // värdet på modCount. throw new NoSuchElementException(); } return theSet.elements[index++]; } public void remove() { throw new UnsupportedOperationException(); } } Skulle man vilja implementera även remove och uppfylla dess protokoll så får man införa flera kontrollattribut i iteratorklassen. Se t ex läroboken figurerna 15.14 och 15.15. __________________________________________________________________________________________ Uppgift 4 Skulle det kunna bli några problem med den första iteratorimplementationen för MySet<E>, d.v.s den som saknar kontroll på om modifieringar av mängden gjorts? Vilka problem blir det i så fall och i vilka situationer? __________________________________________________________________________________________ Listor Bland Javas standardklasser finns det två implementationer av lista, ArrayList<E> och LinkedList<E>. (Det finns också kvar en äldre variant Vector som liknar ArrayList). ArrayList<E> använder en vektorimplementation vilket gör att vissa operationer blir kostsamma. T ex finns det en operation add(int index, E element) som lägger in element på plats index i listan. Det element som låg på plats index och alla som fanns på platser med högre indexnummer måste då skiftas ett steg åt höger i vektorn. Vi koncentrerar oss här på klassen LinkedList<E>, som använder en dubbellänkad struktur för att representera listan. Varje nod i listan innehåller alltså referenser till föregående och efterföljande nod. Noderna innehåller också en referens till ett objekt som satts in i listan. Denna referens är av typen E. Vi kan göra oss följande schematiska bild av en sådan lista med 4 element insatta: Listan och dess ”noder” Insatta objekt Några av metoderna i klassen är: /** Appends the specified element to the end of the list. Returns true. */ public boolean add(E o); /** Inserts the specified element at the specified position in this list */ public void add(int index, E element); /** Returns true if this list contains the specified element. public boolen contains(Object o); /** Returns the index in this list of the first occurrence of the specified element, or -1 if the list does not contain this element. */ public int indexOf(Object o); /** Removes the first occurrence of the specified element in this list. Returns true if the list contained the specified element. */ boolean remove(Object o); /** Returns the number of elements in this list */ public int size(); Det finns betydligt flera metoder. Titta på den fullständiga specifikationen som finns i den dokumentation av Javas standardklasser vilken du enkelt når från kursens hemsida. Kommentar : Den första add-operationen är en funktion som här alltid returnerar true, vilket kan synas egendomligt. Det hänger samman med att alla klasser i Java som representerar samlingar av något slag (eng: collections) är inordnade i en hierarki av abstrakta klasser och interface. Operationen add är specificerad i ett interface Collection<E> (som implementeras av klasssen LinkedList<E>) där man utformat den så att den via sitt returvärde informerar om insättningen lyckades. I vissa klasser som beskriver samlingar vill man nämligen förbjuda dubbletter. Då vill man att add-operationen returnerar false om man försöker sätta in ett likadant objekt som redan finns i listan. I klassen LinkedList<E> tillåts dock dubbletter och add returnerar därför alltid true d.v.s. man behöver egentligen inte bry sig om returvärdet. __________________________________________________________________________________________ Uppgift 5 Skriv programsatser som givet en lista av typen LinkedList<Integer> tar bort alla objekt ur listan som innehåller talet 0 och till sist tar reda på listans storlek. __________________________________________________________________________________________ Listiteratorer I en lista har man ofta behov av att röra sig både framåt och bakåt och då är interfacet Iterator<E> otillräckligt. Därför har följande utvidgade interface ListIterator<E> införts i Java: public interface ListIterator<E> extends Iterator<E> { /** Inserts element into the list. Optional operation. The element is inserted immediately before the next element that would be returned by next(), if any and after the next element that would be returned by previous(). The new element is inserted before the implicit cursor; a subsequent call to next() would be unaffected, and a subsequent call to previous() would return the new element. */ void add(E x); /** Returns true if this iterator has more elements in forward direction. */ boolean hasNext(); /** Returns true if this iterator has more elements in reverse direction. */ boolean hasPrevious(); /** Returns next element in list. */ E next(); /** Returns previous element in list. */ E previous(); /** Removes from list the last element returned by next or previous. Optional operation. */ void remove(); /** Replaces the last element returned by next or previous with the specified element. Optional operation. */ void set(E x); Det finns ytterligare ett par metoder som vi inte tar upp här. Tre av metoderna, add, remove och set, är valfria att implementera (optional). Om man inte vill implementera dem låter man lämpligen metoderna alltid generera UnsupportedOperationException Både next och previous skall generera NoSuchElementException om det inte finns fler element vid en framåt- respektive bakåtiteration i listan. Observera att de metoder som sätter in, tar bort eller ändrar element noga specificerar var detta sker. Man uttrycker sig här i termer av vad föregående eller nästa anrop av next() ger och undviker på detta sätt att införa begreppet ”aktuellt element”. I stället inför man för att underlätta beskrivningen av operationerna en s.k. cursor position. Låt oss för enkelhets skull kalla detta för aktuell position eller enbart position på svenska. Positionen för en listiterator anses vara mellan det element som skulle returneras av ett anrop av previous och ett anrop av next. Om en lista har n element insatta finns det därför n+1 positioner som vi kan numrera 0..n enligt nedanstående figur. Listan och dess ”noder” Insatta objekt Objektens nummer Positioner 0 ^ 0 1 ^ 1 2 ^ 2 n-1 ^ n-1 ^ n Mot bakgrund av denna bild är det också enklare att beskriva resultaten av anrop av metoderna hasNext(), hasPrevious(), next() och previous(). hasNext() repektive hasPrevious() ger true om och endast om det finns minst ett element till höger om respektive till vänster om aktuell position. Ett anrop av next() flyttar aktuell position ett steg åt höger och returnerar det element som därvid passeras. previous() flyttar positionen ett steg åt vänster och returnerar mellanliggande element. Klassen LinkedList i Java har följande två operationer för att skapa listiteratorer: public ListIterator<E> listIterator();// ärvd från AbstractList public ListIterator<E> listIterator(int index); Den första skapar en listiterator med position 0. Den andra skapar en listiterator med den position som anges av parametern. Ett anrop listIterator() är alltså ekvivalent med ett anrop listIterator(0). De listiteratorer som returneras innehåller meningsfulla implementationer även av metoderna add, remove och set. Egenskaperna hos den returnerade listiteratorn kommenteras på följande sätt i dess dokumentation på nätet: The iterators returned by the this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the Iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future. __________________________________________________________________________________________ Uppgift 6 Antag att vi har en lista myList av typen LinkedList<Integer>. Skriv programsatser som utför följande a) Ändrar innehållet i alla element med tal större än 10 så att de innehåller talet 10. b) Söker upp första negativa tal. Om det finns läggs ett nytt element innehållande motsvarande positiva tal in omedelbart efter. c) Tar bort alla dubbletter ut listan. __________________________________________________________________________________________ Seminarieövning 3 Mål Att ge träning i rekursiv utformning av algoritmer med implementation i Java. Att ge kunskap om hur man beräknar rekursiva algoritmers effektivitet. Innehåll Övningar på att formulera, implementera rekursiva lösningar. Evaluering och analys av förslag till rekursiva algoritmer. Binära träd används i några av uppgifterna. Litteratur Rekursion behandlas i läroboken 7.1-7-6. Binära träd i kap 18. __________________________________________________________________________________________ Uppgift 1 I denna uppgift behandlas problemet att beräkna största talet i en vektor med n heltal (n >=1 ). Följande implementation av en algoritm för detta problem är väldigt ineffektiv: /** Tag reda på vilket av de n första elementen i v som är störst */ public int findMax(int[] v, int n) { if (n==1) { return v[0]; } else if (v[n-1] > findMax(v,n-1)) { return v[n-1]; } else { return findMax(v,n-1); } } Vad blir tidskomplexiteten i värsta fall för ett anrop findMax(v,n)? Ställ först upp den rekursionsformel som gäller för tidskomplexiteten och lös den sedan exakt med återsubstitution. Det går att göra en väsentligt effektivare implementation genom en enkel ändring. Utför ändringen och beräkna sedan tidskomplexiteten. Uppgift 2 Exemplen i denna uppgift är hämtade ur en studie gjord av Bruria Haberman och Haim Averbuch1. Genom att samla in studenters rekursiva lösningar på olika problem försökte man göra sig en bild av vilka misstag som var vanligast. Inte bara felaktiga lösningar var i detta sammanhang intressanta utan också sådana som är onödigt komplicerade. I en del av studien lät man också andra studenter titta på lösningarna för att evaluera dem. Låt oss knyta an till detta genom att studera nedanstående rekursiva lösningsförslag till några problem. Försök i första hand för varje lösning avgöra om den är korrekt eller ej. För lösningar som du anser korrekta kan du sedan gå vidare och ge ytterligare synpunkter t ex om de kan förenklas eller göras elegantare. Försök till sist utgående från de misstag och olämpligheter som du upptäckt formulera ett antal regler/goda råd som bör följas vid utformning av rekursiva algoritmer. Observera att lösningarna är schematiskt skrivna och inte i korrekt Java-kod. a) Fyra förslag till att beräkna n!: fac(n) om n == 0 return 1 annars return n*fac(n-1) -------fac(n) om n==0 return 1 annars om n == 1 return 1 annars return n*fac(n-1) -------fac(n) om n==1 return 1 1. The Case of Base Cases: Why are They so Difficult to Recognize? Student Difficulties with Recursion. Proceedings of The 7th Annual Conference on Innovation and Technology in Computer Science Education Aarhus, Denmark June 2002. annars return n*fac(n-1) -------fac(n) return n*fac(n-1) -------- b) Två förslag att lösa problemet ”finns det en udda siffra i ett icke-negativt tal” rekursivt: hasOddDigit(num) om num == 0 return false annars om num % 2 == 1 return true annars return hasOddDigit(num/10) -------hasOddDigit(num) om num <= 9 om num % 2 == annars return annars om num % 2 == annars return -------- 1 return true false 1 return true hasOddDigit(num/10) c) Två förslag till till att undersöka om elementet x ingår i en lista (list.tail() används för att beteckna den lista som består av alla element utom det första i list): isMember(x,list) om list är tom return false annars om x == första elementet i list return true annars return isMember(x,list.tail()) -------isMember(x,list) om x == första elementet i list return true annars return isMember(x,list.tail()) -------- d) Två förslag att räkna antal noder i ett binärt träd med roten n: count(n) om n == null return 0 annars om n.left == null && n.right == null return 1 annars om n.left == null return 1 + count(n.right) annars om n.right == null return 1 + count(n.left) annars return 1 + count(n.left) + count(n.right) -------count(n) om n == null return 0 annars return 1 + count(n.left) + count(n.right) -------- __________________________________________________________________________________________ I uppgifterna 3-4 hanteras binära träd som byggs upp med hjälp av av följande två klasser. Klasserna finns i samma paket. Man kan då i BinaryTree använda attributen i BinaryNode genom att utnyttja punktnotation. public class BinaryNode<E> { E element; // nodens innehåll BinaryNode<E> left; // refererar till vänster barn BinaryNode<E> right; // refererar till höger barn /** Konstruktor. Ger noden innehållet x, left och right blir null */ BinaryNode(E x) { element = x; left = right = null; } // metoder } public class BinaryTree<E> { protected BinaryNode<E> root;// refererar till rotnoden // metoder } __________________________________________________________________________________________ Uppgift 3 Skriv i klassen BinaryTree en metod som med rekursiv teknik beräknar trädets höjd. Uppgift 4 Ett binärt träd sägs vara höjdbalanserat (eller ibland bara balanserat) om det för varje nod i trädet gäller att höjden av dess vänstra och högra underträd skiljer sig på högst ett. Ex: Balanserat Ej balanserat Skriv i klassen BinaryTree en operation som med rekursiv teknik undersöker om trädet är balanserat. Seminarieövning 4 Mål Att visa hur interfacet Comparator<E> för jämförelseoperationer kan användas. Att ge övning i att utforma rekursiva metoder på träd samt att introducera designmönstret Template. Innehåll Exempel på utformning av en ADT för en sorterad samling element med hjälp av interfacet Comparator<E>. Exempel på rekursiva implementationer av metoder för träd. Introduktion av begreppet designmönster speciellt ”the Template pattern”. Litteratur Läroboken 6.4, 3.7 __________________________________________________________________________________________ Interfacet Comparator Vissa abstrakta modeller för samlingar av objekt bygger på att objekten går att jämföra med varandra. Detta gäller t ex prioritetskö där vi har operationer som ”sök upp minsta”, ”tag bort minsta”. Modellen lexikon (dictionary) med operationerna insert, find och delete förutsätter bara att vi kan jämföra objekten avseende likhet. När vi t ex använder en hashtabell för att implementera ett lexikon utnyttjar vi enbart equals-metoden. Andra implementationsalternativ bygger emellertid på att elementen är ordnade. Det gäller för binära sökträd och flera andra typer av sökträd. När vi utnyttjar dessa för implementation av lexikon öppnar sig därför också möjligheten att lägga till operationer såsom ”sök minsta”, ”sök största” och ”räkna upp (iterera) i storleksordning”. Vi har hittills i dessa sammanhang utgått från att de objekt vi hanterar implementerar interfacet Comparable. Detta innebär emellertid vissa begränsningar. Antag att vi skall implementera en ADT där vi behöver kunna göra jämförelser mellan de ingående objekten och att vi utformar den enligt följande skiss: public class SomeSortedCollection<E extends Comparable<? super E>> { /** Konstruktor. Skapa en tom samling. */ public SomeSortedCollection() {...} /** Sätt in x i samlingen. */ public void insert(E x) { ...} /** Sök upp ett objekt som matchar x i samlingen. */ public E find(E x) {...} ... övriga operationer } Antag nu att vi skall använda klassen för en samling Person-objekt och att jämförelseoperationen för dessa använder personens namn (String) enligt: public class Person implements Comparable<Person> { private String name; private int phoneNbr; public Person(String name, int phoneNbr) { this.name = name; this.phoneNbr = phoneNbr; } public int compareTo(Person rhs) { return name.compareTo(rhs.name); } public boolean equals(Object rhs) { return compareTo((Person) rhs) == 0; } public int getPhoneNbr() { return phoneNbr; } public String getName() { return name; } } Om vi använder SomeSortedCollection<Person> som en telefonkatalog för ett antal personer verkar det vettigt att samlingen är sorterad efter namn. Sökoperationen man vill utföra är nog oftast av typen ”givet ett namn, sök upp motsvarande person i katalogen”, vilket nu kan utföras med följande satser: SomeSortedCollection<Person> myCollection = new SomeSortedCollection<Person>(); ... här sätter vi in ett antal element ... Person toFind = new Person("Kalle",0); Person found = myCollection.find(toFind); if (found != null) { System.out.print("Kalle has phone number: " + found.getPhoneNbr()); } Vi har här förutsatt att det finns metoder getName() och getPhoneNbr() för att nå attributen name och phoneNbr i klassen Person. Antag nu att man önskar att även snabbt kunna söka upp en person vars telefonnummer man känner till. Om klassen SomeSortedCollection har en iterator-operation kan man naturligtvis använda sig av denna och iterera sig fram element för element tills man hittar en person med rätt nummer. Detta blir dock ineffektivt. Man skulle kunna få effektivare sökning med avseende på telefonnummer om man sätter in personerna i två olika objekt av typen SomeSortedCollection, en som är sorterad efter namn och en som är sorterad efter telefonnummer. Problemet är att med den nuvarande utformningen är detta inte genomförbart. Klassen Person skulle ju då behöva ha två olika implementationer av metoden compareTo; den vi redan implementerat ovan och ytterligare en som jämför på telefonnummer. För att lösa problemet kan man i stället införa klasser som enbart innehåller jämförelseoperationer. Objekt av sådana klasser (s.k. funktionsobjekt) kan sedan överföras till aktuell collection-klass, som använder dem för att göra jämförelser. Nedan beskrivs närmare hur detta går till. I Javas standardbibliotek (paketet java.util) finns ett interface Comparator<E> som innehåller en metod för att jämföra två objekt med varandra: public interface Comparator<E> { /** Returnera resultatet av jämförelse mellan lhs och rhs. Resultatet är: <0 om lhs är mindre än rhs >0 om lhs är större än rhs =0 om lhs och rhs är lika */ int compare(E lhs, E rhs); } Man kan låta en eller flera klasser implementera detta interface. Om vi vill jämföra våra Person-objekt på dels namn dels på telefonnummer kan vi införa följande båda klasser: public class NameComparator implements Comparator<Person> { public int compare(Person lhs, Person rhs) { return lhs.getName().compareTo(rhs.getName()); } } public class NbrComparator implements Comparator<Person> { public int compare(Person lhs, Person rhs) { return lhs.getPhoneNbr() - rhs.getPhoneNbr(); } } __________________________________________________________________________________________ Uppgift 1 Vi har tidigare poängterat att man när man inför jämförelseoperationer för en klass också bör omdefiniera metoden equals så att operationerna är konsistenta. När vi använder interfacet Comparable ser vi därför normalt till att equals ger resultatet true precis när compareTo ger reultatet 0. Hur går det med detta krav nu när jämförelse för en viss klass kan definieras på flera olika sätt? Läs gärna om interfacen Comparable och Comparator i dokumentationen av Javas standardklasser för att se vad som sägs där. __________________________________________________________________________________________ Vi behöver nu inte längre kräva att de objekt som sätts in i en samling SomeSortedCollection nödvändigtvis implementerar interfacet Comparable. Vi ger därför vår klass en ny rubrik: public class SomeSortedCollection<E> Vi måste dock se till att samlingen på något sätt kan jämföra de i samlingen ingående objekten genom att införa ett attribut (cmp) av typ Comparator<? super E> i SomeSortedCollection. Detta attribut får sitt värde i en ny konstruktor: public SomeSortedCollection(Comparator<? super E> cmp) { this.cmp = cmp; } I implementationen av operationerna i SomeSortedCollection använder man nu cmp varje gång man behöver utföra en jämförelse mellan två objekt i samlingen enligt: cmp.compare(x,y). I fallet med de två personsamlingarna ovan kan en användare av SomeSortedCollection göra så här: SomeSortedCollection<Person> nameCollection = new SomeSortedCollection<Person>(new NameComparator()); SomeSortedCollection<Person> numberCollection = new SomeSortedCollection<Person>(new NbrComparator()); Person p = new Person("Kalle",12345); nameCollection.insert(p); // Sätt in personen numberCollection.insert(p); // i båda samlingarna. if (nameCollection.find(new Person("Kalle",0) != null) { ... } if (numberCollection.find(new Person("",12345) != null) { ... } __________________________________________________________________________________________ Uppgift 2 Antag att man vill att samlingen skall ha även den parameterlösa konstruktorn public SomeSortedCollection(); och att om denna används man i implementationen av SomeSortedCollection<E> skall utgå ifrån att de objekt som kommer att sättas in implementerar interfacet Comparable<E>, vilket i Javas standardklasser brukar kallas att de jämförs på ”det naturliga sättet”. I så fall kan man i klassen SomeSortedCollection implementera en (inre) klass som implementerar interfacet Comparator<E> och i vilken man utgår från att objekten är av typen Comparable<E>. Sedan tilldelar man i den parameterlösa konstruktorn attributet cmp en instans av denna klass. Gör en sådan implementation av en ”default-comparator”. Tips finns i läroboken avsnitt 6.4. __________________________________________________________________________________________ Rekursiv utformning av algoritmer för träd I nästa uppgift behandlar vi några exempel på rekursiva algoritmer på träd. I exemplet används nedanstående klasser för att representera binära träd: class BinaryNode<E> { E element; // Innehåll i noden. int preOrderNbr; // Se uppgiftstexten nedan. BinaryNode<E> left, right; BinaryNode(E x) { element = x; left = right = null; } ... } public class BinaryTree<E> { private BinaryNode<E> root; public BinaryTree() { root = null; } ... } __________________________________________________________________________________________ Uppgift 3 I klassen BinaryTree skall en metod preOrderNumber()implementeras. Metoden skall ge nodernas heltalsattribut preOrderNbr värde. Den första nod som besöks vid en preordergenomgång av trädet skall få nummer 1 den andra nummer 2 o.s.v. a) Följande implementation är inte korrekt. Visa detta genom att undersöka vilka nummer noderna får i ett träd bestående av en rot och två barn. public void preOrderNumber() { preOrderNumber(root,1); } private void preOrderNumber(BinaryNode<E> n, int nbr) { if (n!=null) { n.preOrderNbr = nbr++; preOrderNumber(n.left, nbr); preOrderNumber(n.right,nbr); } } b) Gör en korrekt implementation av metoden. c) Lägg till attribut inOrderNbr och postOrderNbr i klassen BinaryNode<E>. Implementera sedan en metod number() i klassen BinaryTree<E>. Metoden skall ge samtliga tre heltalsattribut i noderna värden. Värdena skall motsvara nodens ordningsnummer vid preorder-, inorder- respektive postordergenomgång av trädet. Metoden skall bara gå igenom trädet en gång. __________________________________________________________________________________________ Designmönster I ett designmönster (eng: design pattern) beskriver man problem som dyker upp gång på gång inom programvaruproduktion. Man försöker beskriva lösningen på ett så generellt sätt att den kan tillämpas i alla de sammanhang där problemet uppträder. Designmönster handlar inte om hur man t ex designar klasser för hantering av länkade listor eller hashtabeller. De är snarare beskrivningar av hur ett antal klasser och objekt skall förhålla sig till varandra och kommunicera för att lösa ett allmänt designproblem i ett visst sammanhang. I ett designmönster tar man fram de väsentliga aspekterna hos problemet. Man försöker också ge mönstret ett namn som väl beskriver dessa aspekter. Mönster har ofta namn som är metaforer såsom Visitor, Adapter, Decorator,... Avsikten med designmönster är att en gång för alla beskriva lösningen på ett designproblem så att inte alla som råkar ut för samma problem skall behöva uppfinna hjulet på nytt. Idén har sitt ursprung inom arkitekturen där Christopher Alexander et.al. 1977 i en bok med titeln A Pattern Language lade grunden för tekniken. Ett citat ur boken: ”Each pattern describes a problem that occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this a million times over, without ever doing it in the same way twice”. I kursen har vi redan stött på åtminstone ett designmönster, nämligen Iterator. Det bakomliggande problemet är här ett behov av att kunna gå igenom alla element i en samling (Collection). Lösningen på problemet är (i Java) att införa ett interface med de operationer som behövs för att uttrycka iterationen t ex hasNext() och next() och att för alla klasser som beskriver samlingar införa en operation som returnerar ett objekt av en klass som implementerar detta interface (metod med signaturen Iterator iterator()). I objektorienterade språk som inte har interface får man i stället införa en abstrakt klass Iterator. Lägg märke till att lösningen inte innebär att man en gång för alla kan skriva koden. Interfacet Iterator kan skrivas men sedan måste var och en som implementerar en collection-klass implementera metoden iterator (och också skriva en klass som implementerar interfacet Iterator på ett för den aktuella samlingen korrekt sätt). Vårt designmönster beskriver alltså hur klasserna förhåller sig till varandra/kommunicerar genom att föreskriva existensen av metoden iterator och interfacet Iterator. Vi skall här beskriva ytterligare ett designmönster nämligen Template. Vi börjar med ett konkret exempel där mönstret kan användas. Exempel. Ett aritmetiskt uttryck som består av heltal och de fyra binära operatorerna +, –, * och / kan representeras av ett binärt träd där löven innehåller talen och övriga noder innehåller operatorer: * + + 5 2 8 * 10 3 (5 + 2) * (8 - 3) 2 6 10 + 2 * 6 Figur 1: Två binära träd som representerar aritmetiska uttryck Värdet av ett träd definieras rekursivt som rotens operator tillämpad på värdet av de båda underträden. Värdet av ett löv är talet i lövet. Noderna i ett uttrycksträd där operanderna utgör heltal kan representeras av följande klasser: /** Basklass för noder i ett uttrycksträd */ abstract class ExpressionNode { } /** Abstrakt klass, representerar en operatornod i ett uttrycksträd */ abstract class BinOpNode extends ExpressionNode { protected ExpressionNode left, right; // Refererar till barnen. public BinOpNode(ExpressionNode left, ExpressionNode right) { this.left = left; this.rigth = right; } } /** Representerar en operandnod i ett uttrycksträd */ class OperandNode extends ExpressionNode { private int val; // heltalsvärdet public OperandNode(int val) { this.val = val; } } ExpressionNode OperandNode BinOpNode AddNode SubNode MulNode DivNode Figur 2: Klassdiagram som visar de ingående klasserna och hur de ärver varandra /** Representerar en additionsnod i ett uttrycksträd */ class AddNode extends BinOpNode { public AddNode(ExpressionNode left, ExpressionNode right) { super(left,right); } } /** Representerar en subtraktionsnod i ett uttrycksträd */ class SubNode extends BinOpNode { // konstruktor som i AddNode } /** Representerar en multiplikationsnod i ett uttrycksträd */ class MulNode extends BinOpNode { //konstruktor som i AddNode } /** Representerar en divisionsnod i ett uttrycksträd */ class DivNode extends BinOpNode { // konstruktor som i AddNode } I klassen ExpressionNode skall vi införa en metod: printTree(String indent). Metoden skall skriva ut trädet så att dess struktur blir tydlig. Utskrift för de båda exempeluttrycken i figur 1 skall se ut så här: Uttryck 1 Mul Add 5 2 Sub 8 3 Uttryck 2 Add 10 Mul 2 6 Vid utskrift av barnen till en förälder indenterar man med två blanktecken mer än vid utskrift av föräldern. Vi inför följande metoder i ExpressionNode: public void printTree() { printTree(""); } protected abstract void printTree(String indent); __________________________________________________________________________________________ Uppgift 4 Implementera metoden printTree(String indent) i alla de fem konkreta klasserna. __________________________________________________________________________________________ I lösningen till uppgift 4 upprepas likartad kod i de fyra implementationerna av printTree(String indent) i AddNode, SubNode, MulNode och DivNode. Det enda som skiljer dem åt är vilken sträng (”Add”, ”Sub”, ”Mul” , ”Div”) som skrivs ut. Vi kan undvika att upprepa de gemensamma delarna av koden genom att i stället implementera printTree i den abstrakta klassen BinOpNode enligt följande mönster: Skriv ut opCode() med rätt indentering; Gör rekursivt anrop för utskrift av vänster underträd; Gör rekursivt anrop för utskrift av höger underträd; Metoden i BinOpNode anropar en abstrakt metod opCode som returnerar en sträng motsvarande den aritmetiska operationens namn: protected abstract String opCode(); Observera att vi inte behöver kontrollera om barnen finns. Ett objekt av typen BinOpNode har alltid två barn. Metoden printTree(String indent) behövs nu inte längre i de fyra subklasserna till BinOpNode. I stället skall dessa implementera metoden opCode. __________________________________________________________________________________________ Uppgift 5 Implementera metoden ovan i klassen BinOpNode. Implementera också opCode i de fyra konkreta subklasserna till BinOpNode. __________________________________________________________________________________________ Den nya utformningen av lösningen i uppgift 5 är ett exempel på användning av designmönstret Template. Det går ut på att man i en abstrakt superklass implementerar ett algoritmskelett där detaljer som inte kan utföras ”sparas” till subklasserna. Dessa detaljer utförs i skelettet genom anrop av abstrakta metoder som sedan implementeras av konkreta subklasser. I vårt exempel har vi i BinOpNode skrivit ett algoritmskelett som skriver ut ett binärt uttrycksträd genom att gå igenom trädet i preorder. Utskriften av roten i underträdet är en detalj som måste implementeras av de konkreta subklasserna och som därför i skelettet utförs genom anrop av en abstrakt metod opCode. __________________________________________________________________________________________ Uppgift 6 Inför följande metod i ExpressionNode: /** Beräknar värdet på det uttryck i vilket denna nod är rot */ public abstract int value(); Implementera operationen value . Tillämpa Template-mönstret vid implementation. I detta fall innebär det att en rekursiv implementation av value implementeras i klassen BinOpNode och att denna använder sig av en annan (abstrakt) metod som implementeras i var och en av de fyra konkreta subklasserna till BinOpNode. Dessutom implementeras value i klassen OperandNode. __________________________________________________________________________________________ Seminarieövning 5 Mål Att ge kunskaper om heapar, deras representation och användning. Att förstå hur en graf kan implementeras med hjälp av närhetslistor. Innehåll Diskussion och jämförelse mellan olika möjliga representationer av heapar och binära träd. Grafrepresentation i Java. Litteratur Heapar, prioritetsköer: 6.9, 21.1-21.4. Grafer14.1. __________________________________________________________________________________________ Uppgift 1 Vektorn är en lämplig representation av en binär heap eftersom en sådan är ”fylld” på alla nivåer utom möjligen den som ligger längst bort från roten. På denna sista nivå ligger dessutom noderna samlade längst till vänster. Vektorrepresentationen av en binär heap med n noder kan därför utnyttja platserna 1..n. Roten placeras på plats 1 och barnen till nod på plats i finns på platserna 2i och 2i+1. Fadern till noden på plats i finns på plats i/2. Man undviker att använda plats 0 i vektorn dels för att få dessa enkla samband mellan noder och deras barn, dels för att det är praktiskt att ha en vaktpost på plats 0 när man implementerar metoder för heapen. a) Vektorn är inte lika lämplig som representation av binära träd i allmänhet. Tänk efter hur stor vektor som skulle krävas för att representera ett binärt träd med n noder i följande två fall • trädet är skevt, dvs har maximal höjd • trädet har två nivåer mer än vad en binär heap med samma antal noder har. b) Den vanliga länkade strukturen för binära sökträd är inte så lämplig att använda vid implementationen av en binär heap bl a eftersom det är svårare än i en vektor att ange platsen för nästa insättning. Ett sådant träd representeras ju i princip av en referens till roten och det gäller då att beskriva hur man skall gå nedåt i trädet för att hamna på den plats där nästa insättning får göras. Finns det något sätt att beskriva denna väg? Tips: studera förhållandet mellan den binära talrepresentationen av antalet noder i trädet och vägen från roten till rätt plats. Uppgift 2 Den implementation av prioritetskö som görs med hjälp av en binär heap (BinaryHeap) i läroboken innehåller ingen metod för att slå samman två prioritetsköer. Beskriv hur man genom att använda metoderna i klassen gör för att slå samman två prioritetsköer till en ny. Vad blir tidskomplexiteten om de båda ursprungliga köerna innehåller n element vardera? Beskriv också hur man i stället kan implementera en metod i klassen BinaryHeap som utför motsvarande operation. Vad blir tidskomplexiteten då? Uppgift 3 Antag att man har tillgång till en klass för hantering av prioritetskö med metoderna isEmpty, findMin, deleteMin och insert. Man behöver i något sammanhang ha heltal i en prioritetskö och utföra operationer som plockar ut största talet i stället för minsta och som tar reda på det största i stället för det minsta. Vad man egentligen behöver är alltså en max-heap i stället för en min-heap. Hur kan man lösa detta utan att behöva göra en helt ny implementation? Det finns flera sätt. __________________________________________________________________________________________ Följande klasser kan användas för att representera en riktad graf: public class DiGraph<V,E> implements Iterable<DiGraph<V,E>.Vertex> { /** Constructs an empty directed graph */ public DiGraph(); /** Inserts a new vertex in the graph. value is the value associated with the newly inserted vertex. Returns the newly created vertex. */ public Vertex addVertex(V value); /** Inserts a new directed edge in this graph. value is the value associated with the newly created edge. source is thesource vertex of the newly created edge. destination is the destination edge of the newly created edge. NoSuchElementException is thrown if source or destination is not present in this graph. */ public void addEdge(E value, Vertex source, Vertex destination) throws NoSuchElementException; /** Creates an iterator for the vertices of this graph. */ public Iterator<Vertex> iterator; /** Marks all vertices of this graph as not visited. */ public void unvisit(); /** Returns a String representation of the vertices and edges of the graph. */ public String toString(); } Noderna respektive bågarna i grafen representeras av följande inre klasser: public class Vertex implements Iterable<Edge> { /** Returns the value associated with this vertex. */ public V getValue(); /** Sets the value associated with this vertex. */ public void setValue(V value); /** Creates an iterator over the outgoing edges of this vertex. */ public Iterator<Edge> iterator(); /** Marks this vertex as visited. */ public void visit(); /** Returns true if this vertex is visited, otherwise false. */ public boolean isVisited(); /** Marks the vertex as not visited. */ public void unvisit(); /** Returns a String representation of the data associated with this vertex.*/ public String toString(); } public class Edge { /** Returns the value associated with this edge. */ public E getValue(); /** Sets the value associated with this edge. */ public void setValue(E value); /** Returns the source vertex of this edge. */ public Vertex getSource(); /** Returns the destination vertex of this edge. */ public Vertex getDestination(); /** Returns a String representation of the data associated with this edge. */ public String toString(); } Observera att klasserna Vertex och Edge är inre klasser i klassen DiGraph. Det gör att de i sin implementation kan använda typparametrarna V respektive E för data som knyts till noder respektive bågar. Dokument av grafklassen och dess inre klasser finns också att nå via adressen http://www.cs.lth.se/EDA027/ coursedocs. När vi skall använda klasserna i egna implementationer behöver vi deklarera objekt som representerar grafer, noder och bågar. Det görs enligt följande exempel där vi skapar en graf där noderna innehåller data av typen String och bågarna innehåller data av typen Integer: DiGraph<String,Integer> g = new DiGraph<String,Integer>(); DiGrapg<String,Integer>.Vertex v = g.addVertex("Lund"); DiGraph<String,Integer>.Vertex w = g.addVertex("Helsingborg"); DiGraph<String,Integer>.Edge fromLundToHbg = g.addEdge(58,v,w); DiGraph<String,Integer>.Edge fromHbgToLund = g.addEdge(58,w,v); Noder i en graf med typ V knuten till noder och typen E knuten till bågarna deklareras alltså genom DiGraph<V,E>.Vertex respektive DiGraph<V,E>.Edge. För att traversera alla noder i en graf g kan följande mönster användas (eftersom grafklassen implementerar Iterable): for (DiGraph<V,E>.Vertex v : g) { // behandla v } Om man vill besöka samtliga grannar till en nod v kan man göra så här (eftersom nodklassen också implementerar Iterable): for (DiGraph<V,E>.Edge e: v) { DiGraph<V,E>.Vertex w = e.getDestination(); // behandla w } Om vi väljer att implementera en egen klass som ärver från DiGraph<V,E> så kan vi använda de enkla typnamnen Vertex respektive Edge för noder respektive bågar. Ex: public class MyGraph<V,E> extends DiGraph<V,E> { /** Tag reda på antalet noder i denna graf. */ public int nbrOfVertices() { int i = 0; for (Vertex v: this) i++; return i; } } Klassen DiGraph representerar en riktad graf. Den går dock bra att använda även för att representera oriktade grafer genom att man låter varje oriktad båge mellan två noder a och b representeras av två riktade bågar, en från a till b och en från b till a. Se tidigare exempel där en (icke enkelriktad) väg mellan Lund och Helsingborg motsvaras av två bågar i grafen, en i vardera riktningen. __________________________________________________________________________________________ Uppgift 4 En oriktad graf sägs vara sammanhängande om varje par av noder i grafen är förbundna med en väg (en följd av bågar). Om den inte är sammanhängande består den av ett antal komponenter som var för sig utgör sammanhängande delgrafer. T ex består följande graf av fyra komponenter En metod som räknar antalet komponenter i grafen kan implementeras med djupetförst-genomgångar av grafen (eftersom en djupetförst-genomgång utgående från en nod x besöker alla noder i samma komponent som x). Implementera en sådan metod i en subklass till DiGraph: public class MyGraph<V,E> extends DiGraph<V,E> { /** Räknar antalet komponenter i denna graf */ public int nbrOfComponents() {...} } Seminarieövning 6 Mål Att kunna känna igen problem som låter sig formuleras med graf som abstrakt modell och att behärska en rad viktiga grafalgoritmer. Att förstå principerna i grundläggande sorteringsalgoritmer och hur dessa kan användas för att lösa även andra typer av problem. Innehåll Grafalgoritmer för bl a bestämning av kortaste väg och topologisk sortering. Övning i att formulera problem med hjälp av grafer och identifiera lämplig algoritm. Tillämpning av grundprinciperna i ett par av de vanligaste sorteringsalgoritmerna (Quicksort och Mergesort) för att lösa andra problem. Litteratur Grafer: 14.1-14.3, 14.5.1-14.5.3. Sortering: 8.1-8.3, 8.5-8-6, 21.5. __________________________________________________________________________________________ Uppgift 1 Behandlar Dijkstras algoritm för beräkning av kortaste väg. a) Visa med ett enkelt exempel att Dijkstras algoritm ger fel svar om inte samtliga bågar i grafen har kostnad >=0. b) Givet en graf med såväl positiva som negativa kostnader på bågarna. För att kunna beräkna kortaste vägar även i detta fall med Dijkstras algoritm förelås följande: Öka kostnaden på varje båge med en konstant c, vald så att alla kostnader blir >=0. Använd sedan Dijkstras algoritm för att konstruera kortaste vägar. Förklara varför denna metod inte fungerar. c) En mängd orter och ett vägnät kan representeras av en riktad graf. Orterna representeras av noder i grafen. En väg som förbinder orterna a och b representeras med en båge från a till b. Om vägen inte är enkelriktad representeras den av två riktade bågar, en från a till b och en från b till a. Till bågarna knyts kostnader som anger vägsträckans längd. Med Dijkstras algoritm kan man beräkna kortaste vägar från en nod till alla andra noder till vilka det finns en väg (en serie av bågar). Antag nu att en enda av orterna har ett sjukhus. Då är man snarare intresserad av att kartlägga kortaste vägar från alla andra noder till sjukhusorten. Hur kan man göra? Uppgift 2 Utgå från beskrivningen i uppgift 2c men anta nu att det finns sjukhus på två av orterna u och v. Man är då intresserad av att beräkna kortaste väg från alla orter till den av orterna u och v som ligger närmast. Hur gör man? Uppgift 3 En viss läroboksförfattare beskriver sambanden mellan kapitlen i en av sina läroböcker med en graf. Ett kapitel motsvaras av en nod. Båge från en nod u till en nod v betyder att det kapitel som motsvarar v bygger på det kapitel som motsvarar noden u. A. Java primer B. Analysis Tools C. Stacks, Queues E. Trees F. BST G. PrioQueues H. Sorting I. Graphs D. Lists a) Hur gör man för att ta reda på i vilken ordning man kan läsa samtliga kapitel? b) Hur gör man för att ta reda på vilka kapitel som är nödvändiga att läsa före ett visst kapitel. Man vill samtidigt veta i vilken ordning dessa kapitel kan läsas. Uppgift 4 Det händer i sällsynta fall att valutakurser sätts på ett sådant sätt att man kan tjäna på att växla från en valuta via ett antal andra tillbaks till den ursprungliga. Exempel: Sommaren 2001 var det oroligt på den turkiska valutamarknaden och den amerikanska dollarn steg onormalt högt i värde i förhållande till den turkiska valutan (TL) turkiska lira aller turkiska pund. Man kunde köpa en US$ i Sverige för 10.50 SKr. I Turkiet kunde sedan en US$ växlas in till 1 350 000 TL. I Turkiet var en SKr värd 110 000 TL och således kunde (åtminstone i teorin) 1 350 000 TL växlas till 12.27 SKr. En vinst på 1.77 SKr. Detta leder oss till följande problemformulering: Givet en mängd valutor och växlingskurser dem emellan, undersök om det går att göra dylika vinster. Omformulera problemet som ett grafproblem Räcker kursens grafalgoritmer till för att lösa problemet? Om inte så leta i övriga avsnitt av bokens grafkapitel efter lämplig algoritm. Uppgift 5 Beskriv hur man med samma teknik som i sorteringsalgoritmen Quicksort effektivt kan göra för att ta reda på det i:e talet i storleksordning bland n tal i en vektor Uppgift 6 Effektiva sorteringsalgoritmer används ibland som en slags förbehandling i algoritmer för olika problem. Förklara hur sortering kan utnyttjas för att konstruera effektiva algoritmer för följande problem: a) Ta bort alla dubbletter ur en vektor med n tal. b) Givet ett tal x och en vektor M innehållande n tal. Avgör om det finns två tal i M vars summa är x. Visa också att om talen är sorterade från början så kan man lösa problemet på linjär tid. Uppgift 7 När man önskar slå samman k sorterade filer till en sorterad fil kan man göra på följande sätt. Man läser ett element från varje fil och placerar dem i någon datastruktur M. Det minsta elementet i M är nu det allra minsta på alla filerna. Tag bort detta element (e) ur M och skriv ut det först på en resultatfil. Sätt sedan i M in nästa element (om sådant finns) från den fil ifrån vilket e kom. Fortsätt på samma sätt tills det inte finns fler element i M. Vilken datastruktur bör man välja för M för att algoritmen skall bli effektiv? Vad blir komplexiteten?