Programmeringsteknik

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