Scarica Divide et impera - java e più Appunti in PDF di Fondamenti di informatica solo su Docsity!
Divide et impera
Divide et impera
- La tecnica detta “ divide et impera ” è una strategia generale per impostare algoritmi (par. 9.4).
- Consideriamo un problema P e sia n la dimensione dei dati, la strategia consiste nel: - suddividere il problema in k sottoproblemi Pi di dimensione inferiore (ciascuno di dimensione ni ) e successivamente riunire i risultati ottenuti dalle k soluzioni.
- La frase è attribuita a Filippo il Macedone e fu un principio politico: mantenere divise le popolazioni dominate per poter governare con più facilità.
Divide et impera
- Se i k sottoproblemi sono “formalmente” simili al problema di partenza, si ottiene una scomposizione ricorsiva. Ci deve pertanto essere una dimensione h del problema che porti ad una risoluzione diretta, vale a dire che non necessiti della ricorsione.
- Indichiamo con:
- S l’insieme dei dati
- k il numero dei sottoproblemi
- h la dimensione limite
- Si può scrivere uno schema generale per la scomposizione.
Divide et impera
algoritmo DIVETIMP (S, n) se n < h allora risolvere direttamente il problema P altrimenti dividere S in k sottoinsiemi risolvere separatamente i k sottoproblemi P 1 , …, Pk: DIVETIMP(S 1 ,n 1 ), … , DIVETIMP(Sk,nk) riunire i risultati ottenuti //finese //fine algoritmo
Divide et impera: complessità
- Indichiamo con T(n) la complessità del problema P sull’insieme dei dati di dimensione n ; poiché l’algoritmo è ricorsivo si ottengono delle formule “di ricorrenza”:
T(n) = costante n<h T(n) = D(n) + C(n) + T(n 1 ) + T(n 2 ) + … T(nk)
- D(n) : complessità dell’algoritmo per dividere l’insieme
- C(n) : complessità dell’algoritmo per riunire i risultati
- T(ni) : complessità dell’algoritmo sull’insieme di dimensione ni.
Ordinamenti quicksort
e mergesort
Quicksort
- L’ idea è la seguente:
- è più conveniente ordinare due array di s e t componenti piuttosto che un array di n componenti (s + t = n)
- si aumenta l’efficienza dell’ordinamento scambiando elementi lontani tra loro. (par. 9.4)
Quicksort
- Verifichiamo la prima idea.
- Supponiamo s=t=n/2 e prendiamo la formula n(n-1)/2 che rappresenta la complessità dell’ordinamento nel caso peggiore e riscriviamola per n/2 invece che n: 2 ·[(n/2) (n/2 – 1) /2] = n^2 /4 – n/
- Confrontiamo le formule: n^2 /4 – n/2 < n^2 /2 – n/2 = (n^2 – n)/2 = = n ·(n-1) /
Quicksort
- Come scegliere l’elemento conf?
- Dobbiamo stabilire un criterio che si possa facilmente ripetere in tutte le suddivisioni successive.
- Stabiliamo di scegliere la prima componente di quella porzione di array (da n1 a n2) che vogliamo ordinare: v[n1].
- Ci sono varie scritture dell’algoritmo quicksort, alcune ottimizzano il numero di confronti, ma lasciano inalterata la complessità.
Quicksort
- Per realizzare la partizione avremo bisogno di due indici: un indice i che scorre l’array con valori crescenti e che parte dalla posizione successiva a quella di conf (i=n1+1), un altro indice k che descrive l’array con valori decrescenti e parte dall’ultima posizione (k=n2).
- Quando questi due indici saranno uguali avremo terminato la partizione e si potrà “sistemare” conf al suo posto.
- Vediamo un progetto per l’algoritmo di partizione.
Quicksort
algoritmo partizione(n1, n2, v) conf ← v[n1] i ← n1+ k ← n mentre i ≠ k eseguire mentre v[i] ≤conf e i ≠ k eseguire i ← i+ //fine mentre mentre v[k] ≥conf e i ≠ k eseguire k ← k- //finementre scambiare v[i] con v[k]
Quicksort
//finementre: ciclo esterno //sistemare conf nella posizione k=i: è al suo posto se v[k] > conf allora k ←k- //finese scambiare v[n1] con v[k] //fine partizione
- E necessario il confronto tra conf e v[k]?
Quicksort
10 5 11 1 13 2 20 conf i=2 i=3 k=6 k=
i=4 i= k= i = k = 5 termina anche il ciclo esterno v[k]>conf (10>13) quindi k- 1 5 2 10 13 11 20
Quicksort
- Esempio. 20 3 1 10 9 7 11 conf i=2 i=3 i=4 i=5 i=6 i=7= k
i = k = 7 termina anche il ciclo esterno v[k]>conf falso : k non varia
Quicksort
- Complessità. Contiamo i confronti tra conf e gli elementi v[i]: 0 se n = 0,1 (n1<n2) T(n) = D(n) + C(n) + T(k-1) + T(n-k) D(n) è la complessità dell’algoritmo partizione C(n) = 0 “guardare” le due parti dell’array D(n) = O(n): n-1 confronti nei predicati dei cicli interni + 1 confronto per sistemare conf.
Quicksort
- Caso peggiore : vettore ordinato , la partizione è sbilanciata:
T(n) = n + T(0) + T(n-1) = n + T(n-1) = = n + (n-1 + T(0) + T(n-2)) = n + n-1 + T(n-2) = = n + (n-1) + (n-2) + T(n-3) = = ….. = n + (n-1) + + 1 + T(0) + T(n – (n-1)) = = n (n-1)/ O(n^2 /2)
Mergesort
algoritmo mergesort(v, p, q) se p<q allora medio ← (p+q)/2 //troncata chiamare mergesort(v, p, medio) chiamare mergesort(v, medio+1, q) chiamare merge(v, p, medio, q) finese
finemergesort
Mergesort
- Per gestire la fusione pensiamo all’array diviso in due parti, da p a medio , e da medio+1 a q ; inoltre usiamo un array di supporto s per “appoggiare” le componenti di v in ordine.
- Progetto dell’algoritmo di fusione (merge) algoritmo merge(v,p,medio,q) h ← p i ← p k ← medio+
Mergesort
mentre h ≤ medio e k ≤ q eseguire se v[h] ≤ v[k] allora s[i] ← v[h] h ← h+ altrimenti s[i] ← v[k] k ← k+ finese i ← i+ finementre //ricopiare la parte di array non esaminata
Mergesort
se h = medio+ allora copiare in s la seconda parte altrimenti copiare in s la prima parte finese ricopiare s sul v //fine merge
- Esercizio. Implementare gli algoritmi quicksort e mergesort ed eseguire le prove dei tempi o contare le chiamate ricorsive.
Mergesort
- Complessità. Contiamo i confronti tra gli elementi dell’array: 0 se n = 0, T(n) = D(n) + C(n) + T(n/2) + T(n/2) D(n) = 0 calcolo di medio C(n) è la complessità dell’algoritmo di fusione C(n) = O(n): vengono considerati tutti gli elementi delle due parti lunghe n/
Mergesort
- Il numero di confronti è sempre lo stesso perché anche se l’array è ordinato si esegue sempre la divisione a metà e la fusione delle due parti; le partizioni sono bilanciate:
T(n) = n + T(n/2) + T(n/2) = n + 2T(n/2) = = n + 2(n/2 + 2T(n/4)) = 2n + 2^2 T(n/2^2 ) = = ….. = k·n + 2k·T(n/2k)
se n = 2k^ allora k = log 2 n O(n ···· log 2 n)
Mergesort e Quicksort
- Confrontiamo i due algoritmi.
- L’algoritmo mergesort ha la complessità più bassa nel caso peggiore O(nlog 2 n); esegue però molte ricopiature per eseguire la fusione.
- L’algoritmo quicksort ha caso peggiore O(n^2 /2), ma nel caso favorevole e medio è O(nlog 2 n).
- Alcuni linguaggi (Java) implementano un algoritmo sort per il problema dell’ordinamento utilizzando: l’ordinamento per inserimento per n<7, e quicksort negli altri casi.
Mergesort
- Albero delle chiamate ricorsive. v0 v1 v2 v3 v4 v5 v6 v 1 0 8 5 4 3 -1 9
1 0 8 5 4 3 -1 9
1 0 8 5 4 3 -1 9
1 0 8 5 4 3 -1 9
0 1 (^) 5 8
0 1 5 8
Altri algoritmi per Fibonacci
- Esempio. Vogliamo calcolare 4^8.
48 = 4 · 4 · …. · 4 8 42 = 16 162 = 4^4 = 256 2562 = 4^8 quindi in 3= log 28 passi abbiamo eseguito il calcolo.
Mn^ = ( M n/2)^2 con n pari
Altri algoritmi per Fibonacci
poiché la divisione è troncata, se n è dispari (n/2) · 2 è uguale a ((n-1)/2) · 2 = (n-1) occorre perciò un’altra moltiplicazione per M.
intestazione funzione fibonacci5 (n intero) M ← I chiama potenzamatrice(M, n-1) restituire M[0][0] //finealgoritmo
Altri algoritmi per Fibonacci
intestazione algoritmo potenzamatrice(matrice M, intero n) se n> allora potenzamatrice(M, n/2) M ← M * M //finese se n è dispari allora M ← M * A //finese //finealgoritmo
- La complessità di tempo è O(log 2 n)
- La complessità di spazio è O(1).
Puntatori o
riferimenti
Puntatori o riferimenti
- Un puntatore è una variabile che contiene
l’indirizzo di un’area di memoria.
- Quando si definisce una variabile, questa viene
allocata in memoria e viene individuata dal suo nome. È possibile anche accedere alla variabile tramite il suo indirizzo.
- Per definire in C++ una variabile puntatore si
utilizza il simbolo ***** e si indica il tipo di dato dell’area di memoria.
Puntatori o riferimenti
- Sintassi. *nometipo nomepuntatore;
- Esempio. int i; i=5; *int p;
5
i
p
Puntatori o riferimenti
*int p; si dice che p è un puntatore ad un’area di memoria di tipo int. Pertanto p può contenere l’indirizzo di memoria di una variabile di tipo int.
- Per ottenere l’indirizzo di memoria di una variabile già allocata, si utilizza l’operatore & operatore “indirizzo_di”
Puntatori o riferimenti
- Per collegare il puntatore p all’area allocata per la variabile i eseguiamo l’assegnazione:
p=&i;
- In tale modo si può accedere ad i anche tramite p : *p è il contenuto dell’area “vista” da p.
5
p (^) i