Föreläsning 12
Innehåll
Sortering
O(n2)-algoritmer:
urvalssortering
insättningssortering
O(n log n)-algoritmer:
Mergesort Quicksort
Sortering
Varför sortera?
För att göra sökning effektivare.
För att förenkla vissa algoritmer.
Varför olika sorteringsalgoritmer?
Olika sorteringsalgoritmer passar bra i olika sammanhang.
Ingen enskild algoritm är bäst i alla möjliga situtioner.
Sortering i Java
I klassen java.util.Arrays finns metoder för att sortera vektorer t ex:
public static void sort(int[] items)
public static void sort(Object[] items)
elementen jämförs med compareTo
public static <T> void sort(T[] items, Comparator<?
super T> comp)
elementen jämförs med comp.compare
Exempel:
int[] a = {1, 4, 1, 9, 5, 2, 6};
Arrays.sort(a);
I interfacet java.util.List finns en metod sort för att sortera listan (fungerar alltså för t.ex. ArrayList och LinkedList).
Sortering i Java
Exempel
En vektor med Book-objekt ska sorteras.
Klassen Book:
public class Book implements Comparable<Book> { private String isbn;
private String title;
private String author;
private int nbrPages;
// konstruktor och övriga metoder public int compareTo(Book o) {
return isbn.compareTo(o.isbn);
Sortering i Java
Comparable
Book[] a = new Book[4];
a[0] = new Book("isbn4", "titleB", "authorC", 125);
a[1] = new Book("isbn3", "titleA", "authorC", 523);
a[2] = new Book("isbn2", "titleD", "authorA", 199);
a[3] = new Book("isbn1", "titleC", "authorB", 278);
...
// sortera efter isbn-nummer - jämförs i compareTo Arrays.sort(a);
...
Klassen Book måste implementera interfacet Comparable.
Inuti metoden sort används compareTo för att jämföra elementen.
Sortering i Java
Comparator
Book[] a = new Book[4];
a[0] = new Book("isbn4", "titleB", "authorC", 125);
a[1] = new Book("isbn3", "titleA", "authorC", 523);
a[2] = new Book("isbn2", "titleD", "authorA", 199);
a[3] = new Book("isbn1", "titleC", "authorB", 278);
...
// sortera efter titlar
Arrays.sort(a, new TitleComparator());
...
Klass som implementerar interfacet Comparator:
public class TitleComparator implements Comparator<Book> { public int compare(Book b1, Book b2) {
return b1.getTitle().compareTo(b2.getTitle());
Sortering i Java
Lambdauttryck
Sortera efter titlar:
Arrays.sort(a, (b1, b2) ->
b1.getTitle().compareTo(b2.getTitle()) );
Sortera efter antal sidor:
Arrays.sort(a, (b1, b2) -> b1.nbrPages() - b2.nbrPages() );
Istället för att skriva en klass som implementerar interfacet Comparator kan vi använda ett lambdauttryck.
Inuti metoden sort används compare för att jämföra elementen.
Kommentar: om subtraktion i Comparator
I föregående bild används subtraktion för att få ett värde < 0, == 0, > 0:
b1.nbrPages() - b2.nbrPages()
Differensen får fel tecken om termerna är väldigt stora (i storleksordningen Integer.MAX_VALUE eller MIN_VALUE), och har olika tecken. Problemet kallas overflow, och har att göra med att datatypen int har ett begränsat maximalt antal siffror (32 bitar, binära siffror).
Overflow kan inte inträffa i vårt exempel med böcker och sidantal. (Varför?) Om man hanterar stora tal kan hjälpmetoden Integer.compare användas:
Urvalsortering i vektor
Urvalsortering (eng. selection sort)
Sök minsta elementet i den osorterade delen av vektorn och byt plats med första osorterade element (first = första elementet i den
osorterade delen):
3.5 6.2 2.8 5.0 1.1 4.5 first
min
1.1 6.2 2.8 5.0 3.5 4.5 first
min
1.1 2.8 6.2 5.0 3.5 4.5 first
min
1.1 2.8 3.5 5.0 6.2 4.5 first
min
1.1 2.8 3.5 4.5 6.2 5.0 first
min
1.1 2.8 3.5 4.5 5.0 6.2
Tidskomplexitet: n − 1 + n − 2 + ... + 1 = O(n2)
Urvalssortering
Tidskomplexitet är O(n2).
Efter k pass är de k minsta (eller största) elementen sorterade. Kan därför vara lämplig om man bara vill få fram de k minsta (eller
största) och k är litet.
Tidskomplexitet är då O(k ∗ n)
Insättningssortering i vektor
Insättningssortering (eng. insertion sort)
Element på plats k i vektorn sätts in på rätt plats bland de redan sorterade elementen på platserna 0..k − 1
Detta görs för k = 1, 2, . . . , n
3.5 6.2 2.8 5.0 1.1 4.5 sort
osort
3.5 6.2 2.8 5.0 1.1 4.5
2.8 3.5 6.2 5.0 1.1 4.5
2.8 3.5 5.0 6.2 1.1 4.5
1.1 2.8 3.5 5.0 6.2 4.5
1.1 2.8 3.5 4.5 5.0 6.2 sort
osort sort
osort sort
osort sort
osort sort
Tidskomplexitet (värstafall):
1 + 2 + 3 + . . . + n − 1 = n(n − 1)/2 = O(n2).
Även medelfallet kan visas vara O(n2).
Diskutera
Blir urvalssortering snabbare eller långsammare om vektorns element råkar vara sorterade i stigande ordning?
Blir insättningssortering snabbare eller långsammare om vektorns element råkar vara sorterade i stigande ordning?
Insättningssortering
Tidskomplexitet är O(n2) i värsta fall och i medelfall.
Dock bra metod om vektorn är ”nästan” sorterad från början:
Om vektorn är sorterad utförs bara en jämförelse per pass – tidskomplexiteten blir då O(n).
Om vektorn består av n sorterade element följda av k osorterade behövs endast k pass.
Man börjar med att sortera in det (n + 1):a sedan det (n + 2):a o s v. I varje pass görs i värsta fall O(n) jämförelser.
Totalt O(k ∗ n) d.v.s. O(n) om k är litet i förhållande till n.
Insättningssortering
public static <T extends Comparable<T>> void sort(T[] a) { for (int i = 1; i < a.length; i++) {
T nextVal = a[i];
int nextPos = i;
while (nextPos > 0 &&
nextVal.compareTo(a[nextPos - 1]) < 0) { a[nextPos] = a[nextPos - 1];
nextPos--;
}
a[nextPos] = nextVal;
} }
Kommentar: begränsad typparameter
Metoden sort skulle kunna skrivas så här:
public static void sort(Object[] a) {...}
För att anropet av compareTo inuti sort ska fungera måste nextVal typkonverteras till Comparable:
... ((Comparable) nextVal).compareTo(...)...
Klassen som beskriver elementen i vektorn måste implementera Comparable. Annars genereras ClassCastException vid
exekveringen.
Man kan istället låta klassen vara generisk och begränsa
typparametern T så att det redan vid kompileringen krävs att klassen som ersätter T implementerar Comparable<T>:
public static <T extends Comparable<T>> void sort(T[] a) {...}
Insättningssortering
Variant med komparator
public static <T> void sort(T[] a, Comparator<T> comp) { for (int i = 1; i < a.length; i++) {
T nextVal = a[i];
int nextPos = i;
while (nextPos > 0 &&
comp.compare(nextVal, a[nextPos - 1]) < 0) { a[nextPos] = a[nextPos - 1];
nextPos--;
}
a[nextPos] = nextVal;
}
? super T
Överkurs
”? super T” kan utläsas ”okänd superklass till T (inklusive T)”
Deklarationen av typparametern T bör egentligen se ut så här: <T extends Comparable<? super T> istället för bara <T extends Comparable<T>>.
Förklaring: Antag att vi har följande klasser:
class Person implements Comparable<Person> { ... } class Student extends Person { ... }
Om den andra, enklare varianten används kan vi inte kan vi inte skriva:
Student[] v = ...
sort(v);
Klassen Student implementerar inte Comparable<Student> utan istället genom arv Comparable<Person>.
Mergesort
Sortera med söndra- och härskateknik
Sortera vänstra halvan Sortera högra halvan
Samsortera de båda sorterade halvorna
7 2 5 9 3 8 10 2
2 5 7 9 2 3 8 10
Merge – samsortering av sorterade följder
Algoritm
Givet två följder v1 och v2 med element sorterade i växande ordning.
Samsortera till en följd res.
Algoritm:
i = j = k = 0
så länge det finns obehandlade element kvar i både v1 och v2 jämför elementet i v1[i] med elementet i v2[j]
om det minsta elementet är från v1 res[k] = v1[i]
i = i + 1 annars
res[k] = v2[j]
j = j + 1 k = k + 1
En av följderna v1 och v2 har obehandlade element kvar.
Flytta dessa element till res.
Samsortering av sorterade följder – exempel
1 4 6 6 2 4 7 res
v1 v2
1 4 6 6 2 4 7
res v1 v2
1
2 4 7 res
v1 v2
1 2
2 4 7 res
v1 v2
1 2 4 1 4 6 6 1 4 6 6
1 4 6 6 2 4 7
res v1 v2 2 4 7
res v1 v2
1 2 4 4 1 4 6 6
1 2 4 4 6
1 4 6 6 2 4 7
res v1 v2
1 2 4 4 6 6
Samsorteringen i Mergesort
I samsorteringssteget i Mergesort (merge) motsvaras de båda följderna v1 och v2 av de båda sorterade vektorhalvorna.
Det går inte att utföra samsorteringen i den ursprungliga vektorn. En hjälpvektor, lika stor som den som ska sorteras, behövs.
När man i merge-steget skall slå samman två delvektorer:
används motsvarande utrymme i hjälpvektorn (tmpArray):
Resultatet flyttas sedan tillbaka till ursprungsvektorn.
Samsorteringen i Mergesort
Exempel
Slå samman delvektorerna v1 och v2 i vektorn a (bestående av ett element vardera):
Resultatet flyttas sedan tillbaka till den ursprungliga vektorn.
7 2 5 9 3 8 10 2
2 7
tmpArray a
merge – implementeringsskiss
Slå samman de sorterade delvektorerna a[leftPos] .. a[rightPos - 1]
och a[rightPos] .. a[rightEnd]:
private static <T extends Comparable<T>> void merge(T[] a,
T[] tmpArray, int leftPos, int rightPos, int rightEnd) { int leftEnd = rightPos - 1;
int tmpPos = leftPos;
...
leftPos rightPos rightEnd
merge – implementeringsskiss
Forts
while (leftPos <= leftEnd && rightPos <= rightEnd) { if (a[leftPos].compareTo(a[rightPos]) <= 0) {
tmpArray[tmpPos] = a[leftPos];
leftPos++;
} else {
tmpArray[tmpPos] = a[rightPos];
rightPos++;
}
tmpPos++;
}
/* Nu är en av delvektorerna tom. Kopiera över resten av elementen i den icke tomma vektorn till tmpArray */
Mergesort – implementering
/** Sorterar elementen i vektora a */
public static <T extends Comparable<T>> void sort(T[] a) { T[] tmpArray = (T[]) new Comparable[a.length];
mergeSort(a, tmpArray, 0, a.length - 1);
}
private static <T extends Comparable<T>> void
mergeSort(T[] a, T[] tmpArray, int first, int last) { if (first < last) {
int mid = first + (last - first) / 2;
mergeSort(a, tmpArray, first, mid);
mergeSort(a, tmpArray, mid + 1, last);
merge(a, tmpArray, first, mid + 1, last);
} }
Stabila sorteringsalgoritmer
Stabila sorteringsalgoritmer
Bibehåller ordningen för element med lika nycklar efter sorteringen.
Exempel: Antag att vi har personer ordnade efter förnamn:
Ada Andersson, Bo Eriksson, Lars Andersson, Lena Andersson
Om vi vill sortera efter efternamn istället, men samtidigt bibehålla den tidigare ordningen mellan förnamnen så måste vi använda en stabil
sorteringsalgoritm.
Ada Andersson, Lars Andersson, Lena Andersson, Bo Eriksson
Mergesort – tidskomplexitet
Att samsortera två sorterade delvektorer av sammanlagd storlek n kostar O(n).
1 merge av två delvektorer av storlek n/2, kostnad n
2 merge av två delvektorer av storlek n/4, kostnad 2 ∗ n/2 = n 4 merge av två delvektorer av storlek n/8, kostnad 4 ∗ n/4 = n
Antal nivåer = log n =⇒ total kostnad O(n log n)
Quicksort
Söndra- och härskaalgoritm.
Oftast snabb
Sämre än Mergesort i värsta fall – O(n2).
Bra (snabb) i medelfall – O(n log n).
Värstafallet kan göras statistiskt osannolikt.
Inget extra minnesutrymme för temporär vektor krävs.
Quicksort – algoritm
Välj ut ett element (pivotelement).
Se till att det hamnar på rätt plats:
Flytta om elementen så att element ≤ pivot hamnar till vänster och element ≥ pivot hamnar till höger.
Kallas partitionering av vektorn.
x
≤ x ≥ x
Pivot-elementet, på rätt plats
Upprepa rekursivt på de båda delvektorerna till vänster respektive till höger om pivotelementet.
Quicksort – implementering
public static <T extends Comparable<T>> void sort(T[] a) { quickSort(a, 0, a.length - 1);
}
/* Privat hjälpmetod.
Sorterar delvektorn a[first]..a[last]
*/
private static <T extends Comparable<T>> void
quickSort(T[] a, int first, int last) { if (first < last) {
int pivIndex = partition(a, first, last);
quickSort(a, first, pivIndex - 1);
quickSort(a, pivIndex + 1, last);
Quicksort – val av pivot
I princip kan vilket element som helst väljas.
Vi börjar för enkelhets skull med att välja första elementet i vektorn.
Inte särskilt bra val. Vi återkommer senare med en diskussion om bättre val.
Quicksort – partitioneringssteget
Sök från vänster upp ett element som är ≥ pivot.
Sök från höger upp ett element som är ≤ pivot.
Byt plats på dessa.
Fortsätt tills hela vektorn genomletats.
Pivotelementet kan sättas in mellan de båda vektordelarna som uppstår.
Arbetet blir proportionellt mot vektorns längd.
Partitionering – exempel
6 1 8 9 4 3 5 2 0
pivot = 6 7
6 1 0 9 4 3 5 2 8 7
6 1 0 9 4 3 5 2 8 7
6 1 0 2 4 3 5 9 8 7
Efter byte:
Efter byte:
6 1 0 2 4 3 5 9 8 7
Byt plats på detta och pivot
5 1 0 2 4 3 6 9 8 7
pivot
Partitionering – sorterad vektor
Dåligt val av pivot
Om vektorn är sorterad och om pivot väljs som första elementet hamnar Quicksort i sitt värsta fall:
1 2 3 4 5 6 7 8
pivot = 1
Byt plats på detta och pivot
1 2 3 4 5 6 7 8
pivot
Quicksort – tidskomplexitet
Man kan visa att det bästa fallet för Quicksort är när vektorn delas mitt itu i varje rekursiv upplaga.
Då är tidskomplexiteten = O(n log n) x
≤ x pivot ≥ x
Sämsta fall är när den ena delvektorn blir tom i varje rekursiv upplaga.
Då är tidskomplexiteten = O(n2) x
≥ x pivot
Quicksort – bättre val av pivot
Välj median av första, mittersta och sista elementet.
Eliminerar riskerna i samband med sorterad eller nästan sorterad indata.
6 1 4 9 8 3 5 2 7 0
left mid right
Sortera de tre elementen i växande ordning:
0 1 4 9 6 3 5 2 7 8
left mid right
pivot
Quicksort – bättre val av pivot
Forts
Byt elementet på plats mid med elementet på plats left.
Då hamnar pivotelementet längs till vänster precis som förut.
6 1 4 9 0 3 5 2 7 8
pivot
Nu kan partitioneringssteget utföras som förut.
Varianter av partitioneringssteget
Stanna eller ej (och byta) vid likhet med pivot?
Om vi inte stannar och byter och alla nycklar är lika hamnar vi i sämsta fallet.
5 5 5 5 5 5 5 5 5 5
pivot
Om vi stannar och byter och alla nycklar är lika blir det bästa fallet.
5 5 5 5 5
5 5 5 5 5
Quicksort – efter partitioneringen
Efter partitioneringen sorteras delvektorerna
a[low]. . . a[pivIndex-1] och a[pivIndex+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.
Sortering
Exempel på vad du ska kunna
Redogöra för och jämföra olika sorteringsalgoritmer:
Insättningssortering i vektor Urvalssortering i vektor
Heapsort (behandlas i samband med prioritetsköer).
Mergesort Quicksort
Genomföra sortering på enkla exempel med ovan nämnda metoder Samsortera två sorterade följder
Förklara begreppen pivot-element och partitionering (Quicksort).
Använda idéerna från sorteringsalgoritmerna för att lösa andra