Equazioi di ricorreza e Ordiameti lieari Iformatica@SEFA 0/0 - Lezioe Massimo Lauria <massimo.lauria@uiroma.it> Veerdì, Novembre 0 Ricorsioe ed equazioi di ricorreza Aalizzado il Mergesort abbiamo visto che il tempo di esecuzioe di u algoritmo ricorsivo può essere espresso come u equazioe di ricorreza, ovvero u equazioe del tipo () C whe c 0 () i a i (g i ()) + f () whe > c 0 dove a i, C e c 0 costati positive itere e g i e f soo fuzioi da N a N, e vale sempre che g i () <. Ad esempio il ruig time di Mergesort è () (/)+Θ(). Metre il ruig time della ricerca biaria è: () (/) + Θ() Ci soo diversi metodi per risolvere le equazioi di ricorreza, o comuque per determiare se () è O(g) oppure Θ(g) per qualche fuzioe g. http://massimolauria.et/courses/ifosefa0/ I molti casi o è ecessario essere precisi el quatificare (), oppure quali soo i valori esatti di C e c 0. Nella maggior parte quei valori codizioao () solo di u fattore costate, che viee comuque igorato dalla otazioe asitotica. Lo stesso vale per la fuzioe f (): riscalarla icide sulla soluzioe della ricorreza per u fattore costate.
Spesso ci iteressa solo l asitotica e per di più a volte ci iteressa solo ua limitazioe superiore dell ordie di crescita.. Metodo di sostituzioe Si tratta di idoviare la soluzioe della ricorreza e verificarla dimostradoe la correttezza via iduzioe matematica. Ad esempio risolviamo la ricorreza del Mergesort utilizzado come tetativo di soluzioe () c log per c grade abbastaza. Il caso base è verificato scegliedo c > (). Assumiamo poi che merge utilizzi d operazioi e che c > d. E utilizziamo l ipotesi iduttiva per sostituire ella ricorreza. () (/)+d c(/) log(/)+d c log c+d c log L uso dell iduzioe per risolvere la ricorreza può portare ad errori legati alla otazioe asitotica. Facciamo coto che vogliamo dimostrare che () O(), ovvero () c per qualche c. () (/) + d c/ + d c + d Si sarebbe tetati di dire che (c + d) O() e che quidi ci siamo riusciti. uttavia la dimostrazioe usa l ipotesi iduttiva che ( ) c per < e quidi se da questa ipotesi o deduciamo la stessa forma () c l iduzioe o procede correttamete.. Metodo iterativo e alberi di ricorsioe É il metodo che abbiamo utilizzato durate l aalisi delle performace di Mergesort. L idea è quella di iterare l applicazioe della ricorreza fio al caso base, sviluppado la formula risultate e utilizzado maipolazioi algebriche per determiare il tasso di crescita. Esempio: Aalizziamo la ricorreza () ( / ) +
()... + ( / ) + / + 9( / ) + / + 9 / + ( / ) + / + 9/ + / +... + log () Θ() i + Θ( log () ) i 0 + o() O() Per visualizzare questa maipolazioe è utile usare u albero di ricorsioe. Ovvero ua struttura ad albero che descrive l evoluzioe dei termii della somma. Vediamo ad esempio () (/) + ) ) () ) ) ) ) ) ) Θ() Θ() Θ() Θ() Θ() Θ() Θ() Θ() L albero ha log livelli Il primo livello ha operazioi, il secodo e ha /, il terzo e ha /,... L ultimo ha Θ() operazioi. il umero totale di operazioi è Θ() + ( + + +...) Θ( )
. Master heorem Questi due metodi richiedoo u po di abilità e soprattutto u po di improvvisazioe, per sfruttare le caratteristiche di ogi esempio. Esiste u teorema che raccoglie i casi più comui e forisce la soluzioe della ricorreza direttamete. eorema. Siao a e b costati e f () ua fuzioe, e () defiito sugli iteri o egativi dalla ricorreza: () a(/b) + f (), dove /b rappreseta /b o /b. Allora () può essere asitoticamete limitato come segue. Se f () O( log b (a) ɛ ) per qualche costate ɛ > 0, allora () Θ( log b (a) );. Se f () Θ( log b (a) ) allora () Θ( log b (a) log );. Se f () Ω( log b (a)+ɛ ), per qualche costate ɛ > 0, e se a f (/b) < c f () per qualche c < e per ogi sufficietemete grade, allora () Θ( f ()). Notate che il teorema o copre tutti i casi. Esistoo versioi molto più sofisticate che coproo molti più casi, ma questa versioe è più che sufficiete per i ostri algoritmi. Mergesort è il caso, co a b e f () Θ(). Ricerca biaria è il caso, co a, b e f () Θ(). () (/) + è il caso. No vedremo la dimostrazioe ma è sufficiete fare uo sketch dell abero di ricorsioe per vedere che questo ha altezza log b ; ogi odo ha a figli; al livello più basso ci soo a log b () log b (a) odi che costao Θ() ciascuo; i odi a distaza i da quello iiziale costao, complessivamete, a i f (/b i ).
Duque il costo totale è: Θ( log b (a) ) + log b () a j f (/b j ). I oguo dei j 0 tre casi euciati dal teorema, l asitotica è quella idicata. Ordiameti i tempo lieare Esistoo modi di ordiare che impiegao solo Θ(), ma questi metodi o soo, ovviamete, ordiameti per cofroto. Sfruttao ivece il fatto che gli elemeti da ordiare appartegao ad u domiio limitato.. Esempio: Coutig Sort Il couti sort si basa su u idea molto semplice: se ad esempio dobbiamo ordiare ua sequeza di elemeti, dove oguo dei quali è u umero da a 0, possiamo farlo facilmete i tempo lieare:. teedo 0 cotatori,..., 0 ;. fare ua scasioe della lista aggiorado i cotatori;. riporre ella lista copie di, copie di, ecc... def coutigsort(seq): if le(seq)==0: retur # operazioi a = mi(seq) b = max(seq) # creazioe dei cotatori couter =[0]*(b-a+) for x i seq: couter[x-a] += # costruzioe dell output output=[] for v,v i eumerate(couter,start=a): output.exted( [v]*v ) retur output prit( coutigsort([,,,,,9,,,,,,,,,9,9,,,,,,,,,,])) 9 0 [,,,,,,,,,,,,,,,,,,,,,,, 9, 9, 9] Ovviamete qualuque tipo di dato ha u miimo e u massimo, i ua lista fiita. uttavia se la lista cotiee elemeti i u domiio molto grade (e.g. umeri tra 0 e 0 dove è la lughezza dell iput) allora questo algoritmo è meo efficiete degli algoritmi per cofroti.
. Dati cotestuali Negli algoritmi di ordiameto per cofroto ci i dati origiali vegoo spostati o copiati all itero della sequeza, e tra la sequeza ed evetuali liste temporaee. Si immagii ad esempio il caso i cui ogi elemeto ella lista di iput sia ua tupla (i,dati) dove i è la chiave rispetto a cui ordiare, ma dati ivece è iformazioe cotestuale arbitraria. Negli ordiameti per cofroto l iformazioe cotestuale viee spostata isieme alla chiave. La ostra implemetazioe del coutigsort o gestisce questo caso, e va modificata.. Dati i cotatori i, calcoliamo i quale itervallo della sequeza di output vadao iseriti gli elemeti i iput co chiave i. L itervallo è tra le due quatità i j 0 j (icluso) e i j 0 j (escluso).. Scorriamo l iput uovamete e copiamo gli elemeti i iput ella lista di output. def coutigsort(seq): if le(seq)==0: retur # operazioi a = mi(k for k,_ i seq) b = max(k for k,_ i seq) # creazioe dei cotatori couter =[0]*(b-a+) for k,_ i seq: couter[k-a] += # posizioi fiali di memorizzazioe posizioi =[0]*(b-a+) for i i rage(,le(couter)): posizioi[i]=posizioi[i-]+ couter[i-] # costruzioe dell output for k,data i seq[:]: seq[ posizioi[k-a]]=(k,data) posizioi[k-a] += sequeza=[(,"paul"),(,"rigo"),(,"george"),(,"pete"),(,"stuart"),(,"joh") ] coutigsort(sequeza) prit(sequeza) 9 0 9 0 [(, george ), (, pete ), (, paul ), (, stuart ), (, rigo ), (, joh )]
Ordiameto stabile Nell esempio precedete abbiamo visto che ci soo elemeti diversi che hao la stessa chiave di ordiameto. I geerale ua lista da ordiare può coteere elemeti "uguali" el seso che seppure distiti, per quato riguarda l ordiameto possoo essere scambiati di posizioe seza problemi, ad esempio (, george ) e (,"pete") possoo essere ivertiti ella ordiata, seza che l ordiameto sia ivalidato. Si dice che u ordiameto è stabile se o modifica l ordie relativo degli elemeti che hao la stessa chiave. U iversioe ell ordiameto di ua sequeza S è ua coppia di posizioi i, j ella lista, 0 i < j < le(s), tali che il valori i S[i] si trova dopo il valore i S[j] ua volta fiito l ordiameto. U ordiameto stabile miimizza il umero di iversioi. utti gli ordiameti che abbiamo visto fio ad ora soo stabili. Per esempio el caso di ordiameti per cofroto è capitato di dover fare operazioi del tipo if S[i] <= S[j]: operazioi che o causao u iversioe tra S[i] e S[j] else: operazioi che causao u iversioe tra S[i] e S[j] dove i è miore di j. Se ivece dell operatore <= avessimo utilizzato l operatore < allora il comportameto dell algoritmo sarebbe cambiato solo el caso i cui S[i] fosse stato uguale a S[j]. L ordiameto sarebbe stato comuque valido ma o sarebbe più stato u ordiameto stabile.. Ordiameti multipli a cascata Se avete ua lista di brai el vostro lettore musicale tipicamete avrete i vostri brai ordiati, semplificado, per. Artista. Album. raccia el seso che i brai soo ordiati per Artista, quelli dello stesso artista soo ordiati per Album, e quelli ello stesso album soo ordiati
U modo per otteere questo risultato è ordiare prima per raccia, poi per Album, e poi per Artista. Questo avviee perché gli ordiameti usati soo stabili. Quado si ordia per Album, gli elemeti co lo stesso Album verrao mateuti elle loro posizioi relative, che erao ordiate per raccia. Successivamete ua volta ordiati per Artista, i brai dello stesso Artista mategoo il loro ordie relativo, ovvero per Album e raccia. I geerale è possibile ordiare rispetto a ua serie di chiavi differeti, key, key,... keyn, i maiera gerarchica, ordiado prima rispetto keyn e poi adado su fio a key. Modifichiamo coutisort per farlo lavorare su ua chiave di ordiameto arbitraria. def default_key(x): retur x def coutigsort(seq,key=default_key): if le(seq)==0: retur # operazioi a = mi(key(x) for x i seq) b = max(key(x) for x i seq) # creazioe dei cotatori couter =[0]*(b-a+) for x i seq: couter[key(x)-a] += # posizioi fiali di memorizzazioe posizioi =[0]*(b-a+) for i i rage(,le(couter)): posizioi[i]=posizioi[i-]+ couter[i-] # costruzioe dell output for x i seq[:]: seq[ posizioi[key(x)-a]]=x posizioi[key(x)-a] += 9 0 9 0 Ordiare sequeze di iteri gradi Radixsort Abbiamo già detto che il coutigsort è u ordiameto i tempo lieare, adatto a ordiare elemeti le cui chiavi di ordiameto hao u rage molto limitato. Ma se i umeri soo molto gradi che possiamo fare? No possiamo ordiare ua lista di 000000 di umeri positivi da bit co il coutig sort, perché la lista dei cotatori sarebbe eorme (e piea di zeri). Però possiamo cosiderare u umero di come ua tupla di elemeti b... b 0 i {0, } ed utilizzare u ordiameto stabile per ordiare rispetto a b 0
ordiare rispetto a b... ordiare rispetto a b Ivece di lavorare bit per bit possiamo cosiderare u umero di come ua tupla di elemeti b b b b 0 i {0,..., } ed utilizzare u ordiameto stabile per ordiare rispetto a b 0 ordiare rispetto a b ordiare rispetto a b ordiare rispetto a b Naturalmete usare ua decomposizioe più fitta richiede più chiamate ad ordiameto, ma oguo su u domiio più piccolo. Il giusto compromesso dipede dalle applicazioi. Ora calcoliamo le chiavi b i utilizzado quattro fuzioi. def key0(x): retur x & def key(x): retur (x//) & def key(x): retur (x//(*)) & def key(x): retur (x//(**)) & x =** + ** + ** - prit(key0(x), key(x), key(x), key(x)) 9 0 Duque possiamo implemetare radixsort (che ricordiamo, per come è stato realizzato fuzioa solo su umeri positivi di bit). def radixsort(seq): for my_key i [key0,key,key,key]: coutigsort(seq,key=my_key) sequeza=[,,,,,9,,,,,,,,,9,9,,,,,,] radixsort(sequeza) prit(sequeza) [,,,,,,,,,,,,, 9, 9, 9,,,,,, ] 9
. Plot di esempio I questo plot vediamo il tempo impiegato da questi algoritmi per ordiare ua lista di umeri tra 0 e 000000. Questi algoritmi soo molto più veloci di bubblesort e isertiosort e questo si vede ache i pratica. Le liste di umeri o soo particolarmete lughe (solo 00000 elemeti), ma impossibili da ordiare utilizzado ordiameti Θ( ). Vediamo tre algoritmi: Mergesort Coutigsort co itervallo [0,0000] e [0,000000] Radixsort co chiavi da bit e co chiavi da bit Il ruig time di coutigsort è molto più codizioato dall itervallo di valori che dalla lughezza della sequeza da ordiare (almeo per soli 00000 elemeti da ordiare). Radixsort utilizza più chiamate a coutigsort ma su u domiio più piccolo. Due chiavi a bit soo più efficieti di chiavi a bit, e bit producoo uo spazio delle chiavi di elemeti. Uo spazio più semplice da gestire per coutigsort rispetto ad u domiio di 000000 elemeti. 0