Kapitel 7: Analys av sorteringsalgoritmer Kapitel 7 i Weiss bok handlar om problemet med att sortera en räcka av element vi skall analysera körtiderna för några av sorteringsalgoritmerna vi bevisar också att det inte finns någon generell sorteringsalgoritm med en bättre körtid än O(N log N) Sorteringsalgoritmer har undersökts och utvecklats under en lång tid för de flesta sorteringsalgoritmerna kan man analysera körtiden: O(N 2 ) eller O(N log N) det finns enkla sorteringsmetoder vars exakta körtid man fortfarande inte lyckats analysera Man kan visa att det inte finns någon generell sorteringsalgoritm som är effektivare än O(N log N) 1 Insättningssortering Sätter in elementen ett efter det andra på sin rätta plats bland de redan sorterade elementen Vi gör N-1 genomgångar (eller pass) av räckan i pass p (p [1, N-1] ser vi till att elementen före p är i sorterad ordning 0 p N-1 sorterade inte ännu sorterade I pass p flyttas elementet i position p vänsterut tills vi hittar dess rätta plats bland de p+1 första elementen elementet i position p sparas undan i en temporär variabel alla element större än elementet i position p bland de p första elementen flyttas ett steg till höger elementet p sätts in på sin rätta plats 2 1
Algoritm för insättningssortering public static void insertionsort(comparable [] a) { int j; for(int p=1; p < a.length; p++) { Comparable tmp = a[p]; for(j=p; j>0 && tmp.compareto(a[j-1])<0; j--) a[j] = a[j-1]; a[j] = tmp; } } Elementet i position p sparas i varibeln tmp Går igenom elementen till vänster om p dessa element är redan i sorterad ordning de element som är större än tmp flyttas ett steg till höger skapar en lucka för elementet i tmp på rätt ställe bland de redan sorterade elementen 3 Analys av insättningssortering Algoritmen består av två nästade for-loopar båda looparna kan utföras N gånger i värsta fall körtid: O(N 2 ) det värsta fallet uppstår när input-räckan är sorterad i omvänd ordning Jämförelsen j>0 && tmp.compareto(a[j-1])<0 kan utföras högst p gånger för varje värde på p summering över alla p ger N 1 i =1+ 2 + 3+ + (N 1) = i=1 9 8 7 6 5 4 3 2 1 N(N 1) 1= O(N 2 ) 2 4 2
Analys av insättningssortering Om input-räckan är redan sorterad är körtiden O(N) eftersom jämförelsen j>0 && tmp.compareto(a[j-1])<0 alltid är falsk dvs. tmp a[j-1] så inga element behöver flyttas 1 2 3 4 5 6 7 8 9 Om input-räckan är nästan sorterad, dvs. bara några element är i fel ordning, är insättningssortering mycket effektiv insättningssortering är i praktiken den snabbaste metoden att sortera en liten mängd data (20-30 element) Genomsnittlig körtid är O(N 2 ) 5 Undre gräns för enkla sorteringsmetoder En inversion i en räcka A av tal är ett ordnat par (i, j) sådant att i < j men A[i] > A[j] dvs. elementen på position i och j är i fel ordning när vi skall sortera räckan i stigande ordning I exemplet med talräckan 34, 8, 64, 51, 32, 21 finns 9 inversioner: (34, 8), (34, 32), (34, 21), (64, 51), (64, 32), (64, 21), (51, 32), (51, 21), (32, 21) Det här är de platsbyten (swaps) som skall göras vid insättningssortering 6 3
Undre gräns för enkla sorteringsmetoder När man byter plats på två närliggande element som är i fel ordning avlägsnas en inversion en sorterad räcka har inga inversioner körtiden för sorteringen består därför av tiden för att avlägsna alla inversioner, plus eventuellt annat arbete som algoritmen gör Körtiden för insättningssortering kan skrivas som O(I+N) I är antalet inversioner O(N) annat arbete (dvs. att gå igenom räckan från början till slut i den yttre loopen) Om antalet inversioner i räckan är O(N) blir körtiden för insättningssortering O(N+N) = O(N), dvs. linjär 7 Genomsnittliga antalet inversioner Vi beräknar det genomsnittliga antalet inversioner i en permutation av en talräcka det här ger ett exakt uttryck för den genomsnittliga körtiden för insättningssortering, eftersom den är O(I+N) Vi antar för enkelhets skull att inga duplikat finns inputräckan är en permutation av de N första heltalen Teorem: det genomsnittliga antalet inversioner i en räcka av N olika heltal är N(N-1)/4 8 4
Bevis Låt L vara en godtycklig lista av heltal t.ex. L = 21, 32, 51, 64, 8, 34 L R betecknar listan i omvänd ordning L R = 34, 8, 64, 51, 32, 21 Tag ett par av element (x, y) ur listan i exakt en av listorna L och L R är (x, y) en inversion Det totala antalet par som kan bildas av listorna L och L R är N(N-1)/2 men nu har vi betraktat den dubbla mängden data, dvs. två listor som innehåller samma data men i omvänd ordning ett par (x,y) är en inversion bara i den ena av listorna L och L R I genomsnitt har en lista N(N-1)/4 inversioner 9 Undre gräns för enkla sorteringsalgoritmer Teorem: en algoritm som sorterar genom att byta om närliggande element kräver Ω(N 2 ) tid i genomsnitt Bevis: det genomsnittliga antalet inversioner i en godtycklig räcka är N(N-1)/4 = Ω(N 2 ) enligt ovan varje utbyte avlägsnar en inversion, så att Ω(N 2 ) utbyten behövs i genomsnitt Ger en undre gräns för alla enkla sorteringsalgoritmer t.ex. för selektionssortering och bubbelsortering som bara utför byten mellan närliggande element. för att en sorteringsalgoritm skall kunna ha en körtid bättre än kvadratisk måste den göra jämförelser och utbyten mellan element som ligger längre ifrån varandra 10 5
Undre gräns för körtiden Det här är ett exempel på en undre gräns för körtiden av en algoritm vanligen kan vi bevisa värstafalls-resultat, dvs. att körtiden för en algoritm växer långsammare än någon känd funktion, dvs. O(f(n)) genomsnittliga resultat skulle vara mera intressant, men det är ofta svårt att realistiskt avgöra vad som är ett genomsnittligt fall Uppskattningar av undre gränsen, dvs. att körtiden åtminstone växer lika snabbt som någon känd funktion, är vanligtvis svåra att göra 11 MergeSort Mergesort är en rekursiv algoritm som använder en divide-andconquer (dela-och-härska) metod problemet delas in i två mindre delproblem, vilka löses rekursivt resultaten av del-lösningarna kombineras sedan med mergeoperationen Den fundamentala operationen i Mergesort är att slå ihop två sorterade listor eftersom listorna är sorterade kan de sammanslås i en genomgång av inputen, om resultatet placeras i en tredje lista Algoritmen använder två inputräckor A och B, en output räcka C, och tre räknare Aptr, Bptr och Cptr pekarna initialiseras till början av sina respektive räckor 12 6
Merge-operationen Det mindre av elementen A[Aptr] och B[Bptr] kopieras till nästa element i C, och pekarna uppdateras när någondera inputlistan är uttömd kopieras resten av listan till C A: 1 13 24 26 B: C: 2 15 27 38 Aptr Bptr Cptr 13 Illustration av Merge Jämför a[leftpos] med a[rightpos] och kopiera det mindre av dessa till tmparray[tmppos] inkrementera pekarna sluta när någon av dessa har nått leftend eller rightend leftend rightend leftpos rightpos tmparray: tmppos 14 7
Implementation av MergeSort Rekursionen avslutas när left = right räckan a har då delats upp tills vi har fått delräckor av längd ett Sen börjar räckorna slås ihop med merge-rutinen private static void mergesort(comparable a[], Comparable[] tmparray, int left, int right) { if (left < right) { int center = (left + right)/2; mergesort(a, tmparray, left, center); mergesort(a, tmparray, center+1, right); merge(a, tmparray, left, center+1, right); } } public static void mergesort(comparable [] a) { Comparable [ ] tmparray = new Comparable[a.length]; mergesort(a, tmparray, 0, a.length - 1); } 15 Implementation av Merge private static void merge(comparable [] a, Comparable [] tmparray, int leftpos, int rightpos, int rightend) { int leftend = rightpos - 1; int tmppos = leftpos; int numelements = rightend - leftpos + 1; // Huvudloop while ( leftpos <= leftend && rightpos <= rightend ) if( a[leftpos].compareto(a[rightpos]) <= 0) tmparray[tmppos++] = a[leftpos++]; else tmparray[tmppos++] = a[rightpos++]; // Kopiera resten av första halvan while( leftpos <= leftend) tmparray[tmppos++] = a[leftpos++]; // Kopiera resten av högra halvan while( rightpos <= rightend) tmparray[tmppos++] = a[rightpos++]; // Kopiera tmparray tillbaka for( int i=0; i < numelements; i++, rightend--) a[rightend] = tmparray[rightend]; } 16 8
Analys av Mergesort Klassiskt exempel på hur man analyserar en rekursiv algoritm genom att lösa en rekurrensrelation Vi antar att N (antalet element att sortera) är en potens av 2 N = 2 k för något värde k det betyder att vi kan alltid kan dela räckan jämnt i två halvor Betecknar tiden att sortera N stycken tal med T(N) för N=1 är tiden konstant, dvs. T(1) = 1 För N>1 består tiden för Mergesort av tiden att rekursivt sortera två räckor av storlek N/2, plus tiden att kombinera resultatet, vilken är linjär med antalet element N Körtiden ges av rekurrensrelationen T(N) =2T(N/2) + N kan lösas med en standard teknik som kallas teleskopering 17 Teleskopering Vi skall lösa rekurrensrelationen T(N) =2T(N/2) + N Först divideras båda sidorna av ekvationen med N T(N) N = T(N /2) N /2 +1 Ekvationen gäller för alla N som är en potens av 2, så vi kan också skriva T( N / 2) T( N / 4) = N / 2 N / 4 +1 T( N / 4) N / 4 T( N / 8) = +1 o.s.v. till och med N / 8 T(2) 2 = T(1) 1 +1 18 9
Teleskopering (forts.) Vi adderar alla ekvationerna adderar ihop alla termer på båda sidorna av ekvationerna Samma termer förekommer i summan både på högra och vänstra sidan, dvs. de tar ut varandra T(N) T(N /2) = N N /2 +1 T( N / 2) T( N / 4) = N / 2 N / 4 +1 T( N / 4) T( N / 8) = N / 4 N / 8 +1... T(2) 2 = T(1) 1 +1 19 Teleskopering (forts.) Kvar efter teleskoperingen blir T( N ) N = T(1) 1 dvs. T(N) N = T(1) 1 + logn + 1+1+...+1 log N stycken vilket ger T(N) = O(N log N) eftersom T(1) = 1 Vi antog att N = 2 k, dvs. en jämn potens av 2 analysen kan relativt enkelt utvidgas till fallet att N inte är en jämn potens av 2, med liknande resultat 20 10
Quicksort Quicksort är den snabbaste allmänna sorteringsmetoden genomsnittlig körtid är O(N log N) värstafalls-körtid är O(N 2 ) värstafalls beteendet kan i praktiken alltid undvikas bara algoritmen implementeras på ett korrekt sätt Quicksort baseras på dela-och-härska metoden rekursiv implementation 21 Quicksort algoritmen Algoritmen väljer ett element v bland elementen i räckan som skall sortera så att ungefär hälften av elementen är mindre än v och ungefär hälften av elementen är större än v Jämför varje element med v och delar upp räckan i två delmängder S 1 = {element som är mindre än eller lika med v} S 2 = {element som är större än eller lika med v} v v v De två del-listorna sorteras sedan rekursivt på samma sätt 22 11
Quicksort algoritmen Algoritm för att sortera en räcka S om antalet element i S är 0 eller 1, returnera välj ett splitvärde v i S partitionera S-{v} i två disjunkta grupper S 1 = {x S-{v} x v} elementen mindre än eller lika med v S 2 = {x S-{v} x v} elementen större än eller lika med v returnera räckan Quicksort(S 1 ) v Quicksort(S 2 ) Talet v som används för att dela räckan kallas pivot ytterst viktigt att välja pivot-elementet så att räckan delas jämnt i två lika stora delar Ett bra val av pivotvärde är median-av-tre väljer som pivotelement medianen av det första, mellersta och sista värdet i räckan garanterar att en ojämn uppdelning inte kan uppstå t,ex, för färdigt sorterad input 23 Median-av-tre Medianen av N tal är det N/2 :te största av talen det bästa pivot-värdet skulle vara medianen av talen skulle dela talräckan i två lika stora delar tyvärr tar beräkningen av medianvärdet för lång tid, så vi måste välja en enklare metod Ett bra val av pivotvärde är median-av-tre väljer som pivotelement medianen av det första, mellersta och sista värdet i räckan garanterar att en ojämn uppdelning inte kan uppstå för färdigt sorterad input 6 1 4 9 8 3 5 2 7 0 Median=6 24 12
Cutoff För små räckor (N 20) är insättningssortering snabbare än Quicksortering använder insättningssortering för att sortera små delräckor i implementationer av Quicksort algoritmen gör också implementationen enklare, eftersom man inte behöver beakta degenererade fall som t.ex. att ta median-av-tre av en räcka av två tal Ett bra cutoff värde, dvs. storlek på räckan när det lönar sig att ta till insättningssortering, är N = 20 25 Implementation av Quicksort Den publika driver-rutinen för Quicksort tar ett argumet räckan som skall sorteras Anropar en privat rutin quicksort som tar tre argument a räckan som skall sorteras left startindex i a för delräckan som skall sorteras right slutindex i a för delräckan som skall sorteras public static void quicksort(comparable [] a) { quicksort(a, 0, a.length-1); } 26 13
Median-av-tre Median-av-tre rutinen ordnar färdigt a[left], a[center] och a[right] i sorterad ordning det största av de tre värdena placeras sist i a[right] och det minsta i a[left] pivoten placeras nästsist i a[right-1] // Sortera left, center och right, placera pivot i right-1 private static Comparable median3(comparable [] a, int left, int right) { int center = (left + right)/2; if (a[center].compareto(a[left]) < 0) swapreferences(a,left, center); if (a[right].compareto(a[left]) < 0) swapreferences(a,left, right); if (a[right].compareto(a[center]) < 0) swapreferences(a, center, right); // Placera pivoten swapreferences(a,center, right-1); return a[right-1]; } 27 Pekarnas initialisering Vi initialiserar i och j till left+1 och right-2 i partitioneringsfasen a[left] är mindre än pivoten, så vi får en vaktpost för j när den går bakåt pivoten i a[right-1] är en vaktpost för i när den går framåt vi behöver inte explicit kontrollera om vi når endera ändan av räckan, eftersom i kommer att stanna på pivoten och j stannar på elementet i position left left 0 1 4 9 7 3 5 2 6 8 right i j 28 14
Quicksort rutinen private static void quicksort(comparable [] a, int left, int right) { int CUTOFF = 10; if (left + CUTOFF) <= right) // Gör quicksort { Comparable pivot = median3(a, left, right); // Starta partitionering int i = left, j = right - 1; for ( ; ; ) { while (a[++i].compareto(pivot) < 0){} while (a[--j].compareto(pivot) > 0){} if (i < j) swapreferences(a, i, j); else break; } swapreferences(a, i, right-1); // Återställ pivoten i mitten quicksort(a, left, i-1); // Sortera S1 rekursivt quicksort(a, i+1, right); // Sortera S2 rekursivt } else // Gör en insättningssortering på delräckan insertionsort(a, left, right); } 29 Detaljer i implementationen Pekarna i och j initieras till left och right-1 i st. f. left+1 och right-2 de inkrementeras / dekrementeras före de används i jämförelserna: a[++i].compareto(pivot) a[--j].compareto(pivot) rutinen swapreferences kunde skrivas ut explicit för att undvika overheaden av ett proceduranrop en bra kompilator kan automatiskt göra det här om rutinen är deklarerad som en final metod While-looparna är mycket effektiva översätts eller tolkas till mycket effektiv exekverbar kod: inkrementering / dekrementering med ett, jämförelse och hoppinstruktion 30 15
Analys av Quicksort Quicksort är en rekursiv algoritm uttrycker körtiden som en rekurrensrelation Vi gör följande antagande i analysen pivoten väljs slumpmässigt bland elementen i S inget cutoff värde används för små räckor Utan dessa antaganden blir analysen mycket komplicerad men vår förenklade analys ger ändå samma resultat som det generella fallet 31 Rekurrensrelationen för Quicksort Körtiden för att sortera en räcka av längd 0 eller 1 är 1 T(0) = T(1) = 1 Körtiden för att sortera en räcka av längd N består av tiden för två rekursiva anrop, plus en linjär tid för partitioneringen Tiden för att välja pivot är konstant innehåller inga loopar utan bara tre jämförelser Ger rekurrensrelationen T(N) = T(i) + T(N-i-1) + cn där i = S 1 är antal element i S 1 S 1 v S 2 i N i 1 32 16
Värstafallsanalys I värsta fall väljs pivoten alltid så att den är det minsta (eller största) elementet i räckan då är i = 0 och rekurrensen därför (om vi ignorerar T(0) = 1) T(N) = T(N-1) + cn, för N > 1 Rekurrensrelationen löses med teleskopering vi skriver ut alla ekvationerna för räckor av längd mellan N och 2 och summerar ekvationerna T(N) = T(N-1) + cn T(N-1) = T(N-2) + c(n-1) T(N-2) = T(N-3) + c(n-2)... T(2) = T(1) + c*2 v S 2 N 1 33 Värstafallsanalys (forts.) Vi adderar de kvarvarande ekvationerna och får T(N) = T(1) + cn + c(n-1) + c(n-2) +... + c2 d.v.s. T(N) = T(1) + c i = O(N 2 ) N i =2 Quicksort har alltså en värstafallskörtid som är O(N 2 ) i praktiken uppstår aldrig det värsta fallet för alla rekursiva anrop om vi använder median-av-tre partitionering 34 17
Bästafallsanalys I bästa fall delar alltid den valda pivoten input-räckan i två lika stora delar Rekurrensrelationen blir då T(N) = 2T(N/2) + cn båda rekursiva anropen sorterar räckor av storleken N/2 Lösningen av rekurrensrelationen görs med teleskopering på exakt samma sätt som i Mergesort Resultatet blir T(N) = cn log N + N = O(N log N) 35 Genomsnittsfall Antag att varje storlek för S 1 är lika sannolik dvs. en viss storlek på S 1 har sannolikheten 1/N antagandet gäller för vårt val av pivot och partitioneringsstrategi, men inte för vissa andra Då är medelvärdet av T(i) och av T(N-i-1) Vi sätter in medelvärdena för T(i) och T(N-i-1) i rekurrensrelationen T(N) = T(i)+T(N-i-1)+cN och får T( N ) = 2 N N 1 j =0 T( j)+ cn 1 N N 1 j =0 T( j) 36 18
Genomsnittsfall (forts.) Vi multiplicerar ekvationen med N och får N 1 N *T(N) = 2 T( j)+ cn 2 j=0 Vi behöver få bort summatecknet för att förenkla uttrycket. Vi kan teleskopera med ekvationen för N-1 N 2 ( N 1)T( N 1) = 2 T( j)+ c( N 1) 2 j =0 Vi subtraherar den senare ekvationen från den förra och får N *T(N) (N 1)T(N 1) = 2T(N 1)+ 2cN c av summorna blir bara 2T(N-1) kvar skriver ut c(n-1) 2 som c(n 2-2N+1) = cn 2-2cN + c kvar efter subtraktionen blir 2cN - c 37 Genomsnittsfall (forts.) Vi lämnar bort den betydelselösa -c och flyttar över (N-1)T(N-1) till högra sidan och får N*T(N) = (N+1)T(N-1) + 2cN Nu har vi en formel för T(N) i termer av T(N-1) vi förbereder för teleskopering genom att dividera båda sidorna av uttrycket med N(N+1) T( N ) N +1 T( N 1) = + 2c N N +1 Uttrycket för körtiden är nu i en form som kan teleskoperas 38 19
Teleskopering Vi skriver ut alla formler från N till 2 och adderar dem T( N ) N +1 T( N 1) = N T( N 1) = + 2c N N +1 T( N 2) N 1 + 2c N T( N 2) T( N 3) = N 1 N 2 + 2c N 1... T(2) 3 = T(1) 2 + 2c 3 Kvar efter teleskoperingen blir T( N ) N +1 = T(1) N+1 2 + 2c 1 i i =3 39 Körtid i genomsnitt N +1 1 Summan är ungefär ln(n+1) + γ - 3/2 i i= 3 där γ 0.577... är Eulers konstant Vi får alltså T( N ) N +1 = O(log N ) och T(N) = O(N log N) Körtiden för Quicksort är således också i genomsnitt O(N log N) 40 20
Selektionsproblemet Selektionsproblemet: given en input bestående av en lista med N element och ett heltal k, vilket är det k:te största elementet? Specialfall: om k=1 är problemet ekvivalent med att hitta det minsta värdet om k=n är problemet ekvivalent med att hitta det största värdet båda dessa kan lösas i linjär tid, dvs. O(N) om k=n/2 är problemet ekvivalent med att hitta medianen av talen En enkel lösning är att sortera elementen i räckan och returnera elementet i position k körtiden bestäms av den sorteringsalgoritm som används, dvs. O(N 2 ) eller O(N log N) 41 En lösning med en prioritetskö Vi kan också lösa selektionsproblemet med en prioritetskö vi läser in de N elementen i en räcka och anropar buildheap på den för att ordna räckan till en prioritetskö utför sedan k stycken deletemin operationer det sista elementet som tas ut är då det k:te största (eller minsta) Körtiden blir O(N + k log N) O(N) tid för att konstruera prioritetskön med buildheap för varje av de k deletemin operationer som görs krävs O(log N) tid För specialfallet att hitta medianen ger det här en O(N log N) algoritm 42 21
En linjär-tids algoritm för selektion Quicksort kan enkelt modifieras för att lösa selektionsproblemet kallas för Quickselect algoritmen Quickselect har en genomsnittlig körtid på O(N) i värsta fall är körtiden fortfarande O(N 2 ), på samma sätt som i Quicksort Det värsta fallet inträffar på samma sätt som in Quicksort valet av pivot-värde går alltid så att den ena av delmängderna s 1 och s 2 blir tom 43 Quickselect algoritmen Låt S i beteckna antalet element i mängden S i Algoritmen: 1. Om S = 1, så är k = 1 och vi returnerar elementet i S som svar. Om ett cutoff värde används och S cutoff, så sortera S och returnera det k:te minsta elementet 2. Välj ett pivotelement, v S 3. Partitionera S-{v} i S 1 och S 2 på samma sätt som i quicksort 4. Om k S 1 så finns det k:te minsta elementet i S 1. Returnera i så fall quickselect(s 1, k) Om k =1+ S 1 så är pivoten det k:te minsta elementet, vilket returneras som svar. Annars ligger det k:te minsta elementet i S 2, och är det (k- S 1-1):te minsta elementet i S 2. Gör ett rekursivt anrop och returnerar quickselect(s 2, k - S 1-1) 44 22
Illustration k S 1 det k:te minsta elementet finns i mängden S 1 det är också det k:te minsta i S 1 k = S 1 +1 det k:te minsta elementet är pivoten k > S 1 +1 det k:te minsta elementet finns i S 2 är det k - S 1-1:te minsta i S 2 k S 1 S 2 v v k S 1 S 2 45 Körtid för Quickselect I motsats till quicksort så gör quickselect bara ett rekursivt anrop (i stället för två) Värsta fallet för quickselect är identiskt med quicksort och är O(N 2 ) inträffar när en av S 1 och S 2 är tom för varje rekursivt anrop Den genomsnittliga körtiden däremot är O(N) analysen av körtiden är liknande som för quicksort I implementationen av quickselect hittas det k:te minsta elementet i positionen k-1 om man inte vill förstöra den ursprungliga ordningen måste man göra en kopia av räckan 46 23
Implementation av Quickselect private static void quickselect(comparable [] a, int left, int right, int k) { if (left + CUTOFF) <= right) { Comparable pivot = median3(a,left, right); int i = left, j = right - 1; // Starta partitionering for ( ; ; ) { while(a[++i].compareto(pivot) < 0){} while(a[--j].compareto(pivot) > 0){} if (i < j) swapreferences(a, i, j); else break; } swapreferences(a, i,right-1); // Återställ pivoten if(k - 1 < i) quickselect(a, left, i-1, k); else if(k - 1 > i) quickselect(a, i+1, right, k); } else //Gör en insättningssortering på delräckan insertionsort(a, left, right); } 47 Allmän undre gräns för sortering Kan man göra bättre sorteringsalgoritmer än O(N log N)? Svaret är nej vi kan visa att sorteringsmetoder som använder jämförelser av element kräver Ω(N log N) jämförelser ger en undre gräns för alla sorteringsalgoritmer som jämför elementen parvis med varandra Vi skall bevisa att alla sorteringsalgoritmer som jämför element med varandra kräver i värsta fall log N! jämförelser och i genomsnitt log N! jämförelser Vi antar att alla N element är olika metoden måste ju även fungera för detta fall 48 24
Beslutsträd En algoritm som sorterar med användande av jämförelser kan representeras av ett beslutsträd Antal jämförelser som görs av sorteringsalgoritmen är lika med djupet av det djupaste lövet i exemplet i figur 7.17 i boken (och på nästa slide) använder algoritmen tre jämförelser i värsta fall Genomsnittliga antalet jämförelser är lika med genomsnittliga djupet av löven i trädet För att bevisa vårt påstående behöver vi använda några grundläggande egenskaper hos träd 49 Exempel på ett beslutsträd Ett beslutsträd för sortering av tre element Figur 7.17 b < c a < b < c a < b < c a < c < b a < b < c a < c < b c < a < b a < b c < a < b a < b < c a < c < b b < a < c b < c < a c < a < b c < b < a b < a b < a < c b < c < a b < a < c b < c < a c < b < a a < c c < a 2 b < c 3 c < b 4 c < b a < c < b a < c b < a < c c < a b < c < a 8 9 10 11 5 1 6 c < b < a 7 50 25
Antal löv i ett träd Lemma 7.1 Låt T vara ett binärt träd av djupet d Då har T högst 2 d löv Bevis genom induktion Om d = 0 (dvs. trädet består av en enda nod = roten) så finns det inga löv, och påståendet gäller annars, om d 1 finns det en rotnod som inte är ett löv, och ett vänstra och högra subträd vardera med ett djup högst d-1 enligt induktionshypotesen kan dessa två ha högst 2 d-1 löv vardera, vilket betyder att hela trädet har högst 2 d löv Detta bevisar lemmat 51 Undre gräns för sortering Lemma 7.2 Ett binärt träd med L löv har ett djup på högst log L följer direkt av Lemma 7.1 Teorem 7.6 En sorteringsalgoritm som bara använder jämförelser mellan element kräver åtminstone log N! jämförelser i värsta fall Bevis: ett beslutsträd för att sortera N element har N! löv dvs. lika många som antalet permutationer av de N elementen teoremet följer nu av Lemma 7.2 52 26
Undre gräns för sortering Teorem 7.7 En sorteringsalgoritm som bara använder jämförelser mellan element kräver Ω(N log N) jämförelser dvs. N log N är en undre gräns för antalet jämförelser det går inte att konstruera sorteringsalgoritmer med en bättre körtid Bevis: vi visar att log N! är Ω(N log N) dvs. att N log N är en undre gräns för log N! 53 Bevis för Teorem 7.7 log N! = log(n * (N-1) * (N-2) *... * 2 * 1) använder nu log(a*b) = log(a) + log(b) log N! = log(n)+log(n-1)+log(n-2)+...+log(2)+log(1) om vi lämnar bort de N/2 sista termerna i summan får vi något som är mindre än det ursprungliga uttrycket log N! log(n) + log(n-1) + log(n-2) +... + log(n/2) om vi ersätter alla N/2 termerna i summan med den minsta får vi ett ännu mindre uttryck N/2 log(n/2) vi använder log A/B = log A - log B = N/2 (log N - log 2) 54 27
Bevis för Teorem 7.7 (forts) Vi har alltså log N! N/2 (log N - log 2) sätter in log 2 = 1 log N! N/2 (log N - 1) multiplicerar in N/2 log N! N/2 log N - N/2 = Ω(N log N) 55 Bucket sort Vi bevisade just att man inte kan sortera effektivare än O(N log N) i värsta fall om man bara jämför element parvis vi presenterar en sorteringsalgoritm som har linjär körtid, dvs. O(N) kallas bucket sort eller postrumssortering Antag att vi har n element, alla positiva heltal mindre än m tag en räcka count av storlek m, initialiserad till noll läs elementen ett åt gången när vi läser ett element med värdet i inkrementerar vi count[i] med ett när vi har behandlat alla element går vi igenom räckan count och skriver ut så många element i som count[i] anger Körtiden är O(m+n) n operationer för att läsa inputräckan m operationer för att skriva ut resultaträckan Om m är O(n) blir körtiden O(n) dvs. linjär 56 28
Bucket sort (forts.) Algoritmen använder en mera kraftfull operation än jämförelser mellan ett par av element när vi läser ett element med värdet i och inkrementerar count[i] gör vi en m-vägs jämförelse vi jämför elementet med alla de andra m-1 möjliga värdena, i ett enda algoritmsteg Det här är möjligt tack vare antagandet att alla element är positiva heltal mindre än m vi kan högst ha m olika värden i inputräckan Algoritmen kräver O(m) minnesutrymme kan inte använda algoritmen om m är för stort 57 29