Programmeringsteknik - fördjupningskurs David Karlsson 11 april 2012 1 Innehåll 1 Objektorientering 4 2 Kommentering av koden 6 3 Typer av fel 3.1 Exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 6 4 Autoboxing - unboxing 7 5 Generik 8 6 Java Collections Framework 6.1 Iteratorer . . . . . . . . . . . . . . . . . . . . . 6.2 Implementation av iterator . . . . . . . . . . . 6.3 foreach . . . . . . . . . . . . . . . . . . . . . . . 6.4 Söka efter objekt i en collection . . . . . . . . . 6.5 Sortera objekt i en collection . . . . . . . . . . 6.6 Listor . . . . . . . . . . . . . . . . . . . . . . . 6.7 Stackar . . . . . . . . . . . . . . . . . . . . . . 6.8 Köer . . . . . . . . . . . . . . . . . . . . . . . . 6.9 Implementering av köer och stackar . . . . . . . 6.9.1 Stack genom att delegera till LinkedList 6.9.2 Egen klass för stack och kö . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 10 11 12 13 13 14 17 17 18 19 19 7 Algoritmers effektivitet, tidskomplexitet 7.1 Ordonotation . . . . . . . . . . . . . . . . 7.2 Asymptotisk tidskomplexitet . . . . . . . 7.3 Olika tidskomplexiteter . . . . . . . . . . 7.3.1 Worst case och Average case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 22 22 22 22 8 Rekursion 8.1 Rekursiv metod . . . . . . . . . . . . . . . 8.2 Rekursiv lista . . . . . . . . . . . . . . . . 8.3 Samband mellan rekursion och induktion 8.4 Induktion för att verifiera rekursion . . . . 8.5 Divide and conquer approach . . . . . . . 8.6 Val av lösning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 23 25 28 29 29 31 9 Grafiska användargränssnitt 32 10 Träd 32 10.1 Terminologi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 10.2 Binära träd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 10.2.1 Traversering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 2 11 Binära sökträd 11.1 Sökning i binära sökträd . . . . . . . . . . . . . . . 11.2 Implementering av binärt sökträd . . . . . . . . . . 11.2.1 Insättning: . . . . . . . . . . . . . . . . . . 11.2.2 Borttagning: . . . . . . . . . . . . . . . . . 11.3 Tidskomplexitet hos binära sökträd, en funktion av 11.4 Garanterat minimal höjd . . . . . . . . . . . . . . 11.5 Problematik: insättningsordning . . . . . . . . . . 11.6 Algoritmer för att hålla binära sökträd balanserade 11.7 Detektera obalans . . . . . . . . . . . . . . . . . . 11.8 Kostnad för att balansera ett träd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . höjden . . . . . . . . . . . . . . . . (AVL-träd) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 34 34 35 36 36 38 38 38 39 39 12 Set 12.1 Sorted Set . . . . . . . . . . . 12.2 Set klasser . . . . . . . . . . . 12.2.1 TreeSet . . . . . . . . 12.2.2 Interfacet Comparator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 40 40 40 40 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Map 42 13.1 Map klasser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 13.2 Några metoder i Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 13.3 Implementera Map med sökträd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 14 Hashtabeller 14.1 Sluten hashtabell . . . . . . . . . . . . 14.2 Öppen hashtabell (separate chaining) . 14.3 Metoden hashCode() . . . . . . . . . . 14.3.1 Sökning i öppen hashTabell . . 14.4 Använda HashMap . . . . . . . . . . . 14.5 Använda HashSet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 44 45 45 45 45 46 15 Prioritetsköer 46 15.1 PriorityQueue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 15.2 Implementering av prioritetskö - tidskomplexiteter . . . . . . . . . . . . . . . . . . . 47 16 Heapar 16.1 Heap i vektor . . . . . . . . . . . . . . 16.1.1 Insättning . . . . . . . . . . . . 16.1.2 Tidskomplexitet för insättning 16.1.3 Peek . . . . . . . . . . . . . . . 16.1.4 Poll . . . . . . . . . . . . . . . 16.1.5 Tidskomplexitet för poll . . . . 16.2 Heap av osorterad samling . . . . . . . 16.2.1 Hjälpmetoden Heapify . . . . . 16.2.2 Heapifys tidskomplexitet . . . . 16.2.3 Heapify implementering . . . . 16.3 Sortering med prioritetskö . . . . . . . . . . . . . . . . . . 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 48 48 49 49 49 49 50 50 51 51 51 16.4 Heapsort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16.5 Alternativa prioritetsköer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Sortering 17.1 Urvalssortering . . . . . . . . . . . . . . . . 17.2 Insättningssortering . . . . . . . . . . . . . 17.2.1 Insättningssortering implementerad . 17.3 Mergesort . . . . . . . . . . . . . . . . . . . 17.3.1 Mergesort implementerad . . . . . . 17.3.2 Tidskomplexitet . . . . . . . . . . . 17.4 Quicksort . . . . . . . . . . . . . . . . . . . 17.4.1 Algoritm för quicksort . . . . . . . . 17.4.2 Implementering . . . . . . . . . . . . 17.4.3 Tidskomplexitet . . . . . . . . . . . 17.4.4 Val av pivotelement . . . . . . . . . 17.4.5 Förbättringar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Sorterings algoritmer i sammanfattning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 54 54 54 54 55 55 56 57 57 57 58 59 59 59 59 19 Bevis: 60 Detta häfte innehåller en sammanfattning av allt som gåtts igenom på föreläsningarna i kursen EDAA01, vid LTH. Kursen heter: Programmeringsfördjupning, men borde fortfarande gå under namnet Algoritmer och datastrukturer. 1 Objektorientering Java är ett objekt orienterat programmeringsspråk. Det innebär att lösningarna och problemen struktureras upp som objekt med egenskaper och metoder. Objektbeskrivningen är abstrakt, dvs enbart objektegenskaper som är viktiga och relevanta för vårt problem tas med. Definition 1 (Abstrakt datatyp) En abstrakt datatyp(ADT) är en specifikation av en klass eller ett interface. En klass är en implementation av en ADT. Exempel Komplexa tal En abstrakt datatyp kan till exempel vara en modell för komplexa tal innehållande funktioner för att hämta real- respektive imaginärdel samt addera mm. Motivation för abstrakta datatyper kan vara: • Man kan göra ett klassbibliotek. • Implementeringen kan ändras utan att resten av koden påverkas. • Koden kan utnyttjas för flera problem. • Koden kan testas sepparat. 4 Definition 2 (Interface) För att specifiera en abstrakt datatyp används ofta interface. Ett interface är ett gränssnitt innehållande alla metoddeklarationer för datatypen, det vill säga tomma metoder. Interfacet implementeras sedan av en eller flera klasser där även metoderna implementeras. Interfacet är således en specifikation på vad en klass ska innehålla. På så vis kan utvecklarna ta fram andra delar av programmet innan interfacen implementerats, då de vet vad de kommer innehålla. En klass kan implementera flera interface och ett interface kan implementeras av flera klasser. p u b l i c c l a s s MyComplexNumber2 implements ComplexNumber , C l o n e a b l e { // I m p l e m e n t e r a r a l l a metoder i i n t e r f a c e n ComplexNumber och Cloneable } Listing 1: Exempel implementera flera interface 5 2 Kommentering av koden För att skapa struktur i koden och underlätta för javadocs används pre- och postconditions. Preconditions beskriver vilka villkor som måste uppfyllas innan en metod exekveras. Postconditions beskriver hur exekveringen påverkar objektet. p u b l i c c l a s s BankAccount { private int balance ; ... /∗∗ ∗ D e p o s i t s t h e s p e c i f i e d amount . ∗ p r e : The s p e c i f i e d amount i s >= 0 ∗ p o s t : The s p e c i f i e d amount i s added t o b a l a n c e ∗ @param n The amount t o d e p o s i t ∗/ public void deposit ( i n t n) { balance = balance + n ; ... 3 Typer av fel Syntaxfel: Koden som skrivits följer inte java syntaxreglerna. Syntaxfel kan vara en glömd parentes eller en variabel som används innan deklarerats etc. Exekveringsfel: Koden som exekveras ger ett fel. Det kan tillexempel vara en index variabel som blir för stor för en vektor, nollpekare eller liknande. Logiska fel: Programmet körs som det ska men producerar ett felaktigt resultat. Tillexempel en implementation av ett digitalt filter som inte filterar. 3.1 Exception När ett exekveringsfel uppstår genereras ett undantag, exception. I vanliga fall avslutas då programmet och ett meddelande skrivs ut i java terminalen där felets typ beskrivs samt en stacktrace presenteras. Man kan undvika att programmet avbryts om man själv väljer att hantera ett exception. Detta görs med hjälp av catch-block. Exceptions kan vara checked eller unchecked: • Checked exception: ex file not found. • Unchecked exception: felet beror på programmeraren, t ex nullpointer eller index out of bounds. När man fångat en exception kan man rätta till felet. Det kan tillexempel vara ett fel av typen file not foundsom uppstår när man försöker öppna en fil som inte finns. I catch blocket kan man då presentera en dialog för användaren där man kan leta fram filen manuellt etc. try { // kod som kan g e n e r e r a e x c e p t i o n } c a t c h ( E x c e p t i o n C l a s s 1 e1 ) { // kod f ö r a t t h a n t e r a e x c e p t i o n av typen E x c e p t i o n C l a s s 1 6 } c a t c h ( E x c e p t i o n C l a s s 2 e2 ) { // kod f ö r a t t h a n t e r a e x c e p t i o n av typen E x c e p t i o n C l a s s 2 } finally { // kod som u t f ö r s e f t e r try −b l o c k e t e l l e r e f t e r catch −b l o c k e t } Man kan även generera exceptions själv med hjälp av throw: throw new I l l e g a l A r g u m e n t E x c e p t i o n ( " n < 0 " ) ; Koden ovan genererar en Illegal argument exception med meddelandet n < 0. Egna exception-typer kan skapas genom att man skriver egna exception klasser som extensions till ex. exception klassen eller runtime exception klassen. p u b l i c c l a s s SpellException extends Exception { ... } Kasta exceptions vidare till anropande funktion: p u b l i c Scanner c r e a t e S c a n n e r ( S t r i n g f i l e N a m e ) throws FileNotFoundException { // Här g e n e r e r a s e x c e p t i o n om f i l e n i n t e g å r a t t öppna Scanner s c a n = new Scanner ( new F i l e ( f i l e N a m e ) ) ; return scan ; } På detta visa behöver inte metoden hantera exceptions utan detta överlåts helt enkelt till den anropande koden. 4 Autoboxing - unboxing De primitiva datatyperna: boolean, char, int, mfl. är inte subtyper av klassen Object, och kan således inte användas som object-typen i listor och liknande (Collections-typer). Då måste värdena konverteras till object-typ. Motsvarande object-typ för int är Integer, char är Character, boolean är Boolean osv. Från och med Java 1.5 sköts typkonverteringen automatiskt t ex: I n t e g e r i = new I n t e g e r ( 1 2 ) ; i = i + 1 0 0 ; // K o n v e r t e r a s a u t o m a t i s k t t i l l I n t e g e r . i n t j =10; myArrayList . add ( j ) ; // K o n v e r t e r a s a u t o m a t i s k t t i l l I n t e g e r . U nboxing : Integer =⇒ int (1) Boxing : int =⇒ Integer (2) 7 5 Generik Generik innebär att man implementerar generella klasser eller interface som kan användas till flera olika objekttyper. Tidigare gav man metoderna parametrar av typen object för då kunde alla subtyper till object användas. Numera kan man använda typparametrar när man definerar en klass. Därefter typecastar man klassens instantiering till den typ man vill ha genom att sätta typparametrarna. p u b l i c c l a s s A r r a y L i s t <E> { . . . } ... A r r a y L i s t <I n t e g e r > myList = new A r r a y L i s t <I n t e g e r > ( ) ; Fördelen med det senare sättet är att kompilatorn nu kan upptäcka typfel. Om man har en lista med < Integer > och försöker lägga till en sträng uppstår ett fel då listan är deklarerad för att innehålla enbart Integerobjekt. Alla operationer är dock inte castade, tex ArrayList<E> innehåller metoderna: p u b l i c b o o l e a n c o n t a i n s ( Object x ) ; p u b l i c b o o l e a n remove ( Object x ) ; Detta för att användare som inte känner till typen ska kunna anropa metoderna med true/false som svar beroende på om objekten hittas. En generisk klass kan även ha flera typparametrar: p u b l i c c l a s s Pair<K, V> { p r i v a t e K key ; private V value ; p u b l i c P a i r (K key , V v a l u e ) { t h i s . key = key ; t h i s . value = value ; } public V getValue ( ) { return value ; } p u b l i c v o i d s e t V a l u e (V v a l ) { value = val ; } } ... Pair<S t r i n g , I n t e g e r > p a i r = new Pair<S t r i n g , I n t e g e r >(" June " , 3 0 ) ; i n t nbrDays = p a i r . g e t V a l u e ( ) ; Även nästlade typparametrar är tillåtna: p u b l i c i n t e r f a c e Set<E> { b o o l e a n add (E x ) ; int size (); b o o l e a n c o n t a i n s ( Object x ) ; 8 ... } ... Set<Set<S t r i n g >> // n ä s t l i n g Typparametrarna kan även begränsas. Om man till exempel vill vara säker på att klassen i typparametern implementerar comparable kan man skriva: p u b l i c c l a s s A S o r t e d C o l l e c t i o n <E e x t e n d s Comparable<E>> Vi kan därmed säkert använda metoden compareTo på objekt av typen E i implementeringen av ASortedCollection. Varning 1 Parametern till en generisk klass får inte vara en primitiv: SomeClass<int> c = ... // ej möjligt! Typvariabler kan inte användas för att skapa objekt: E x = new E(); // Fel! Typparametrar kan inte användas för att överlagra metoder: public class MyClass<T,U> public void p(T x) ... public void p(U x) ... // Fel! ... 6 Java Collections Framework Java collections är ett ramverk av abstrakta klasser, interface och konkreta klasser för samlingar av element, så som listor, köer mm. Java collections är hierarkiskt uppbyggt kring interfacet Collection. Se figur 1. Grunden i interfacet collection är metoderna: i n t e r f a c e C o l l e c t i o n <E> { b o o l e a n add (E x ) ; b o o l e a n c o n t a i n s ( Object x ) ; b o o l e a n remove ( Object x ) ; b o o l e a n isEmpty ( ) ; int size (); ... } Några klasser i java collections, som implementerats med respektive interface, visas nedan i 3. Queue =⇒ LinkedList, P riorityQueue List =⇒ ArrayList, LinkedList Set =⇒ HashSet SortedSet =⇒ T reeSet M ap =⇒ HashM ap SortedM ap =⇒ T reeM ap 9 (3) <<Interface>> SortedSet <<Interface>> Set <<Interface>> SortedMap <<Interface>> List <<Interface>> Queue <<Interface>> Map <<Interface>> Collection Figur 1: Java collections framework hierarkin av interface. De olika interfacen i collections skiljer sig enligt: Collection : En samling av element, där dubbletter tillåts. Queue : En samling av element som utgör en kö. List : En samling element där dubbletter tillåts och där positionering är mo?jlig (första, sista, element på plats i, ...). Set : En samling element där dubbletter är förbjudna. SortedSet : Som Set men med krav att elementen går att jämföra. Map : en samling av element, där varje element har en en nyckel och ett värde (jfr. lexikon) SortedMap : som Map men med krav att nycklarna går att jämföra. 6.1 Iteratorer p u b l i c i n t e r f a c e I t e r a t o r <E> { /∗∗ Returns t r u e i f t h e i t e r a t i o n has more e l e m e n t s . ∗/ b o o l e a n hasNext ( ) ; /∗∗ Returns t h e next e l e m e n t i n t h e i t e r a t i o n . ∗/ E next ( ) ; /∗∗ Removes from t h e u n d e r l y i n g c o l l e c t i o n t h e l a s t e l e m e n t r e t u r n e d by t h e i t e r 10 v o i d remove ( ) ; } Interfacet collection ärver från interfacet iterable, innehållande funktionen iterator som returnerar ett iteratorobjekt (se interface ovan). Iteratorobjektet används när man behöver gå igenom objekten i en collection. T ex en ArrayList med strängar som man vill jämföra. För att iterera objekten måste Iterator instantieras enligt nedan, där funktionen iterator används för att skapa en iterator tillhörande pList. //En s a m l i n g med Person o b j e k t d e f i n i e r a s och f y l l s : A r r a y L i s t <Person> p L i s t = new A r r a y L i s t <Person > ( ) ; ... // I t e r a t o r n i n s t a n t i e r a s . I t e r a t o r <Person> i t r = p L i s t . i t e r a t o r ( ) ; w h i l e ( i t r . hasNext ( ) ) { Person p = i t r . next ( ) ; // behandla v a r j e Person o b j e k t . ... } 6.2 Implementation av iterator p u b l i c c l a s s A r r a y C o l l e c i t o n <E> implements C o l l e c t i o n <E> { private E[ ] theCollection ; private int size ; // K o n s t r u k t o r . . . // Metoder . . . p u b l i c I t e r a t o r <E> i t e r a t o r ( ) { r e t u r n new A r r a y I t e r a t o r ( ) ; } p r i v a t e c l a s s A r r a y I t e r a t o r implements I t e r a t o r <E> { p r i v a t e i n t pos ; private ArrayIterator () { pos = 0 ; } p u b l i c b o o l e a n hasNext ( ) { r e t u r n pos < s i z e ; } p u b l i c E next ( ) { i f ( hasNext ( ) ) { E item = t h e C o l l e c t i o n [ pos ] ; pos++; r e t u r n item ; } 11 else { throw new NoSuchElementException ( ) ; } } p u b l i c v o i d remove ( ) { throw new U ns up po rt ed Op e ra ti on Ex ce pt io n ( ) ; } } } Ovan visas en klass som implementerar en förenklad version av Iterator (en korrekt implementering måste upptäcka om samlingen modifierats mellan next anropen och då generera en exception). Iterator har placerats som en inre klass och eftersom iterator funktionen i den överliggande klassen instantierar iteratorn kan iteratorklassen även vara privat. 6.3 foreach Ett enkelt sätt att iterera över collections och slippa skriva en instantiering av iteratorn varje gång är att använda sig av foreach. En foreach fungerar både för objekt av typ som implementerar Iterable och för vektorer. Nedan visas ett enkelt exempel där personList kan vara av valfri typ med stöd för foreach. f o r ( Customer c : p e r s o n L i s t ) { System . out . p r i n t l n ( c . getName ( ) ) ; } Begränsningen med foreach är dels att den inte tillåter användaren att använda iteratorn i koden, till exempel när man behöver ta bort element ur en lista: I t e r a t o r <Person> i t r = p L i s t . i t e r a t o r ( ) ; w h i l e ( i t r . hasNext ( ) ) { i f ( i t r . next ( ) . e q u a l s ( x ) ) { i t r . remove ( ) ; } } En annan begränsning uppstår då man vill iterera parallellt över flera samlingar, då får man använda iteratorer, se nedan. Däremot kan man göra nästlade foreach-loopar, d.v.s en foreach innuti en annan. A r r a y L i s t <Person> l i s t 1 , l i s t 2 ; ... I t e r a t o r <Person> i t r 1 = l i s t 1 . i t e r a t o r ( ) ; I t e r a t o r <Person> i t r 2 = l i s t 2 . i t e r a t o r ( ) ; w h i l e ( i t r 1 . hasNext ( ) && i t r 2 . hasNext ( ) ) { System . out . p r i n t l n ( i t r 1 . next ( ) + " " + i t r 2 . next ( ) ) ; } 12 6.4 Söka efter objekt i en collection Skriver man en klass Personsom innehåller ex. namn och personnummer som man sedan ska lägga i någon form av collection så måste man utvidga equals metoden, från början finns equals i superklassen object och fungerar så att den jämför om objekten är identiska. För att den ska fungera som vi vill på vår Person klass måste vi överskugga equals i object klassen med en likadan metod i Person. Som jämför namn och id-number, dvs innehållet i objekten. I implementationen nedan jämförs endast personnummer: p u b l i c b o o l e a n e q u a l s ( Object o b j ) { i f ( o b j i n s t a n c e o f Person ) { r e t u r n idNbr == ( ( Person ) o b j ) . idNbr ; } else { return f a l s e ; } En bättre implementation tillåter bara att objekt av samma typ jämförs. Detta görs med hjälp av metoden getClass: p u b l i c b o o l e a n e q u a l s ( Object o b j ) { i f ( o b j == t h i s ) { return true ; } i f ( o b j == n u l l ) { return f a l s e ; } i f ( t h i s . g e t C l a s s ( ) != o b j . g e t C l a s s ( ) ) { return f a l s e ; } r e t u r n idNbr == ( ( Person ) o b j ) . idNbr ; } 6.5 Sortera objekt i en collection För att kunna sortera innehållet i en collection måste jämförelsen utvidgas från att finna likhet till att jämföra objekten. Till detta ändamål finns interfacet Comparable < T > med funktionen compareTo(T x) som returnerar ett nummer, ett negativt tal betyde mindre än, ett positivt tal betyder större än och noll betyder lika. Person klassen ovan skulle kunna utvidgas så det implementerar comparable enligt: p u b l i c c l a s s Person implements Comparable<Person> { ... p u b l i c i n t compareTo ( Person x ) { r e t u r n idNbr − x . idNbr ; } } 13 Då kan och bör, för att ge konsistenta resultat, även equals metoden ersättas med: p u b l i c b o o l e a n e q u a l s ( Object o b j ) { i f ( o b j i n s t a n c e o f Person ) { r e t u r n compareTo ( ( Person ) o b j ) == 0 ; } else { return f a l s e ; } 6.6 Listor Definition 3 En lista är ordnad följd av element med följande egenskaper: • Det finns en före-efter relation. • Det finns ett positionsbegrepp (elementen behöver inte vara sorterade). Exempel på listor är den enkel-länkade och dubbel-länkade i figur 2 nedan. first next element next element next element null element objekt objekt objekt objekt (a) Enkel-länkad lista first previous next element previous next element previous null element objekt objekt objekt (b) Dubbel-länkad lista Figur 2 För att kunna traversera listan används en iterator. Här implementeras en enkel-länkad lista med en inre iteratorklass. Man får inte glömma metoden iterator i huvudklassen som skapar iterator objektet: p u b l i c c l a s s S i n g l e L i n k e d L i s t <E> implements I t e r a b l e <E>{ p r i v a t e ListNode<E> f i r s t ; p u b l i c I t e r a t o r <E> i t e r a t o r ( ) { r e t u r n new M y L i s t I t e r a t o r ( ) ; } public String toString () { S t r i n g B u i l d e r sb= new S t r i n g B u i l d e r ( ) ; sb . append ( ’ [ ’ ) ; ListNode<E> n= f i r s t ; w h i l e ( n!= n u l l ) { sb . append ( n . e l e m e n t . t o S t r i n g ( ) ) ; 14 i f ( n . next != n u l l ) { sb . append ( " , " ) ; } n=n . next ; } sb . append ( ’ ] ’ ) ; r e t u r n sb . t o S t r i n g ( ) ; } p u b l i c v o i d a d d F i r s t (E x ) { ListNode<E> n = new ListNode<E>(x ) ; n . next = f i r s t ; first = n; } public E removeFirst { i f ( f i r s t==n u l l ) { throw new NoSuchElementException ( ) ; } ListNode<E> temp = f i r s t ; f i r s t = f i r s t . next ; r e t u r n temp . e l e m e n t ; } p u b l i c v o i d addLast (E x ) { ListNode<E> n = new ListNode<E>(x ) ; i f ( f i r s t==n u l l ) { first = n; } else { ListNode<E> p = f i r s t ; w h i l e ( p . next != n u l l ) { p=p . next ; } p . next= n ; } } p u b l i c E removeLast ( ) { i f ( f i r s t==n u l l ) { throw new NoSuchElementException ( ) ; } i f ( f i r s t . next == n u l l ) { ListNode<E> temp = f i r s t ; f i r s t = null ; r e t u r n temp . e l e m e n t ; 15 } ListNode<E> p = f i r s t ; ListNode<E> p r e= n u l l ; w h i l e ( p . next != n u l l ) { p r e=p ; p = p . next ; } p r e . next=n u l l ; return p . element ; p r i v a t e s t a t i c c l a s s ListNode<E> { p r i v a t e E element ; // data som l a g r a s p r i v a t e ListNode<E> next ; // r e f e r e r a r t i l l n ä s t a nod ... } p r i v a t e c l a s s M y L i s t I t e r a t o r implements I t e r a t o r <E> { p r i v a t e ListNode<E> pos ; private myListIterator (){ pos= f i r s t ; } p u b l i c Boolean hasNext ( ) { r e t u r n pos != n u l l ; } p u b l i c E next ( ) { i f ( hasNext ( ) ) { ListNode<E> temp = pos ; pos=pos . next ; r e t u r n temp . e l e m e n t ; } else { throw new NoSuchElementException ( ) ; } } } } Med javas API följer två färdiga listklasser: ArrayList<E> som implelenterats med en vektor och LinkedList<E> som implementerats med en dubbel-länkad cirkulär struktur. ArrayList används då man enbart gör insättningar i slutet av listan, då andra insättningar gör att element måste flyttas. Metoderna för get och set är däremot snabbare i arrayList. Interfacet för listor, List, anger att det även ska finnas särskilda operationer som returnerar en ListIterator: listIterator och listIterator(int i), där den sistnämnda börjar på index i. Interfacet ListIterator<E> ärver Iterator<E> och utvidgar med stöd för insättning av element och bakåttra- 16 versering. p u b l i c i n t e r f a c e L i s t I t e r a t o r <E> e x t e n d s I t e r a t o r <E> { boolean hasPrevious ( ) ; E previous ( ) ; v o i d add (E x ) ; ... } Varning 2 ListIteratorn pekar inte på elementen i listan utan hela tiden mellan två element. Så previous och next returnerar objektet före och efter positionen i listan. Detta medför att det i en lista med n-element finns n+1 index (från 0 till n). 6.7 Stackar Definition 4 En stack är en följd av element där uttagning av element alltid sker på det element som satts in sist. The Last in is the first out (LIFO). Figur 3: Stack, LIFO. Det finns inget interface i javas API, det finns dock en klass Stack som ärver vector och ger stackfunktionalitet. Ett renodlat stack interface borde se ut ungefär så här: p u b l i c i n t e r f a c e Stack<E> { /∗∗ Lägg x ö v e r s t på s t a c k e n . ∗/ v o i d push (E x ) ; /∗∗ Tag b o r t och r e t u r n e r a ö v e r s t a e l e m e n t e t . ∗/ E pop ( ) ; /∗∗ Retu rnera ö v e r s t a e l e m e n t e t f r å n s t a c k e n . ∗/ E peek ( ) ; /∗∗ Undersök om s t a c k e n ä r tom . ∗/ b o o l e a n isEmpty ( ) ; } 6.8 Köer Definition 5 En kö är en följd av element där det element som lagts in först är det som kan tas ut eller tas bort. First in first out (FIFO). Ett interface för en kö i dess enklaste form skulle kunna vara: 17 OFFER POLL Figur 4: Kö, FIFO. p u b l i c i n t e r f a c e Queue<E> { /∗∗ S ä t t e r i n x s i s t i kön . ∗/ b o o l e a n o f f e r (E e ) ; /∗∗ Tar r e d a på f ö r s t a e l e m e n t e t i kön . ∗/ E peek ( ) ; /∗∗ Tar r e d a på och t a r b o r t f ö r s t a e l e m e n t e t i kön . ∗/ E poll (); /∗∗ Undersöker om kön ä r tom . ∗/ b o o l e a n isEmpty ( ) ; } Java.util har ett interface Queue som innehåller bland annat: • För att lägga till: boolean add(E x), boolean offer(E x) • För att ta bort: E remove(), E poll() • För att ta reda på: E element(), E peek(); (den första metoden på varje rad genererar en exception, medans den andra returnerar ett specifikt värde). 6.9 Implementering av köer och stackar Det finns några olika alternativ när man väljer hur man vill implementera sina köer eller stackar: 18 • Enklast är att använda en befintlig list-klass och enbart utnyttja de operationer som är specifika för en stack/ kö. • Implementera en egen klass men delegera funktionaliteten till en list-klass. • Implementera en egen klass med enkel datastruktur. 6.9.1 Stack genom att delegera till LinkedList p u b l i c c l a s s Stack<E> { p r i v a t e L i n k e d L i s t <E> e l e m e n t s ; p u b l i c push (E x ) { e l e m e n t s . push ( ) ; } p u b l i c E peek ( ) { r e t u r n e l e m e n t s . peek ( ) ; } ... 6.9.2 Egen klass för stack och kö En stack/kö kan implementeras med den enkla datastrukturer för enkel-länkad lista eller som en vektor. Fördelen med att implementera detta är att antalet metodanrop minskar jämfört om man skulle använda den delegerande metoden ovan. Enkel-länkad datastruktur för stack Stacken består av en referens till den första noden samt noder med next referens. Detta gör att alla operationer kan utföras på konstant tid, oberoende av stackens storlek. Enkel-länkad datastruktur för kö En kö implementeras som en enkel-länkad lista, med referenser till den första och sista noden samt noder med next referens. Också i kön kan alla operationer utföras på konstant tid oberoende av kölängd. Vektors datastruktur för stack En stack kan implementeras som en vektor med index för nästa lediga position. Detta ger en stack med konstant tid för operationerna, men bara så länge längden räcker till. Om man dubblar vektorlängden varje gång den överskrids kan operationerna fortfarande i medeltal gå att utföra på konstant tid. Vektors datastruktur för kö En kö kan implementeras som en vektor, om man använder index och låter vektorns struktur vara cirkulär så att första platsen är efterföljare till sista platsen. Vad som behövs utöver själva vektorn är index för första, respektive sista element, samt antalet element. Precis som i fallet med stacken måste storleken på kön ökas då den blivit full. 19 7 Algoritmers effektivitet, tidskomplexitet Definition 6 (Komplexitets begrepp) En algoritms komplexitet beskrivs ofta på tre nivåer: Effektivitet : Tidskomplexitet som funktion av problemets storlek. Minneskrav : Rumskomplexitet som funktion av problemets storlek. Svårighetsgrad : Ju mer komplicerad algoritm desto svårare att implementera och underhålla. Tidskomplexiteten är den mest intressanta analysen. En hög tidskomplexitet innebär att algoritmen är ineffektiv och dyr, medan en låg tidskomplexitet innebär en effektiv och billig algoritm Tiden för att lösa ett problem är ofta beroende av problemets storlek, men inte alltid. Om man ska summera 100 tal så går det generellt snabbare än att summera 100 000 tal. Men att ta ut det i:te talet ur en vektor går att göra på konstant tid oavsett vektorns storlek. Med tidskomplexitet menar vi ett mätverktyg som ger oss möjlighet att jämföra olika algoritmers effektivitet, oavsett vilken plattform de körs på. Vi vill även kunna avgöra hur komplexiteten beror på problemets storlek, kvadratiskt, logaritmiskt, linjärt, etc. Vi introducerar därför tidskomplexiteten som T (n), där n är problemets storlek. Exempel 1 (Tidskomplexiteten för att summera n-st tal) Vid summeringen utförs n-st additioner och lika många tilldelningar. Detta ger att T (n) = n. Den reella tidsåtgången på en plattform blir då c · T (n), där c är tidsåtgången för en addition på den specifika plattformen. 20 Exempel 2 (Summera vektor) Givet en vektor a=1,4,2,9,7 bilda en vektor med längd n, innehållande summan av de k första talen ur a, k ∈ 0, n − 1. Det vill säga bilda sum(k) för k ∈ 0, n − 1 . Första talet i sum vektorn blir 0, ty 0 tal från a, andra talet blir 0+a(0)=1, tredje 0+a(0)+a(1)=5. osv. =⇒ sum=0,1,5,7,16 Två algoritmer för problemet ovan tas fram och jämförs: for ( i n t k = 0 ; k < n ; k++) { sum [ k ] = 0 ; for ( i n t i = 0 ; i < k ; i ++) { sum [ k ] = sum [ k ] + a [ i ] ; } } Listing 2: Algoritm 1 De operationer som utförs flest gånger finns i den innersta for-loopen, den inre for loopen körs beroende på k-värdet i den yttre for-loopen: k-värde för yttre for-loopen: 0 1 2 ... j ... n-1 antal ggr inre for-loopen körs: 0 1 2 ... j ... n-1 Den yttre for-loopen körs n gånger och för respektive körning körs den inre loopen 0, 1, 2..., n − 1 , se Bevis 1. ggr det vill säga den inre loopen körs 0 + 1 + 2 + ... + n detta kan skrivas som n(n−1) 2 n2 Tidskomplexiteten blir så T (n) ≈ 2 för stora n sum [ 0 ] = 0 ; for ( i n t k = 1 ; k < n ; k++) { sum [ k ] = sum [ k −1] + a [ k − 1 ] ; } Listing 3: Algoritm 2 I algoritm 2 körs enbart en for-loop, då de tidigare värdena i summan utnyttjas räcker detta. forloopen utförs n − 1 gånger och ger således algoritmen en tidskomplexitet på T (n) = n − 1, där n är antalet element i vektorn sum. 21 7.1 Ordonotation Tidskomplexitets begreppet förenklas med införandet av ordonotation, i ordonotation stryks nämligen alla icke dominerande termer och om man så vill konstanter på dessa. Exempelvis: T (n) = 2n2 + n =⇒ T (n) i ordonotation : O(2n2 ) =⇒ O(n2 ) 7.2 (4) Asymptotisk tidskomplexitet När man uppskattar tidskomplexiteten med ordonotation brukar man kalla denna uppskattning för algoritmens asymptotiska tidskomplexitet. 7.3 Olika tidskomplexiteter En algoritm med tidskomplexiteten T (n) = O(1) sägs ha konstant tidskomplexitet. Nedan, i figur 5, visas några olika benämningar på tidskomplexitets-karakteristik sorterade efter respektive karakteristiks växande. Figur 5: Olika tidskomplexiteter. Tidskomplexiteten kan även bero på indata, om man till exempel ska söka ett tal i en vektor, som matas in metoden, beror tidskomplexiteten på indatan. 7.3.1 Worst case och Average case Då tidskomplexiteten beror på indatan brukar man ange ett worst-case (W (n)) och ett averagecase P (A(n)). Worst-case beräknas som W (n) = max(Ti (n)) medan average beräknas A(n) = Pi · Ti (n), vilket gör average svårare eftersom P måste skattas. Exempel 3 W (n) för linjärsökning där n är vektorns längd. I värsta fall hittas inte det sökta objektet och for-loopen utförs från 0....n − 1 det vill säga innehållet i for-loopen utförs n-gånger. Alltså är W (n) = n = O(n). A(n) för linjärsökning, med samma utgångsläge som ovan, fast med sannolikhet P att sökta värdet finns i vektorn. Sannolikheten att värdet finns på en viss position är Pn , det vill säga alla positioner är lika troliga. A(n) blir så en summa som innehåller alla möjliga placeringar av värdet, samt även risken att värdet saknas: X p A(n) = pi · Ti (n) = · (1 + 2 + 3 + 4 + ... + n) + (1 − p) · n (5) n 22 Speciellt för p = 1 fås A(n) = 8 n+1 2 = O(n) Rekursion Begreppet rekursion innebär kortfattat att man delar upp ett problem i mindre delproblem. En rekursiv funktion är typiskt en metod i koden som anropar sig själv. Många datastrukturer går att definiera rekursivt, t ex listor och träd. Figur 6: Kochs snöflinga konstrueras stegvis och som med många andra fraktaler, erhålls stegen från en rekursiv definition. Exempel 4 (Beräkna n! med hjälp av rekursion) 0! = 1, n! = n · (n − 1)! (6) En rekursiv implementering av problemet blir: public s t a t i c int f a c t o r i a l ( int n) { i f ( n == 0 ) { return 1; } else { r e t u r n n∗ f a c t o r i a l ( n −1); } } När programmet hoppar till en ny metod sparas den aktuella raden, lokala variabler och parametervärden. På så vis kan programmet fortsätta där det slutade när undermetoden är färdig. Dessa data sparas som aktiveringsposter och placeras i en stack. Stacken byggs på och plockas ner allteftersom programmet går längre in i - och kommer tillbaka ut från rekursionen, se figur 8. 8.1 Rekursiv metod En rekursiv metod måste ha parametrar som anger problemets storlek. Ett eller flera basfall som löses direkt. Ett eller flera rekursiva anrop, som slutligen leder till ett basfall. I exemplet factorial ovan är n en parameter, n == 0 är basfallet och return n ∗ f actorial(n − 1) är det rekursiva anropet. 23 factorial(0) n=0 return 1 1 factorial(1) n=1 return 1 * factorial(0) 1 factorial(2) n=2 return 2 * factorial(1) 2 factorial(3) n=3 return 3 * factorial(2) 6 Figur 7: Dataflöde då metoden factorial ovan utförs för n = 3. Figur 8: Stacken då metoden factorial ovan utförs för n = 3. - 24 Exempel 5 (Skriva ut tal baklänges) Problem: Givet ett heltal n ≥ 0. Skriv ut siffrorna i omvänd ordning. Basfall: Talet har bara en siffra. Rekursivt: Skriv först ut sista siffran, därefter övriga siffror i omvänd ordning. public s t a t i c void reverse ( int n) { i f (n < 0){ t h r o w new I l l e g a l A r g u m e n t E x c e p t i o n ( " Argument < 0 " ) ; } e l s e i f (n < 10) { System . o u t . p r i n t l n ( n ) ; } else { System . o u t . p r i n t l n ( n % 1 0 ) ; reverse (n / 10); } } Alternativt gör man två metoder, en för att hantera anropet från användaren och en privat metod som utför rekursionen: public s t a t i c void printReverse ( int n) { i f (n < 0){ t h r o w new I l l e g a l A r g u m e n t E x c e p t i o n ( " Argument < 0 " ) ; } else { reverse (n ) ; System . o u t . p r i n t l n ( ) ; } } private s t a t i c void reverse ( int n) { i f (n < 10) { System . o u t . p r i n t l n ( n ) ; } else { System . o u t . p r i n t l n ( n % 1 0 ) ; reverse (n / 10); } } 8.2 Rekursiv lista Hela list begreppet kan defineras som rekursivt. Basfall: Tom lista, n == 0. Rekursivt: Första element följt av lista med n − 1 element. 25 Exempel 6 (Omvänt ordnad utskrift av lista) Man kan skriva ut innehållet ur en lista i omvänd ordning med hjälp av rekursion: p u b l i c void p r i n t R e v e r s e ( ) { printReverse ( f i r s t ) ; } p r i v a t e void p r i n t R e v e r s e ( L i s t N o d e <E> node ) { i f ( node != n u l l ) { p r i n t R e v e r s e ( node . n e x t ) ; System . o u t . p r i n t l n ( node . e l e m e n t ) ; } } Listing 4: Den rekursiva funktionen printReverse anropar sig själv med nästa element när den kommit ända in i rekursionen skriver den ut alla element utåt. basfallet med den tomma listan syns inte. Tidskomplexiteten för metoden är O(n). För att kunna utrycka metoderna rekursivt i en klass behöver vi tillgång till datastrukturen, detta har vi inte om vi använder javas färdiga list-klass. Har vi dock en egen list-klass som vi definierar som rekursiv så kan många metoder i den göras rekursiva. Exempel 7 (Hanois torn) n-st skivor ska flyttas från en pinne till en annan, till sin hjälp har man ett mellansteg. Reglerna är: Bara en skiva i taget får flyttas. En skiva som tas från en pinne måste genast läggas på en av de andra pinnarna. En större skiva får aldrig hamna ovanför en mindre (på samma pinne). Basfall: bara en skiva att flytta (n==1). Rekursivt: flytta de n−1 första skivorna till mellansteget. Flytta därefter den största skivan till destinationspinnen. Flytta slutligen de resterande skivorna från mellansteget till destinationspinnen. Se figur 9. p u b l i c v o i d move ( i n t n , i n t s t a r t , i n t d e s t , i n t temp ) { i f ( n == 1 ) { System . o u t . p r i n t l n ( " Move from " + s t a r t + " t o " + d e s t ) ; } else { move ( n − 1 , s t a r t , temp , d e s t ) ; System . o u t . p r i n t l n ( " Move from " + s t a r t + " t o " + d e s t ) ; move ( n − 1 , temp , d e s t , s t a r t ) ; } } 26 Figur 9: Rekursiv lösning av Hanoi-towers problement. Exempel 8 (Räkna färgrutor i rutnät) Antag ett tvådimensionellt rutnät. Problemet består i att finna antalet rutor i samma färg som är anslutna till rutan med kordinaterna x, y. Det vill säga hur många rutor ingår i det färgade objektet som innesluter x, y. public int countCells ( int x , int y) { i f x , y utanför rutnätet return 0; e l s e i f r u t a n x , y är tom return 0; else töm r u t a n x , y r e t u r n e r a 1 + a n t a l r u t o r i de å t t a a n g r ä n s a n d e r u t o r n a s f l ä c k a r Exempel 9 (Binärsökning) För att söka ett värde i en sorterad vektor finns en effektiv rekursiv algoritm, binärsökning. Basfall 0 element, sökt element finns ej. Rekursivt: Jämför värdet med mittelementet i vektorn. Om värdet är mindre än mittelementet, fortsätt sökningen i den halvan av vektorn som har mindre värden än mittelementet. Om värdet är större, fortsätt sökningen i den större halvan. p u b l i c s t a t i c <E e x t e n d s Comparable<E>> i n t b i n a r y S e a r c h (E [ ] i t e m s , E t a r g e t ) { return binarySearch ( items , t a r g e t , 0 , items . l e n g t h − 1 ) ; } p r i v a t e s t a t i c <E e x t e n d s Comparable<E>> i n t b i n a r y S e a r c h (E [ ] i t e m s , E t a r g e t , i n t f i r s t , i n t l a s t ) { if ( first > last ) { r e t u r n −1; } else { i n t mid = ( f i r s t + l a s t ) / 2 ; i n t c o m p R e s u l t = t a r g e t . compareTo ( i t e m s [ mid ] ) ; i f ( c o m p R e s u l t == 0 ) { r e t u r n mid ; 27 } e l s e i f ( compResult <0){ r e t u r n b i n a r y S e a r c h ( i t e m s , t a r g e t , f i r s t , mid − 1 ) ; } else { r e t u r n b i n a r y S e a r c h ( i t e m s , t a r g e t , mid + 1 , l a s t ) ; } } } Linjärsökning är i värsta fall O(n). Binärsökningen är mycket effektivare: I värsta fall saknas det sökta värdet, annars börjar sökningen med en vektor av storlek n och går vidare: n/2,n/4,n/8,...1,0. Det krävs i värsta fall 2 log(n) halveringar, med konstant arbete. Algoritmen får därför tidskomplexitet: O(logn). 8.3 Samband mellan rekursion och induktion Matematisk induktion är en teknik för att finna matematiska mönster och ställa upp en bevisad formel för detta mönster. Bevisen görs i två steg: Visa att sambanden gäller för små värden på n. Visa att om man antar att sambandet håller för vilket heltal som helst n upp till ett visst värde k, så gäller det även för närmast större värde k + 1. Exempel 10 (En stege) Basfallet: Vi visar att vi når första trappsteget på stegen, n = 1. Induktionsantagande: Vi antar att vi kommit upp till steget n = a, där a är ett tal, vilket som helst. Induktionssteg:När vi står på steg a måste vi bevisa att vi kan nå nästa trappsteg, dvs steg n = a+1 Exempel 11 (aritmetisk talföljd) Summan av talen: 1, 2, 3, ..., n kan skrivas som n(n+1) 2 Basfallet: Bevisa sambandet för n = 1. V L : Sn = 1 + 2 + 3 + ... + n och HL : Sn = n(n+1) . 2 Insättning av n = 1: = 1 =⇒ V L = HL. Vänsterledet blir 1 och högerledet blir 1(1+1) 2 Induktionsantagande: Antag att satsen stämmer för alla positiva heltal, a: Sa = 1 + 2 + 3 + 4 + ... + a = a(a+1) 2 Induktionssteget: Visa att satsen gäller även för nästa tal, a+1: Sa+1 = 1 + 2 + 3 + 4 + ... + a + (a + 1) = (a+1)((a+1)+1) 2 Visa att sedan att Sa+1 är samma som Sa + (a + 1). Skriver vi ut detta med talen får vi: a(a+1) + (a + 1). 2 2(a+1) Skriv om (a + 1) som för att få utseendet att likna det som för Sa+1 . 2 Vi får så: a(a+1)+2(a+1) minsta gemensamma nämnare till täljaren: (a + 1) ger: 2 a(a+1)+2(a+1) (a+1)(a+2) (a+1)((a+1)+1) = = 2 2 2 28 8.4 Induktion för att verifiera rekursion public s t a t i c int f a c t o r i a l ( int n) { i f ( n == 0 ) { return 1; } else { r e t u r n n∗ f a c t o r i a l ( n −1); } } När n = 0 blir resultatet 1 vilket är korrekt med avseende på definitionen av n!. Antag att algoritmen ger korrekt resultat för 0 ≤ n ≤ k. Anrop av f actorial(k + 1) ger (k + 1) ∗ f actorial(k + 1 − 1) = (k + 1) ∗ f actorial(k). Enligt antagandet ovan ger f actorial(k) korrekt resultat för k!. Vi får därför resultatet (k + 1) ∗ k! = (k + 1)!, V. S. B. 8.5 Divide and conquer approach Avser här tekniken att dela upp problem i rekursiva algoritmer som gör två rekursiva anrop i varje iteration. Se exempel i figure 10. 2 87 34 12 56 10 2 34 87 10 12 58 2 10 12 34 56 87 Figur 10: Sortering av vektor med divide-conquer approach. Definition 7 Fibonacci-talen definieras som följden: Fn = Fn−1 + Fn+2 för n ≥ 2 F0 = 0, F1 = 1. Exempel 12 (Fibonacci - divide and conquer) Den enklaste implementationen av Fibonacci, se koden nedan, är ineffektiv då samma tal beräknas gång på gång (O(2n )). En divide and conquer approach med dynamiskprogrammering kan få ner problemet till O(n). 29 public s t a t i c long f i b ( int n) { i f ( n <= 1 ) { return n ; } else { r e t u r n f i b (n − 1) + f i b (n − 2 ) ; } } Koden ovan löser problemet enligt figur 11. Ett effektivare sätt att lösa problemet är om man Figur 11: Fibonacci generering, komplexitet O(2n ). sparar alla tidigare uträkningar i en tabell och återanvänder dem: public long f i b ( int n) { l o n g [ ] t a b l e = new l o n g [ n + 1 ] ; // s k a p a en t a b e l l f o r ( i n t i = 0 ; i <= n ; i ++) { t a b l e [ i ] = −1; // n e g a t i v t v ä r d e <=> e j b e r ä k n a t } return r e c f i b ( table , n ); } private long r e c f i b ( long [ ] table , int n) { if ( table [n] < 0 ) { i f ( n<=1){ t a b l e [ n]=n ; } else { t a b l e [ n ] = r e c f i b ( t a b l e , n−1) + r e c f i b ( t a b l e , n −2); } } return table [ n ] ; } Nu blir istället anropen färre, då vardera beräkning bara utförs en gång. Se figur 12. 30 Figur 12: Fibonacci generering, komplexitet O(n). Nedan presenteras en alternativ lösning med iterativ approach. Också den O(n). public long f i b ( int n) { i f ( n <= 1 ) { return n ; } else { i n t n br 1 = 0 ; i n t n br 2 = 1 ; int res = 0; f o r ( i n t i = 2 ; i <= n ; i ++) { r e s = nb r 1 + n br 2 ; n br1 = n b r2 ; n br2 = r e s ; } return res ; } } 8.6 Val av lösning När man väljer lösningsmetod för ett visst problem bör man tänka på följande: • Rekursion bör användas då det är svårt att uttrycka lösningen icke rekursivt. • Rekursion bör användas om den ger en effektivare lösning. • Rekursion är lika effektiv som en annan lösning men enklare att förstå och implementera. Fördelar med rekursion är: • Det är lätt att översätta den rekursiv algoritm till kod. • Det är enkelt att verifiera en rekursiv algoritm. 31 9 Grafiska användargränssnitt Grafiska användargränssnitt examineras inte i samband med tentamen utan enbart på laboration 3 och 6. De som väljer att lösa sudoku eller implementera en kalkylator som inlämningsuppgift får ytterligare träning på detta. 10 Träd Definition 8 Ett träd är en icke-linjär struktur som bygger på att en parent-nod har flera chlidnoder. Se strukturen nedan i figur 13. Figur 13: Trädstrukturen. En träd struktur kan definieras rekursivt: som ett tomt träd om noder saknas, som en rot med noll eller flera subträd. 10.1 Terminologi • En nod som är över en annan är dess ancestor, den underliggande noden är descendant. Är noderna direkt anslutna så är den övre parent och den undre child. • Den översta noden i trädet saknar parents och kallas rot. • Noder som saknar childs kallas leafs. • Noderna är anslutna till varandra med branches. • Trädets nivåer räknas från roten och uppåt. Roten är på nivå 1 och ett child är på nivå parentlevel + 1. • Ett tomt träd har höjd 0. Höjden är densamma som den maximala nivån i trädet: height = maxlevel . 10.2 Binära träd Definition 9 Ett träd är binärt om varje nod har högst två barn eller högst två subträd. Ett träd är strikt binärt om varje nod har noll eller två barn. Terminologin utvidgas här med höger-, vänster subträd och höger-, vänster barn Aritmetiska uttryck, utryck med +,-,* och / kan illustreras med strikt binära träd. Jämför med dataflow graphs i digital signal behandling. Då lagras operander i löv (lövens värden) och operatorer i de andra noderna. Ett träds värde är värdet av subträden applicerade på operatorn i roten. Nedan visas hur en implementering av ett binärt träd kan se ut: 32 p u b l i c c l a s s BinaryTree<E> { p r i v a t e Node<E> r o o t ; p u b l i c BinaryTree ( ) { r o o t=n u l l ; } . . . trädoperationer . . . private static private private private private c l a s s Node<E> { E data ; Node<E> l e f t ; Node<E> r i g h t ; Node (E data ) { t h i s . data=data ; l e f t =r i g h t=n u l l ; } ... } } Figur 14: Implementeringen av en nod visualiserad. 10.2.1 Traversering Traversering innebär evaluering av alla noder i trädet. Många olika operationer på trädet kan ses som traverseringar. Man kan traversera trädet på olika sätt, nedan följer tre rekursiva metoder för traversering: Preorder - först roten, sedan vänster subträd i preorder, därefter höger subträd i preorder. Inorder - först vänster subträd i inorder, sedan roten och därefter höger subträd i inorder. Postorder - först vänster subträd i postorder, sedan höger subträd i postorder och därefter roten. Enligt resonemangen ovan bestäms alltså ordern av när man evaluerar roten. Sub träden behandlas hela tiden rekursivt, hela vägen ner till roten. 33 11 Binära sökträd I ett binärt sökträd lagras element i växande ordning, från vänster- till höger subträd. Det vill säga elementen i vänster subträd är hela tiden mindre än roten, som i sin tur är mindre än höger subträd. Strukturen är skapad för att man lätt ska kunna hitta, sätta in och ta bort element. (Dubbletter är inte tillåtna). Ordningen som bygger upp ett binärt sökträd medför att en inorder-traversering returnerar elementen i växande ordning. 11.1 Sökning i binära sökträd För att effektiv söka ett element i ett binärt sökträd kan man utnyttja strukturen enligt följande algoritm: • Är elementet i roten, om ja så är sökningen klar. • Om elementet vi söker är mindre än roten gå till vänster child och jämför. Annars gå till höger child. • Upprepa tills sökt element är funnet eller nästa nod att jämföra med är null (misslyckad sökning). Algoritmen innebär i praktiken att man i en sökning följer en gren tills man hittar elementet man söker eller grenen tar slut. 11.2 Implementering av binärt sökträd En implementering av ett generiskt binärt sökträd kan se ut: p u b l i c c l a s s BinarySearchTree <E e x t e n d s Comparable <? s u p e r E>> { p r i v a t e Node<E> r o o t ; p u b l i c BinarySearchTree ( ) { r o o t=n u l l ; } p u b l i c b o o l e a n add (E item ) { . . . } p u b l i c E f i n d (E t a r g e t ) { . . . } p u b l i c b o o l e a n c o n t a i n s ( Object t a r g e t ) { . . . } p u b l i c b o o l e a n remove (E t a r g e t ) { . . . } ... . . . n ä s t l a d k l a s s Node<E> som r e p r e s e n t e r a r nod . . . } Find funktionen kan skrivas: p u b l i c E f i n d (E t a r g e t ) { return f i n d ( root , t a r g e t ) ; } p r i v a t e E f i n d ( Node<E> n , E t a r g e t ) { i f ( n == n u l l ) { 34 return } i n t compResult i f ( compResult return } 11.2.1 null ; = t a r g e t . compareTo ( n . data ) ; == 0 ) { n . data ; Insättning: Vid insättning av nya element ska ordningen i trädet bevaras och dubletter får inte förekomma. Insättning kan utföras med hjälp av misslyckad sökning. Om sökningen misslyckas finns inte elementet i trädet och ska sättas in på det sist evaluerade stället i trädet, där misslyckandet konstaterats, se figur 18. Figur 15: Insättning i binärt sökträd. p u b l i c b o o l e a n add (E item ) { i f ( r o o t == n u l l ) { r o o t= new Node<E>( item ) ; return true ; } else { r e t u r n add ( r o o t , item ) ; } } p r i v a t e b o o l e a n add ( Node<E> n , E item ) { i n t compResult = item . compareTo ( n . data ) ; i f ( compResult == 0 ) { return f a l s e ; } 35 i f ( compResult <0){ i f ( n . l e f t==n u l l ) { n . l e f t = new Node<E>( item ) ; return true ; } else { r e t u r n add ( n . l e f t , item ) ; } } else { i f ( n . r i g h t == n u l l ) { n . r i g h t = new Node <E>( item ) ; return true ; } else { r e t u r n add ( n . r i g h t , item ) ; } } } 11.2.2 Borttagning: För att kunna ta bort ett element ur trädet måste man först söka upp det. Därefter kopplar man föräldern till något av barnen. Sammankopplingen sker på olika sätt beroende på hur många barn noden som ska tas bort har. • Enklaste fallen är när noden har noll eller ett barn. • Fallet med två barn är svårare. Figur 16: Borttagning av nod med noll barn. 11.3 Tidskomplexitet hos binära sökträd, en funktion av höjden De vanliga operationerna på binära sökträd innebär praktiskt att man söker utmed en gren i trädet. För att kunna analysera tidskomplexiteten för ett specifikt träd måste man då ha ett samband 36 Figur 17: Borttagning av nod med ett barn. Figur 18: Borttagning av nod med två barn. mellan trädets höjd och antal noder. Höjd begränsningen uppåt är lätt att göra, då höjden omöjligt kan överstiga antal noder. h≤n (7) Neråt begränsas höjden enligt: nivå 1 har högst en nod, nivå 2 högst 2, nivå 3 högst 4. Det vill säga sambandet på varje nivå är 2i−1 , där i är nivån. Den högsta nivån i ett träd av höjd h är per definition h. Alltså n ≤ 1 + 2 + 4 + 8 + ... + 2i−1 + ... + 2h−1 = 2h − 1. (En analogi kan här göras med de binära talsystemet: ett binärt ord med 8-bitar med fyllt med 1:or har ju värdet 28 − 1 = 255). För att få höjden som funktion av antalet noder löser vi ut h och får så: h ≥2 log(n + 1) 37 (8) 11.4 Garanterat minimal höjd Två olika strukturer av binära träd garanterar minimal höjd. Perfekt binärt träd kallas ett binärt träd som är fullt på alla nivåer och vars alla löv befinner sig på samma nivå. Definitionen innebär att höjden alltid är h =2 log(n + 1). Figur 19: Ett perfekt binärt träd. Komplett binärt träd kallas ett binärt träd som har alla nivåer fulla utom den högsta, vars noder är samlade i på vänster sida. Höjden fås här från intervallet (2h−1 − 1) < n ≤ (2h − 1) Löser vi ut h: 2h−1 < n + 1 ≤ 2h =⇒2 log(n + 1) ≤ h < 1 +2 log(n + 1). Vilket ger h ≈2 log(n + 1). Figur 20: Ett komplett binärt träd. Ett träd med minimal höjd har alltså i värsta fall tidskomplexitetenO(2 log(n)). 11.5 Problematik: insättningsordning Insättnings ordningen påverkar trädet höjd. Om man sätter in 1,2,3...,n får man en rak pinne som inte alls är ideal. Sätter man in 4,2,1,6,5,3,7 får man en finare struktur. Det ideala trädet ha noderna så nära roten som möjligt. Detta innebär att höjden blir O(2 log(n)). Definition 10 (Idealisk form) Ett binärt sökträd har idealisk form om: Alla nivåer utom den högsta har har maximalt antal noder. Det finns ingen bra algoritm som garanterar denna idealiska form. Däremot räcker det att trädets höjd är proportionerlig mot 2 log(n). Detta finns det effektiva insättning och borttagnings algoritmer för. Definition 11 (Balanserat binärt sökträd, AVL-träd) Ett binärt träd är balanserat om det för varje nod i trädet gäller att höjdskillnaden mellan dess båda subträd är högst ett. Adelson-Velsky och Landis (AVL) som står bakom definitionen av det balanserade binära sökträdet bevisade också att detta ha en höjd begränsad av: h ≤ 1.44 ·2 log(n). 11.6 Algoritmer för att hålla binära sökträd balanserade (AVL-träd) Algoritmerna som arbetar för att balansera upp binära sökträd gör detta med hjälp av rotation, se figur 21. 38 Figur 21: Rotationer på AVL-träd. 11.7 Detektera obalans För att hålla reda på balansen i trädet kan man införa ett balance-attribut i nodklassen. I detta attribut bokför man sedan höjdskillnaden mellan höger och vänster subträd. När höjden av ett subträd ändras, tex vid insättning eller borttagning, uppdaterar man attributen. Om höjdskillnaden är > 1 eller < −1 så måste detta åtgärdas med rotationer. Efter rotationerna blir absolutbeloppet av höjden ≤ 1 och trädet är balanserat. 11.8 Kostnad för att balansera ett träd Kostnaden för att balansera ett tidigare balanserat träd efter en insättning är maximalt en enkeleller dubbelrotation. Om obalans uppstår efter en borttagning ur ett tidigare balanserat träd så kan det behövas en enkel- eller dubbelrotation i varje nod på vägen från den nod där obalansen uppstod till roten för att återställa balansen. 39 Höjden i det balanserade trädet förblir O(2 log(n)) om man efter varje förändring balanserar det igen. Varje rotation i trädet kostar enbart O(1), så detta är vettigt att göra. Däremot kräver varje balansering att alla balance-attribut från den insatta noden upp till roten uppdateras. Detta ger en värsta-falls-kostnad på O(2 log(n)). 12 Set Definition 12 (Set) Ett set är en mängd element utan dubbletter. Interfacen Set och Collection har samma operationer. Men i Set tillåts inte dubbletter. 12.1 Sorted Set SortedSet förutsätter att elementen i setet är jämförbara, det vill säga implementerar någon form av comparator-objekt. SortedSets iterator går igenom mängden i växande ordning. Dessutom innehåller SortedSet några metoder som är specifika för sorterade mängder: returnera minsta element, returnera största element... 12.2 Set klasser Två klasser som implementerar Set är: TreeSet som implementerar SortedSet i form av Red-Black-tree som också den har en garanterad höjd på O(2 log(n)). HashSet som använder en hashtabell. 12.2.1 TreeSet TreeSet har klassrubriken och flera konstruktorer: p u b l i c c l a s s TreeSet<E> e x t e n d s A b s t r a c t S e t <E> implements S o r t e d S e t <E>{ p u b l i c TreeSet ( ) { . . . } p u b l i c T r e e S e t ( Comparator <? s u p e r E> c ) { . . . } ... } E förutsätts alltså inte implementera Comparable istället ges den som användaren möjlighet att välja om man vill lagra en klass som implementerar Comparable (den första konstruktorn ovan) eller en klass med egen Comparator som parameter (den andra konstruktorn ovan). 12.2.2 Interfacet Comparator Själva vitsen med interfacet Comparator är att ge användaren möjlighet att jämföra objekt på olika sätt. Tillexempel för att sortera personer både beroende på personnummer och beroende på namn. För att sortera ett treeSet beroende på namn skickas en Comparator med till sorterings metoden, se exempel 13 nedan. (Implementeringar av interfacet Comparable implementerar en metod compareTo). 40 p u b l i c i n t e r f a c e Comparator<T> { /∗∗∗ Compares i t s two arguments f o r o r d e r . ∗ Returns a n e g a t i v e i n t e g e r , z e r o , o r a p o s i t i v e ∗ i n t e g e r a s t h e f i r s t argument i s l e s s than , ∗ e q u a l to , o r g r e a t e r than t h e s e c o n d . ∗/ i n t compare (T e1 , T e2 ) ; } Exempel 13 (Interfacet Comparator) Nedan visas ett exempel med Person klassen som tack vare två Comparator-klasser kan sorteras på olika sätt. p u b l i c c l a s s Person { p r i v a t e S t r i n g name ; p r i v a t e S t r i n g pNbr ; ... } p u b l i c c l a s s NameComparator i m p l e m e n t s Comparator<Person> { p u b l i c i n t compare ( Person p1 , Person p2 ) { r e t u r n p1 . getName ( ) . compareTo ( p2 . getName ( ) ) ; } } p u b l i c c l a s s NbrComparator i m p l e m e n t s Comparator<Person> { p u b l i c i n t compare ( Person p1 , Person p2 ) { r e t u r n p1 . g e t N b r ( ) . compareTo ( p2 . g e t N b r ( ) ) ; } } ... /∗ D e t t a t r ä d kommer a t t o r d n a s e f t e r namn ∗/ TreeSet <Person> nameTree = new TreeSet <Person >(new NameComparator ( ) ) ; /∗ D e t t a t r ä d kommer a t t o r d n a s e f t e r personnummer ∗/ TreeSet <Person> nbrTree = new TreeSet <Person >(new NbrComparator ( ) ) ; Person p1 = new Person ( . . . ) ; Person p2 = new Person ( . . . ) ; nameTree . add ( p1 ) ; nameTree . add ( p2 ) ; nbrTree . add ( p1 ) ; nbrTree . add ( p2 ) ; 41 13 Map I interfacet map betraktas alla element som två delade, bestående av en unik nyckel och ett värde. Den vanliga användingen av en map är att nyckeln används för att söka upp värdet, jämför med en telefonkatalog eller databas. 13.1 Map klasser Två klasser som implementerar Map är: TreeMap som använder sökträd och innehåller operationerna: keySet() som garanterar att den returnerade mängden är ordnad. keyset().iterator() returnerar en iterator som går igenom elementen i växande ordning. HashMap som använder en hashtabell. 13.2 Några metoder i Map p u b l i c i n t e r f a c e Map<K, V> { V g e t ( Object key ) ; b o o l e a n isEmpty ( ) ; V put (K key , V v a l u e ) ; V remove ( Object key ) ; int size (); Set<K> k e y S e t ( ) ; C o l l e c t i o n <V> v a l u e s ( ) ; Set<Map . Entry<K, V>> e n t r y S e t ( ) ; // R e t u r n e r a r en mängd Entry−o b j e k t . /∗ R e p r e s e n t e r a r e t t n y c k e l −v ä r d e p a r ∗/ p u b l i c i n t e r f a c e Entry<K, V> { K getKey ( ) ; V getValue ( ) ; V s e t V a l u e (V ) ; // ä n d r a r v ä r d e t t i l l V och // r e t u r n e r a r d e t gamla v ä r d e t } } 13.3 Implementera Map med sökträd För att kunna lagra nyckel-värdeparen i ett träd görs en inre klass som representerar ett par. Denna typ används sedan för att lagra värdena i ett binärt sökträd. p r i v a t e s t a t i c c l a s s Entry<K e x t e n d s Comparable<K>, V> implements Comparable<Entry<K, V>>, Map . Entry<K, V> { p r i v a t e K key ; private V value ; 42 p r i v a t e Entry (K key , V v a l u e ) { t h i s . key=key ; t h i s . v a l u e=v a l u e ; } p u b l i c K getKey ( ) { r e t u r n key ; } public V getValue (){ return value ; } p u b l i c V s e t V a l u e (V v a l ) { V temp=v a l u e ; v a l u e=v a l ; r e t u r n temp ; } p u b l i c i n t compareTo ( Entry<K, V> r h s ) { r e t u r n key . compareTo ( r h s ) ; } Klassen myTreeMap nedan används för att implementera trädet: p u b l i c c l a s s MyTreeMap<K e x t e n d s Comparable<K>,V> implements Map<K, V> { p r i v a t e BinarySearchTree <Entry<K, V>> t h e T r e e = new BinarySearchTree <Entry<K, V> >(); p u b l i c V g e t ( Object key ) { Entry<K, V> found= t h e T r e e . f i n d ( new Entry<K, V> ( (K) key , n u l l ) ) ; i f ( found != n u l l ) { r e t u r n found . g e t V a l u e ( ) ; } else { return null ; } } p u b l i c V put (K key , V v a l u e ) { Entry<K, V> found= t h e T r e e . f i n d ( new Entry<K, V>(key , n u l l ) ) ; i f ( found != n u l l ) { r e t u r n found . s e t V a l u e ( v a l u e ) ; } else { t h e T r e e . add ( new Entry<K, V> ( key , v a l u e ) ) ; return null ; 43 } } 14 Hashtabeller I ett specialfall med Map och Set, där nycklarna är tal på ett intervall 1..n, kan en vektor användas: Elementet med nyckel k placeras på plats k i vektorn. Sökning, borttagning och insättning blir då detsamma som direkt access till plats k, operationerna får då tidskomplexitet O(1). En generalisering kan göras om man översätter alla nycklar till heltal i ett visst intervall kan man alltid använda en vektor, så länge talen inte kommer att överlappa varandra. Definition 13 (Hashfunktion) En funktion som översätter en nyckel till ett heltal. Per definition översätter hashfunktionen en stor mängd nycklar till en liten mängd heltal, detta medför att kollisioner kommer uppstå. Hashfunktionens kvalitet mäts genom antalet kollisioner den ger upphov till. Den översatta nyckeln kallas hashkod och kan användas som index på en vektor. 14.1 Sluten hashtabell I en sluten hashtabell används helt enkelt en vektor för lagring av elementen. Kollsioner hanteras med hjälp av: Linjär kollisionshanteringsteknik innebär att man sätter in det kolliderande elementet på första lediga plats efter den där det skulle hamnat om ingen kollision inträffat. Tabellen betraktas som cirkulär, så 0 kommer efter tableSize − 1. Då det ofta förekommer att samma hashkod upprepas kommer kluster bildas i tabellen. Detta är ett problem då stora kluster gör sökning i tabellen långsam. Borttagning ur tabellen leder också det till problematik då en tom plats indikerar att inget värde med den nyckeln sats in. För att kompensera vid borttagning sätts en markör, som indikerar att platsen är inaktiv, på den tömda platsen. Värsta fallet för sökning,insättning och borttagning är om alla element hamnat i en följd i tabellen, detta är i och för sig osannolikt, men om det händer måste man evaluera alla platserna i en följd. Detta ger operationerna en värsta tidskomplexitet på O(n). Vid halvfull tabell får man i medeltal komplexitet O(1). Kvadratisk kollisionshanteringsteknik är en bättre teknik att hantera kollisioner på då den är konstruerad för att motverka klustring. Detta görs genom att algoritmen vid en kollision först prövar nästa plats, därefter platsen 4steg framåt, därefter 9 steg framåt enligt: hV al, hV al + 1, hV al + 22 , hV al + 32 , ..., hV al + i2, ... (hV al = hashV alue) (9) Den kvadratiska kollisionshateringen introducerar dock ny problematik. Till exempel är det inte säkert att man alls hittar en ledig plats, trotts att det finns, se exempel 14. Värsta tidskomplexiteten är precis som vid linjär kollisionshantering: O(n), fast i praktiken får man mindre klustring. Exempel 14 Antag att hash-tabellens storlek är 16 och man använder hashfunktionen x%16: Insättning av element med nycklarna 0, 16, 32, 64 =⇒ samtliga hashas till plats 0. 44 14.2 Öppen hashtabell (separate chaining) En öppen hashtabell använder sig av listor för att hantera kollisioner. Varje vektor element innehåller en lista, som i sin tur innehåller alla element som hashats till den listans vektor position. Det vill säga lista nummer k innehåller alla element med hashkod k. Fördelen med en öppen hashtabell är att problematiken som uppstod i den slutna tabellen vid borttagning, det vill säga tomma luckor mitt i tabellen, inte längre är ett problem. I java implementeras öppna hashtabeller i HashSet och HashMap. Värsta fallet vid sökning uppstår då alla element hamnat i samma lista, -något som i och för sig är osannolikt om man har en bra hashfunktion, innebär en värsta tidskomplexitet på O(n). En bra hashfunktion ger alla positioner i tabellen lika stor sannolikhet. Detta i samband med en hashtabell som inte är fylld till mer än 2 · tabelSize ger en medelkomplexitet på O(1). 14.3 Metoden hashCode() I Object-klassen finns en metod hashCode() som översätter ett objekt till ett heltal. Implementeringen är gjord så att olika objekt om möjligt avbildas på olika heltal. Ofta måste man skugga denna metod (och equals()) i den klass vars objekt ska fungera som nyckel i hashtabellen (detta är t ex gjort i String, Integer, mfl.). Objekt för vilka equals ger true bör också få samma hashkod. Exempel 15 (hashCode() för sträng) Skuggningen av hashCode() som görs i String returnerar för en sträng s0 , s1 , s2 , ..., sn−1 : ˙ n−3 + ... + ascii(sn−1 ) ascii(s0 ) · 31n−1 + ascii(s1 ) · 31n−2 + ascii(s2 )31 (10) Eftersom 31 är ett primtal får man relativt få kollisioner. 14.3.1 Sökning i öppen hashTabell Internt i hashSet och hashMap används hashCode() och equals(object) för att finna element: • Med x.hashCode()%tableSize beräknas först platsen för elementet med nyckel x. • Därefter genomsöks listan på platsen efter x med hjälp av equals metoden. 14.4 Använda HashMap Här, nedan, är nycklarna av typen String och därför är hashCode() redan omdefinierad. HashMap<S t r i n g , Person> map = new HashMap<S t r i n g , Person > ( ) ; Person p = new Person ( " Kaj ... Person q = map . g e t ( " Kajsa " ) ; i f ( q != n u l l ) { ... } 45 14.5 Använda HashSet I exempelkoden nedanför används inte nycklar av typen String. HashSet översätter istället hela objektet till med hashCode. Därför krävs en skuggning av equals() och hashCode() i Person. HashSet<Person> s e t = new HashSet<Person > ( ) ; Person p = new Person ( " Kajsa " ) ; s e t . add ( p ) ; ... i f ( s e t . c o n t a i n s ( new Person ( " Kajsa " ) ) ) { System . out . p r i n t l n ( " Kajsa found " ) ; } else { System . out . p r i n t l n t ( " Kajsa not found " ) ; } Skuggas inte hashCode i Person hittas troligen inte objektet. När Kajsa sätts in beräknas hashkoden för objektet som p refererar till. När vi söker efter Kajsa baseras sökningen på hashkoden av det objekt som är parameter till contains-metoden. Detta är ett annat objekt, om än med samma namn. Sökningen utgår från den senares hashkod och dennas respektive plats i vektorn, eftersom hashkoden konstruerats för ett helt annat objekt är det högst osannolikt att dennes plats sammanfaller med objektet som lagts in tidigare. I koden nedan visas en Person-klass med equals och hashCode överskuggade på så vis att problematiken ovan undviks: c l a s s Person { p r i v a t e S t r i n g name ; // namn ... // ö v r i g a a t t r i b u t // k o n s t r u k t o r och ö v r i g a metoder p u b l i c b o o l e a n e q u a l s ( Object r h s ) { i f ( r h s i n s t a n c e o f Person ) { r e t u r n name . e q u a l s ( ( ( Person ) r h s ) . name ) ; } else { return f a l s e ; } } p u b l i c i n t hashCode ( ) { r e t u r n name . hashCode ( ) ; } } 15 Prioritetsköer En prioritetskö skiljer sig från en vanlig kö genom att prioritera det viktigaste först istället för att prioritera det äldsta först. 46 Exempel 16 Patienter som väntar i en akutmottagning kan ses som en form av prioritetskö. Allvarligast skadad/sjuk går först. Elementen i prioritetskön innehåller ett eller flera attribut som bestämmer dess, elementens, prioritet. Flera element kan ha samma prioritet. En prioritetskö ska ha metoder för: insättning samt hämtning/borttagning av högst prioriterade elementet. 15.1 PriorityQueue I java finns det inget speciellt interface för prioritetsköer, man använder istället interfacet för Queue. Klassen priorityQueue implementerar Queue. p u b l i c c l a s s P r i o r i t y Q u e u e <E> implements Queue<E> { // K o n s t r u k t o r f ö r o b j e k t som i m p l e m e n t e r a r Comparable . PriorityQueue ( ) { . . } // Jämför e l e m e n t e n med h j ä l p av komparatorn c . P r i o r i t y Q u e u e ( i n t i n i t i a l C a p a c i t y , Comparator <? s u p e r E> c ) { . . . } // Lägg t i l l e t t e l e m e n t i kön . b o o l e a n o f f e r (E x ) { . . . } // R e t u r n e r a r v ä r d e t med minst p r i o r i t e t s a t t r i b u t . E peek ( ) { . . . } // Tar b o r t och r e t u r n e r a r v ä r d e t med minst p r i o r i t e t s a t t r i b u t . E poll () { . . . } ... } 15.2 Implementering av prioritetskö - tidskomplexiteter Om prioritets kön implementeras som en lista eller en heap fås lite olika tidskomplexiteter. Sorterad lista peek, poll blir O(1). Offer blir O(n) eftersom elementets plats måste sökas. Osorterad lista peek, poll blir O(n) medan offer blir O(1) eftersom elementet kan sättas in först. Heap Ger effektivare operationer, se definition 14. 16 Heapar Definition 14 En heap är ett komplett binärt träd (obs: inte ett sökträd) där varje nod innehåller ett element som är ≤ barnens element. Detta innebär att trädets rot alltid innehåller minsta elementet. 47 16.1 Heap i vektor En heap kan lagras i en vektor, med roten på plats 0. Barnen till nod i placeras på platsen 2i+1 och 2i+2. Omvänt finns föräldern till nod j på platsen j−1 2 . En vektor implementering kan se ut som här nedanför: p u b l i c c l a s s P r i o r i t y Q u e u e <E> implements Queue<E> { p r i v a t e E [ ] queue ; private int size ; . . . konstruktorer . . . b o o l e a n o f f e r (E x ) { . . . } E peek ( ) { . . . } E poll () { . . . } ... } 16.1.1 Insättning Of f er implementeras så insättning sker på första lediga plats i vektorn. Därefter utförs byten uppåt i vektorn till rätt ordning uppåt (kallat percolate up eller add leaf). Exempel 17 (Insättning i heap-vektor) Insättning av ett element med nyckeln 1 i heapen i figur 22 nedan: Figur 22: Heapen före insättning. Bytena som sker vid insättning illustreras i figur 23: Figur 23: Insättnings metodik. Den insatta 1:an bubblar stegvis upp i trädet (percolate up). 48 16.1.2 Tidskomplexitet för insättning Eftersom Heapen är ett komplett binärt träd så är höjden begränsad till h ≈ log(n), för n-st noder. Vid insättning behöver den nya noden i värsta fall jämföras och bytas med alla element på väg upp till roten. Dessa är h-st så jämförelserna blir i värsta fall O(log(n)). I medelfall blir det dock bara O(1) byten. 16.1.3 Peek Minsta elementet finns alltid på platsen 0 i vektorn så peek är alltid O(1). 16.1.4 Poll P oll implementeras på linknande sätt som insättning, tag bort noden på plats 0. Den tomma positionen, 0, i vektorn fylls sedan med det sista elementet, därefter byts elementet i 0 positionen med det minsta av sina barn, om de är mindre. elementet bubblar ner i trädet tills det minsta elementet hamnat i roten. Exempel 18 (Poll i heap-vektor) Utför poll på vektorn i figur 24: Figur 24: Heapen före poll. Bytena som sker illustreras i figur 25 här nedanför. Figur 25: Det sista elementet läggs först och bubblar ner i trädet, omvänt mot vid insättning. 16.1.5 Tidskomplexitet för poll I värsta fall måste byten och jämförelse ske hela vägen från roten ut i ett av löven. Detta leder till en värsta tidskomplexitet på O(log(n)). Eftersom det är en nod långt nedifrån som sätts in i 0-positionen måste byten sannolikt ske hela vägen ner till ett löv, så medelfallet visar sig också vara O(log(n)). 49 16.2 Heap av osorterad samling I det fallet man vill bygga en heap av en osorterad samling, kan man sätta in alla element från den osorterade samlingen i en från början tom kö med hjälp av en for-each eller while-loop. Detta motsvarar O(n · log(n)), men kan effektiviseras om man har direkt tillgång till heapens vektor. För en prioritetskö som implementeras med en heap kan konstruktorn se ut: P r i o r i t y Q u e u e ( C o l l e c t i o n <? e x t e n d s E> c ) { q = . . . ; // skapa en v e k t o r , med t i l l r ä c k l i g s t o r l e k /∗ Lägg ö v e r a l l a e l e m e n t ur c i v e k t o r n q ∗/ I t e r a t o r <? e x t e n d s E> i t r = c . i t e r a t o r ( ) ; int i = 0; w h i l e ( i t r . hasNext ( ) ) { q [ i ] = i t r . next ( ) ; i ++; } s i z e =c . s i z e ( ) ; h e a p i f y ( ) ; / / H e a p i f y ä r en hjälpmetod } 16.2.1 Hjälpmetoden Heapify Heapify ska bygga själva heapen av vektorn, nerifrån och upp (percolate down) med början på noden som finns på plats n/2 − 1 därefter noden på plats n/2 − 2, ..., 0, se figurer 26 - 28 nedan. Figur 26: Börja med det inringade subträdet. 130 finns på plats n/2 − 1 i vektorn. Figur 27: Fortsätt med plats n/2 − 2 i vektorn. Där finns elementet 150. 50 Figur 28: Därefter behandlas roten, som i detta fall byts med vänster barn, därefter fortsätter percolate-down i subträdet (som i fallet med poll). 16.2.2 Heapifys tidskomplexitet Metoden heapify börjar på den näst nedersta nivån och utför här maximalt ett byte i varje underträd, då underträden har maxhöjd 1. I nästa steg är algoritmen på nivån ett steg högre upp, här är noderna visserligen färre men höjden på subträden är här max 2, därför blir det max 2 byten. Utfallet antal noder och höjd upprepas när man itererar uppåt i trädet. Man kan visa att heapify kostar O(n), där n är antalet element i vektorn. Detta kan jämföras med om man byggde trädet succesivt genom användning av metoden offer där de sista elementet hela tiden uppdateras och byts med noder uppåt i vektorn tills trädet är en heap. Detta är mer tidskrävande då. för visso tidiga insättningar kan lätt bytas upp till roten, men ju längre man kommer desto längre blir grenarna som fungerar som byteskedjor för noderna. 16.2.3 Heapify implementering Med en färdig metod för percolateDown blir implementeringen trivial: pri vate void heapify ( ) { f o r ( i n t i = ( s i z e − 2 ) / 2 ; i >= 0 ; i −−) { percolateDown ( i ) ; } Sista elementet finns på plats size − 1 i vektorn och dess förälder finns på plats (size − 2)/2. percolateDown fungerar som så att den börjar med noden på plats i och utför byten nedåt i heapen så länge ordningen är felaktig (metoden används även av operationen poll). 16.3 Sortering med prioritetskö Exempel 19 (Effektiv sortering med hjälp av prioritetskö) Antag att man effektivt vill sortera en vektor a. P r i o r i t y Q u e u e <E> myQ = new P r i o r i t y Q u e u e <E> ( ) ; f o r ( i n t i = 0 ; i < a . l e n g t h ; i ++) { myQ. o f f e r ( a [ i ] ) ; } f o r ( i n t i = 0 ; i < a . l e n g t h ; i ++){ a [ i ]=myQ. p o l l ( ) ; } 51 Koden ovan ger en tidskomplexitet beroende av n = a.length. Eftersom of f er utförs n-st gånger och poll också n-st gånger fås en komplexitet på O(n · log(n)). Används istället heapify fås en effektivare algoritm. Har man tillgång till vektorn kan man sortera direkt i den med hjälp av succesiva poll, se figur 29. Figur 29: Effektiv sortering direkt i heapens vektor. I texten ovan används hela tiden en heap med minsta elementet i roten, en min-heap. Definierar man istället roten till att innehålla det största elementet i alla subträd fås en max-heap. 52 16.4 Heapsort Först utförs heapify men med max-heap som definition: Figur 30: Heapify ger en vektor [150, 130, 90, 10, 80, 20]. Efter heapify: tag det första (det största) noden ur vektorn och byt plats på det och den sista noden (noden på position n − 1). De n − 1 första noderna i vektorn representerar nu trädet: Figur 31: Heapify och ett första-sista byte ger vektor/träd ovan. Därefter återställs ordningen med ännu ett anrop av percolateDown-funktionen: Sedan upprepas Figur 32: Heapify och ett första-sista byte och percolate down igen ger vektor/träd ovan. proceduren genom att man tar elementet på position n−2 och byter det med det största (det första) elementet i vektorn. Då har man en vektor [20, 80, 90, 10 | 130, 150], där de sista två elementen är sorterade. Därefter upprepas percolateDown som återställer heap ordningen och element n − 3 och det första byts. Algoritmen fortgår tills hela trädet förflyttas till sen sorterade högersidan. Heapsort 53 har i värsta fall tidskomplexiteten O(n · log(n)). Efter k-steg är de k största elementen sorterade, man kan då avbryta om man vill. 16.5 Alternativa prioritetsköer Ett alternativ till heapar för att implementera prioritetsköer är att använda binära sökträd, då krävs dock att prioriteterna är unika, alternativt får man göra en vektor med listor på varje position,j, där listorna innehåller alla element med prioritet j. Många implementationer av prioritetsköer har ytterligare metoder för att öka-/minska prioriteten på ett element. I så fall måste heapen, om en sådan används, byggas ut så elementen som sätts in innehåller information om vilken plats i vektorn de befinner sig på. 17 Sortering Sortering används huvudsakligen för att göra sökning snabbare och för att förenkla vissa algoritmer. I klassen java.util.Arrays finns metoder för sortering av vektorer bestående av element av typen int eller Object. I java collections finns metoder för sortering av listor. 17.1 Urvalssortering Vid urvalssortering söks det minsta elementet i vektorn fram, detta får sedan byta plats med det första elementet. Därefter upprepas sökningen i vektorns osorterade område (andra elementet och uppåt), varpå det minsta elementet får byta plats med vektorns andra element. Detta upprepas tills vektorn är helt sorterad, se figur 33. Urvalssorteringsalgoritmen har en tidskomplexitet på: n − 1 + n − 2 + ... + 1 = O(n2 ). Figur 33: Urvalssortering illustrerad. En effektivare variant av urvalssortering är den som utförs av heapsort eller med prioritetskö i det tidigare avsnittet. 17.2 Insättningssortering Elementet på plats k, för vardera k ∈ 1, 2...n, sätts in på rätt plats bland de redan sorterade elementen, på platserna 0...k − 1, se figur 34. Insättningssortering har också den tidskomplexitet O(n2 ), effektivare i det fall att datan redan är delvis sorterad. 54 Figur 34: Insättningssortering illustrerad. 17.2.1 Insättningssortering implementerad p u b l i c s t a t i c <T e x t e n d s Comparable <? s u p e r T>> v o i d s o r t (T [ ] a ) { f o r ( i n t i = 1 ; i < a . l e n g t h ; i ++) { T nextVal = a [ i ] ; i n t nextPos = i ; w h i l e ( nextPos > 0 && nextVal . compareTo ( a [ nextPos −1]) <0){ nextPos −−; } a [ nextPos ]= nextVal ; } } 17.3 Mergesort Mergesort är en divide-conquer algoritm för sortering. Först delas datan upp i två eller flera halvor vilka sorteras var för sig, slutligen slås de samman i en sista sortering. Algoritmen för samsorteringen visas här nedanför: i = j = k = 0 j ä m f ö r e l e m e n t e t i v1 [ i ] med e l e m e n t e t i v2 [ j ] om d e t minsta e l e m e n t e t ä r f r å n v1 r e s [ k ] = v1 [ i ] i=i +1 annars r e s [ k ] = v2 [ j ] j=j +1 k=k+1 upprepa t i l l s a l l a e l e m e n t i en av f ö l j d e r n a har b e h a n d l a t s f l y t t a a l l a e l e m e n t f r å n den andra f ö l j d e n t i l l r e s . Det går alltså inte att utföra sorteringen i den ursprungliga vektorn utan en hjälpvektor, som är stor nog, måste införas. De två minsta elementen i v1 och v2 jämförs och den minsta lagras i resvektorn. Därefter jämför vi nästa element i den vektorn som innehöll det minsta elementet med det minsta i den andra vektorn, det minsta av de två sätts in på nästa plats i res-vektorn. Detta upprepas tills den ena av v1,v2 är tom, då flyttas resten av elementen från den icke tomma av v1,v2 till res. 55 Antag att vi har en osorterad vektor. Vi delar upp vektorn på mitten gång på gång (rekursivt) tills delarnas längd är 1. Därefter använder vi samsorterings algoritmen ovan för att jämföra två element åt gången, vilket resulterar i nya sorterade vektorer med 2 element i varje. Därefter samsorterar vi dessa 2-elements vektorer och får sorterade 4-elements vektorer. Dessa samsorteras vidare till 8, och så vidare tills vi har en hel vektor igen, se figur 35 som illustrerar algoritmen steg för steg. 9 2 2 6 9 2 5 5 5 1 6 2 3 3 6 9 3 10 10 1 5 6 1 7 7 1 3 9 7 7 10 10 Figur 35: Mergesort på en vektor med 8 element, först uppdelad i rekursionen för att sedan åter falla samman. 17.3.1 Mergesort implementerad p r i v a t e s t a t i c <T e x t e n d s Comparable <? s u p e r T>> v o i d merge (T [ ] a , T [ ] tmpArray , i n t l e f t P o s , i n t r i g h t P o s , i n t r igh tEn d ) { int leftEnd = rightPos − 1; i n t tmpPos = l e f t P o s ; w h i l e ( l e f t P o s <= l e f t E n d && r i g h t P o s <= r igh tE nd ) { i f ( a [ l e f t P o s ] . compareTo ( a [ r i g h t P o s ] ) < 0 ) { tmpArray [ tmpPos ] = a [ l e f t P o s ] ; l e f t P o s ++; } else { tmpArray [ tmpPos ] = a [ r i g h t P o s ] ; r i g h t P o s ++; } tmpPos++; } /∗ Nu ä r en av d e l v e k t o r e r n a tom . Kopiera ö v e r r e s t e n av e l e m e n t e n i den i c k e tomma v e k t o r n t i l l tmpArray ∗/ /∗ F l y t t a t i l l s i s t t i l l b a k s e l e m e n t e n f r å n tmpArray t i l l motsvarande p l a t s e r i a ∗/ } 56 /∗∗ S o r t e r a r e l e m e n t e n i v e k t o r a a ∗/ p u b l i c s t a t i c <T e x t e n d s Comparable <? s u p e r T>> v o i d s o r t (T [ ] a ) { T [ ] tmpArray = (T [ ] ) new Comparable [ a . l e n g t h ] ; mergeSort ( a , tmpArray , 0 , a . l e n g t h − 1 ) ; } p r i v a t e s t a t i c <T e x t e n d s Comparable <? s u p e r T>> v o i d mergeSort (T [ ] a , T [ ] tmpArray , i n t f i r s t , i n t l a s t ) { if ( first < last ) { i n t mid = ( f i r s t + l a s t ) / 2 ; mergeSort ( a , tmpArray , f i r s t , mid ) ; mergeSort ( a , tmpArray , mid + 1 , l a s t ) ; merge ( a , tmpArray , f i r s t , mid + 1 , l a s t ) ; } } 17.3.2 Tidskomplexitet • Att samsortera två sorterade delvektorer av storlek n/2. kostar n. • Att mergea två delvektorer av storlek n/2, kostar n • Att mergea två delvektorer av storlek n/4 två gånger kostar 2 · n/2 = n • Att mergea två delvektorer av storlek n/8 fyra gånger kostar 4 · n/4 = n. Detta ger att för en vektor av storlek n, som då har log(n) nivåer, kostar O(n · log(n)) 17.4 Quicksort Algoritmen quickSort bygger också den på divide-conquer teknik. Den är i värsta fall sämre än mergeSort och heapSort, men bättre i medelfall och är därför intressant. 17.4.1 Algoritm för quicksort Quicksort algoritmen startar med att välja ut ett pivot-element, vilket den ser till att lägga på rätt plats i vektorn genom att flytta alla mindre element till vänster om pivot-elementet och alla större element till höger, detta kallas partitionering. Metodiken upprepas därefter rekursivt i delvektorerna till vänster och höger om pivot-elementet. 57 Exempel 20 (Quicksort algoritmens partitioneringssteg) Partitioneringen går till så här: • Sök från vänster och upp till pivot alla element som är > pivot. • Sök från höger om pivot och uppåt alla element som är ≤ pivot. • Byt plats på dessa element. • Fortsätt till hela vektorn behandlats. • Pivot elementet kan sedan sättas in mellan de två vektordelarna som uppstår. Se figur 36 här nedanför för illustration av partitioneringsprocessen. Figur 36: . Efter rekursiv partitionering sorteras delvektorerna a[low]. . . a[i-1] och a[i+1]. . . a[high] rekursivt. 17.4.2 Implementering p u b l i c s t a t i c <T e x t e n d s Comparable <? s u p e r T>> v o i d s o r t (T [ ] a ) { quickSort (a , 0 , a . length − 1); } // P r i v a t r e k u r s i v hjälpmetod som s o r t e r a r d e l v e k t o r n a [ f i r s t ] . . a [ l a s t ] : p r i v a t e s t a t i c <T e x t e n d s Comparable <? s u p e r T>> v o i d q u i c k S o r t (T [ ] a , i n t f i r s t , i n t l a s t ) { if ( first < last ) { i n t pivIndex = p a r t i t i o n (a , f i r s t , l a s t ) ; quickSort (a , f i r s t , pivIndex − 1 ) ; quickSort (a , pivIndex + 1 , l a s t ) ; } } 58 17.4.3 Tidskomplexitet Den effektivaste quicksort algoritmen fås när vektorn delas upp mitt itu i varje rekursivt steg. Då fås tidskomplexiteten O(n·log(n)). Sämsta fallet fås då den ena delvektorn blir tom i varje rekursivt steg, då är tidskomplexiteten O(n2 ). 17.4.4 Val av pivotelement För att undvika riskerna för sämsta tidskomplexitet väljs pivot elementet som medianen mellan första-, mittersta- och sista elementet, se figure 37 - 38 för tillvägagångssätt. Först sorteras de tre elementen efter storleks ordning, därefter placeras det mellersta av de tre på första platsen i vektorn så att partitionering kan utföras som i exemplen ovan. Figur 37: Medianen hamnar i mitten elementet då de tre markerade elementen jämförs. Figur 38: Därefter läggs medianen på första plats. 17.4.5 Förbättringar • Efter partitioneringen sorteras delvektorerna a[low]. . . a[i-1] och a[i+1]. . . a[high] rekursivt. • I praktiken låter man av effektivitetsskäl metoden avstanna när delvektorn i det rekursiva anropet är mindre än 10-20. • Den då nästan färdigsorterade vektorn kan sorteras av någon metod som är bra på nästan sorterad indata. T.ex. är insättningssortering lämplig. 18 Sorterings algoritmer i sammanfattning Urvalssortering O(n2 ): Långsam för stora n. Efter k pass är de k minsta sorterade. Insättningssortering O(n2 ): Bra för nästan sorterad indata (linjär då). Heapssort O(n·log(n)): Kan utformas så att inget extra minnesutrymme krävs. I praktiken långsammare än Quicksort. Efter k pass är de k största elementen sorterade. 59 Mergesort O(n · log(n)): Kräver extra minnesutrymme. I praktiken långsammare än Quicksort. Kan utformas iterativt och användas för att sortera element som finns på fil. Quicksort O(n · log(n)): Men O(n2) i värsta fall. Inget extra minnesutrymme för temporär vektor krävs. Bäst av de nämnda i praktiken om man väljer pivot och utför partitionering förnuftigt. 19 Bevis: Appendix 1 (Bevis) Bevisa att 0 + 1 + 2 + ... + (n − 1) = trianglar av punkter, se figur 39 n(n−1) . 2 Visualisera summan som Lägg i hop 2 likadana trianglar så en rektangel med dimensionerna n · (n − 1) bildas: Figur 39: Summa som trianglar. Eftersom vi la ihop två trianglar motsvarar rektangeln dubbla summan. Alltså är summan n(n−1) 2 Appendix 2 (Testa AVL strukturen online) http://www.qmatica.com/DataStructures/Trees/BST.html 60
© Copyright 2024