1 (8) Lösning av några vanliga rekurrensekvationer Rekursiv beräkning av X n En rekursiv funktion som beräknar x n genom upprepad multiplikation, baserat på potenslagarna X 0 = 1 X n+1 = X X n float pow(float x,int n) { if ( n == 0 ) return 1.0; else return x*pow(x,n-1); Låt T(n) vara antalet multiplikationer som utförs då pow(x,n) anropas. Vi får de två rekurrensekvationerna: T(0) = 0 (1) T(n) = T(n-1) + 1 (2) Motivering: T(0) = 0 eftersom ingen multiplikation utförs då pow(x,0) anropas. I else-grenen utförs en multiplikation plus antalet multiplikationer som utförs av anropet pow(x,n-1), dvs 1 + T(n-1) multiplikationer. Rekurrensekvationen löses genom att utveckla (2) genom successiv substitution: Av (2) följer att T(n-1) = T(n-2) + 1 T(n-2) = T(n-3) + 1 T(n-3) = T(n-4) + 1 o.s.v. Högerledet i ekvation (2) kan då skrivas om med successiv substituition: T(n) = [T(n-2) + 1] + 1 substitution av T(n-2) + 1 för T(n-1) i (2) = T(n-2) + 2 förenkling = [T(n-3) + 1] + 2 substitution = T(n-3) + 3 förenkling = [T(n-4) + 1] + 3 substitution = T(n-4) + 4 förenkling Om proceduren upprepas når vi till slut T(n) = T(n-n) + n = T(0) + n = 0 + n enligt (1) = n T(n) = T(n-1) + 1 T(n-2) + 1 T(n-3) + 1 T(n-4) + 1...
Vi kan alltså dra slutsatsen att T(n) = O(n) 2 (8) Följande rekursiva funktion för beräkning av x n baseras på potenslagarna X 0 = 1 X 2n = X n X n X 2n+1 = X X n X n float pow2(float x,int n) { if ( n == 0 ) return 1.0; else if ( n % 2 == 0 ) return pow2(x,n/2)*pow2(x,n/2); else return x*pow2(x,n/2)*pow2(x,n/2); Vi får rekurrensekvationerna: T(0) = 0 (1) T(1) = 2 (2) T(n) = 2T(n/2) + c (c 2) (3) Motivering: I det ena rekursionsfallet sker en multiplikation, men två i det andra, alltså högst två multiplikationer, därav konstanten c. T(n) = 2[2T(n/4) + c] + c substitution = 4T(n/4) + 3c förenkling = 4[2T(n/8) + c] + 3c substitution = 8T(n/8) + 7c förenkling = 8[2T(n/16) + c] + 7c substitution = 16T(n/16) + 15c förenkling Vi ser ett tydligt mönster av tvåpotenser och i det allmänna fallet har högerledet formen 2 k T(n/2 k ) + (2 k -1) c Genom att betrakta de (oändligt många) n som är tvåpotenser hoppas vi kunna reducera bort T-termen. Antag därför att n = 2 k, då får vi T(n) = n T(n/n) + (n-1) c = n 2 + (n-1) c enligt (2) Vi ser att T(n) = O(n) även i detta fall. Man kan säga att försöket att dela problemet i hälften så stora delproblem misslyckades eftersom varje delproblem löses två gånger. Rekursiva funktioner bör inte utföra redundant arbete.
En D&C-algoritm för X n 3 (8) En effektivare Divide & Conquer-algoritm beräknar x n utan upprepning av redan gjorda beräkningar: float dcpow(float x,int n) { if ( n == 0 ) return 1.0; else { float p = dcpow(x,n/2); if ( n % 2 == 0 ) return p*p; else return x*p*p; Vi får rekurrensekvationerna: T(0) = 0 (1) T(1) = 2 (2) T(n) = T(n/2) + c (c 2) (3) T(n) = [T(n/4) + c] + c substitution = T(n/4) + 2c förenkling = [T(n/8) + c] + 2c substitution = T(n/8) + 3c förenkling = [T(n/16) + c] + 3c substitution = T(n/16) + 4c förenkling Vi ser åter ett tydligt mönster av tvåpotenser och i det allmänna fallet har högerledet formen Antag att n = 2 k, då får vi (eftersom k = 2 log n): T(n) = T(n/n) + ( 2 log n) c = 2 + ( 2 log n) c enligt (2) Alltså är T(n) = O(log n). T(n/2 k ) + k c Anm. Denna analys är i princip även tillämplig för binärsökning.
Hanois torn 4 (8) Funktionen move finns i OH från den första föreläsningen om rekursion. void move(char A,char B,char C,int n) { if ( n > 0 ) { move(a,c,b,n-1); System.out.println(A + --> + B); move(c,b,a,n-1); Låt T(n) vara antalet utskrifter som utförs då move(x,y,z,n) anropas. Vi får rekurensekvationerna: T(0) = 0 (1) T(n) = 2T(n-1) + 1 (2) T(n) = 2[2T(n-2) + 1] + 1 substitution = 4T(n-2) + 3 förenkling = 4[2T(n-3) + 1] + 3 substitution = 8T(n-3) + 7 förenkling = 8[2T(n-4) + 1] + 7 substitution = 16T(n-4) + 15 förenkling I det allmänna fallet har högerledet formen Antag att n = k, då får vi: T(n) = 2 n T(n - n) + (2 n -1) = 2 n 0 + (2 n -1) enligt (1) 2 k T(n - k) + (2 k -1) Vi kan dra slutsatsen att T(n) = O(2 n ) så move har exponentiell tidskomplexitet. Anm. Även induktionsbeviset sist i OH visar att T(n) = 2 n - 1.
Största delsegmentsumman i ett heltalsfält 5 (8) Den rekursiva Divide & Conquer-algoritmen för beräkning av största delsegmentsumman i ett heltalsfält som finns i kursboken har komplexiteten O(n log n). Låt T(n) vara antalet jämförelser som utförs då maxsumrec anropas med ett heltalsfält med n element. Rekurensekvationerna för algoritmen är: T(1) = 1 (1) T(n) = 2T(n/2) + n (2) Motivering: Det sker två rekursiva anrop för hälften så långa fältsegment. Termen n kommer från det faktum att de två looparna analyserar O(n) fältelement där n är längden hos det aktuella segmentet. T(n) = 2[2T(n/4) + n/2] + n substitution = 4T(n/4) + 2n förenkling = 4[2T(n/8) + n/4] + 2n substitution = 8T(n/8) + 3n förenkling = 8[2T(n/16) + n/8] + 3n substitution = 16T(n/16) + 4n förenkling I det allmänna fallet har högerledet formen Antag att n = 2 k, då får vi (eftersom k = 2 log n): 2 k T(n/2 k ) + k n T(n) = n T(n/n) + ( 2 log n) n = n 1 + ( 2 log n) n enligt (1) och vi kan därför dra slutsatsen att T(n) = O(n log n). Anm. Denna analys är i princip också tillämplig för sorteringsalgoritmerna merge sort och quicksort.
Kostnadsamortering vid fältdubblering 6 (8) Vid fältbaserade implementeringar av t.ex. stackar, FIFO-köer, prioritetsköer och hashtabeller används fältdubblering för att kunna amortera tidsåtgången för kopiering av elementen från ett fullt fält till ett tomt större fält. T.ex. ger denna teknik stackoperationen push komplexiteten O(1) i genomsnitt, kopiering inräknat. Vi skall nu visa att kopieringskostnaden för n push-operationer är O(n) då fältdubblering används. Låt T(n) vara den ackumulerade kopieringskostnaden för att sätta in n element i en stack. Antag att vi har en stack med n/2 element och att fältet är fullt. Den ackumulerade kostnaden för tidigare kopieringar av dessa n/2 element är då T(n/2). Elementen skall nu kopieras till ett tomt fält med n platser. Om kostnaden för kopiering per fältelement är C 1 så kostar kopieringen av n/2 element C 1 n/2. När kopieringen är klar kan vi sätta in n/2 element med push utan att någon kopiering behöver ske. Intuitivt kan man inse att för varje element som kopieras från det gamla fältet till det nya, så kan ett annat element senare sättas in med push i den lediga halvan utan att någon kopiering behöver ske. Figuren nedan beskriver situationen: T(n/2) n/2 kopiering C 1 n/2 Om vi sätter T(1) = C 0 och C = C 1 /2 så får vi rekurrensekvationerna: T(1) = C 0 (1) T(n) = T(n/2) + C n (2) T(n) = [T(n/4) + C n/2] + C n substitution = T(n/4) + 3/2 C n förenkling = [T(n/8) + C n/4] + 3/2 C n substitution = T(n/8) + 7/4 C n förenkling = [T(n/16) + n/8] + 7/4 C n substitution = T(n/16) + 15/8 C n förenkling I det allmänna fallet har högerledet formen Antag att n = 2 k, då får vi n/2 push utan kopiering T(n/2 k ) + ((2 k - 1) / (2 k-1 )) C n T(n) = T(n/n) + ((n-1)/(n/2)) C n = C 0 + (n-1) 2 C enligt (1) forts
7 (8) Uppenbarligen är T(n) = O(n), d.v.s. den totala kostnaden för kopiering av element vid insättning av n element med push växer linjärt med antalet insatta element. Således har push i genomsnitt komplexiteten O(1). Statisk fältutvidgning Som en jämförelse kan vi analysera komplexiteten för fältbaserad lagring då fältstorleken ökas med ett konstant antal nya platser C. Låt som förut T(n) vara den ackumulerade kopieringskostnaden för att sätta in n element i en stack. Antag att vi har en stack med n - C element och att fältet är fullt. Den ackumulerade kopieringskostnaden för dessa element är T(n - C). Elementen skall nu kopieras till ett tomt fält med n platser. Om kopieringskostnaden per fältelement är C 1 så kostar kopieringen av n - C element C 1 (n - C). När kopieringen är klar kan vi sätta in C element med push utan att någon kopiering behöver ske. För enkelhets skull antar vi att den initiala fältlängden är C, då kommer fältlängden alltid att vara en multipel av C. T(n - C) n - C kopiering C 1 (n - C) C push utan kopiering Om vi sätter T(1) = C 0 får vi rekurrensekvationerna: T(0) = C 0 (1) T(n) = T(n - C) + C 1 (n C) (2) T(n) = [T(n - 2C) + C 1 (n 2C)] + C 1 (n C) substitution = T(n - 2C) + C 1 ((n 2C) + (n C)) förenkling = [T(n - 3C) + C 1 (n 3C)] + C 1 ((n 2C) + (n C)) substitution = T(n - 3C) + C 1 ((n 3C) + (n 2C) + (n C)) förenkling = [T(n - 4C) + C 1 (n 4C)] + C 1 ((n 3C) + (n 2C) + (n C)) substitution = T(n - 4C) + C 1 ((n 4C) + (n 3C) + (n 2C) + (n C)) förenkling = T(n - kc) + C 1 ((n kc) + + (n 3C) + (n 2C) + (n C)) Antag att k = n/c, då får vi T(n) = T(0) + C 1 (0 + C + 2C + 3C + + (n C)) = C 0 + C 1 C = C 0 + C 1 C n/c termer Således är T(n) = O(n 2 ) så den genomsnittliga tidskomplexiteten per push är med denna metod O(n).
8 (8) Intuitivt kan man inse att ju större fältet blir, desto mindre andel av helheten utgörs av lediga celler efter varje omallokering, och desto större andel består av element som måste kopieras. Om vi sätter konstanten C till 1 måste faktiskt n element kopieras före varje push-operation. När vi betraktar asymptotisk komplexitet är värdet på C oväsentligt. När antalet element växer mot oändligheten kommer kopieringsarbetet att dominera beräkningstiden, oavsett värdet på C. Slutsatsen blir att detta sätt att hantera fält inte bör användas, utan den först nämnda metoden vilken i genomsnitt ger konstant tidsåtgång per push-operation.