Lezione 8 Ricorsione Code
Sommario Paradigma di programmazione Divide et Impera Ricorsione e tempo di calcolo Pile e code un ADT Pila
Ricorsione Molti algoritmi hanno una struttura ricorsiva l’algoritmo è definito in termini di se stesso un algoritmo è ricorsivo quando contiene una o più istruzioni di chiamata a se stesso le chiamate ricorsive non possono succedersi in modo infinito altrimenti l’algoritmo non terminerebbe mai deve sempre esistere una condizione di terminazione che determina quando l’algoritmo smette di richiamarsi in questo caso la computazione prosegue eseguendo un insieme di istruzioni dette passo base
Divide et Impera Paradigma di programmazione Divide et Impera Si sfrutta la soluzione ricorsiva di un problema Il procedimento è caratterizzato dai seguenti passi: Divide: suddivisione del problema in sottoprolemi Impera: soluzione ricorsiva dei sottoproblemi. Problemi piccoli sono risolti direttamente. Combina: le soluzioni dei sottoproblemi sono ricoombinate per ottenere la soluzione del problema originale
Esempio di algoritmo Divide et Impera: merge sort L’algoritmo merge sort ordina un vettore di elementi nel modo seguente: Divide: ogni sequenza di n elementi da ordinare è suddivisa in 2 sottosequenze di n/2 elementi Impera: ordina (sort) ricorsivamente le sottosequenze Combina: fonde (merge) le due sottosequenze per produrre la sequenza ordinata La condizione di terminazione si ha quando si deve ordinare una sequenza di lunghezza 1 (in quanto già ordinata)
Spiegazione intuitiva L’operazione chiave è la procedura di fusione L’idea è di fondere fra di loro due sottosequenze già ordinate (in ordine decrescente ad esempio) Si confrontano in ordine gli elementi delle due sottosequenze Si trascrive l’elemento più piccolo Si reitera il procedimento a partire dall’elemento successivo a quello già considerato
Visualizzazione del concetto di fusione Sequenze: A: 2 4 6 8 B: 1 3 5 7 R: Step di fusione: 1) B: _ 3 5 7 R: 1 2) A: _ 4 6 8 R: 1 2 3) A: _ 4 6 8 B: _ _ 5 7 R: 1 2 3 4) A: _ _ 6 8 R: 1 2 3 4 5) B: _ _ _ 7 R: 1 2 3 4 5 6) A: _ _ _ 8 B: _ _ _ 7 R: 1 2 3 4 5 6 7) B: _ _ _ _ R: 1 2 3 4 5 6 7 8) A: _ _ _ _ R: 1 2 3 4 5 6 7 8
Visualizzazione del concetto di fusione ricorsiva 1 2 2 3 4 5 6 6 2 4 5 6 1 2 3 6 2 5 4 6 1 3 2 6 5 2 4 6 1 3 2 6 Sequenza: 5 2 4 6 1 3 2 6
Pseudo Codice MERGE-SORT(A,p,r) 1 if p<r 2 then q(p+r)/2 3 MergeSort(A,p,q) 4 MergeSort(A,q+1,r) 5 Merge(A,p,q,r)
Pseudo Codice MERGE(A,p,q,r) 1 aux A 2 i p 3 j q+1 4 for k p to r 5 do if i=q+1 6 then A[k] aux[j++] 7 else if j=r+1 8 then A[k] aux[i++] 9 else if aux[i]<aux[j] 10 then A[k] aux[i++] 11 else A[k] aux[j++]
Il merge sort in C++ (l’algoritmo) typedef int Item; // tipo di ogni elemento da ordinare void merge(Item A[], int p, int q, int r) { const int MaxItems=1000; static Item aux[MaxItems]; int i,j; for (i = q+1; i > p; i--) aux[i-1] = A[i-1]; for (j = q; j < r; j++) aux[r+q-j] = A[j+1]; for (int k = p; k <= r; k++) if (aux[j] < aux[i]) A[k] = aux[j--]; else A[k] = aux[i++]; }
Nota: Si è copiata la seconda sottosequenza in ordine decrescente in modo da poter gestire il caso in cui la sequenza originale è formata da un numero dispari di elementi e quindi una sottosequenza è più piccola dell’altra in questo modo non si devono effettuare controlli sugli indici infatti quando l’indice i si riferisce ad un elemento della seconda sottosequenza (B) questa risulterà sicuramente degli elementi di B ancora da elaborare
Il merge sort in C++ void mergesort(Item A[], int p, int r) { if (p<r) { //passo base: quando p==r int q = (r+p)/2; mergesort(A, p, q); mergesort(A, q+1, r); merge(A, p, q, r); }
Il merge sort in C++ int main() { const int N=100; Item A[N]; for (int i=0; i<N; ++i) A[i] = rand()%1000; mergesort(A,0,N-1); for (int i=0; i<N; ++i) { cout << A[i]; if ((i+1)%10==0) cout << endl; else cout << " "; } return 0;
Il merge sort in C++ (l’algoritmo con template) template <class Item> void merge(Item A[], int p, int q, int r){ const int MaxItems=1000; static Item aux[MaxItems]; int i,j; for (i = q+1; i > p; i--) aux[i-1] = A[i-1]; for (j = q; j < r; j++) aux[r+q-j] = A[j+1]; for (int k = p; k <= r; k++) if (aux[j] < aux[i]) A[k] = aux[j--]; else A[k] = aux[i++]; }
Il merge sort in C++ (l’algoritmo con template) template <class Item> void mergesort(Item A[], int p, int r){ if (p<r) { int q = (r+p)/2; mergesort(A, p, q); mergesort(A, q+1, r); merge(A, p, q, r); }
Il merge sort in C++ (l’algoritmo con template) int main(){ const int N=100; int A[N]; for (int i=0; i<N; ++i) A[i] = rand()%1000; mergesort(A,0,N-1); for (int i=0; i<N; ++i) { cout << A[i]; if ((i+1)%10==0)cout << endl; else cout << " "; } double B[N]; for (int i=0; i<N; ++i)B[i] = double(rand()%1000)/1000.0; mergesort(B,0,N-1); cout << B[i]; return 0;
Il merge sort in C++ (l’algoritmo con confronto generico) #include<iostream> #include<stdlib.h> #include <function.h> #include "Student.h" template <class Item, class Compare> void merge(Item A[], int p, int q, int r){ const int MaxItems=1000; static Item aux[MaxItems]; int i,j; Compare key_compare; // oggetto usato per il confronto for (i = q+1; i > p; i--) aux[i-1] = A[i-1]; for (j = q; j < r; j++) aux[r+q-j] = A[j+1]; for (int k = p; k <= r; k++) if (key_compare(aux[j],aux[i])) A[k] = aux[j--]; else A[k] = aux[i++]; }
Nota In <function.h> sono incluse i seguenti oggetti funzioni: template <class _Tp> struct less : public binary_function<_Tp,_Tp,bool> { bool operator()(const _Tp& __x, const _Tp& __y) const { return __x < __y; } }; // oltre a questa esistono anche (con l'ovvio significato) less_equal,greater, greater_equal, equal_to, not_equal_to
Il merge sort in C++ (l’algoritmo con confronto generico) template <class Item, class Compare> void mergesort(Item A[], int p, int r){ if (p<r) { int q = (r+p)/2; mergesort<Item,Compare>(A, p, q); mergesort<Item,Compare>(A, q+1, r); merge<Item,Compare>(A, p, q, r); }
Il merge sort in C++ (l’algoritmo con confronto generico) class BigStudent: public Student{ private: string birthplace; string birthdate; string address1; string address2; string email; // ... etc etc... public: BigStudent(int _id=0, string _name=""): Student(_id,_name) {} }; typedef BigStudent* BigStudentPtr; struct bigstudent_less{ bool operator()(const BigStudentPtr & x, const BigStudentPtr & y) const { return x->getid() < y->getid(); } 124: // Ok adesso una breve dimostrazione 125: int main() 126: { 127: const int N=5; 128: // Array di 5 studenti 129: Student A[N]; 131: // esempio di valori... 132: A[0] = Student(13, "Paolo"); 133: A[1] = Student(32, "Luca"); 134: A[2] = Student(15, "Giovanni"); 135: A[3] = Student(52, "Mario"); 136: A[4] = Student(34, "Gianni"); 138: // istanziazione del mergesort per Student con ordinamento decrescente 139: mergesort<Student, greater<Student> >(A,0,N-1); 140: for (int i=0; i<N; ++i) { 141: cout << A[i]; 142: if ((i+1)%10==0) 143: cout << endl; 144: else 145: cout << " "; 146: } 147: cout << endl; 150: // Oggetti grandi: conviene un array di puntatori!! 151: BigStudentPtr Ap[N]; 153: // esempio di valori... 154: Ap[0] = new BigStudent(13, "Paolo"); 155: Ap[1] = new BigStudent(32, "Luca"); 156: Ap[2] = new BigStudent(15, "Giovanni"); 157: Ap[3] = new BigStudent(52, "Mario"); 158: Ap[4] = new BigStudent(34, "Gianni"); 160: // istanziazione del mergesort per BigStudentPtr con ordinamento decrescente 161: mergesort<BigStudentPtr, bigstudent_less>(Ap,0,N-1); 162: for (int i=0; i<N; ++i) { 163: cout << *(Ap[i]); 164: if ((i+1)%10==0) 165: cout << endl; 166: else 167: cout << " "; 168: } 169: cout << endl; 170: 171: return 0; 172: }
Il merge sort in C++ (l’algoritmo con confronto generico) int main(){ const int N=5; Student A[N]; // Array di 5 studenti // esempio di valori... A[0] = Student(13, "Paolo"); A[1] = Student(32, "Luca"); A[2] = Student(15, "Giovanni"); A[3] = Student(52, "Mario"); A[4] = Student(34, "Gianni"); mergesort<Student, less<Student> >(A,0,N-1); for (int i=0; i<N; ++i) { cout << A[i]; if ((i+1)%10==0)cout << endl; else cout << " "; } cout << endl;
Il merge sort in C++ (l’algoritmo con confronto generico) // Oggetti grandi: conviene un array di puntatori!! BigStudentPtr Ap[N]; Ap[0] = new BigStudent(13, "Paolo"); Ap[1] = new BigStudent(32, "Luca"); Ap[2] = new BigStudent(15, "Giovanni"); Ap[3] = new BigStudent(52, "Mario"); Ap[4] = new BigStudent(34, "Gianni"); mergesort<BigStudentPtr, bigstudent_less>(Ap,0,N-1); for (int i=0; i<N; ++i) { cout << *(Ap[i]); if ((i+1)%10==0)cout << endl; else cout << " "; } cout << endl; return 0;
Il tempo di calcolo del merge sort Per semplificare l’analisi: numero di elementi da ordinare sia una potenza di 2 così per ogni applicazione del merg sort si lavora con sotto sequenze di lunghezza identica Tempo per elaborare sequenze di 1 elemento (1) quando si hanno n>1 elementi Divide: calcolo dell’indice mediano della sequenza (1) Impera: si risolvono ricorsivamente due sotto problemi di dimensione n/2, quindi 2T(n/2) Combina: il tempo di calcolo per la procedura Merge fra due sotto sequenze di n/2 elementi è (n)
Il tempo di calcolo del merge sort Si ha pertanto: T(n)= (1)+2T(n/2)+(n) = 2T(n/2)+(n) O più precisamente: T(n)=(1) per n=1 T(n)= 2T(n/2)+(n) Vedremo che T(n)= (n lg n)
Il tempo di calcolo di un algoritmo ricorsivo Per risolvere il calcolo di ricorrenze del tipo T(n)=aT(n/b)+f(n) si usa il Teorema Principale (non lo dimostreremo) Le ricorrenze del tipo indicato descrivono i tempi di calcolo di algoritmi che dividono un problema di dimensione n in a sottoproblemi di dimensione n/b risolvono i sotto problemi in tempo T(n/b) determinano come dividere e come ricomporre i sottoproblemi in un tempo f(n)
Il Teorema principale Sia T(n)=aT(n/b)+f(n) con a,b>1 e f(n) asintoticamente positiva allora T(n) può essere asintoticamente limitato se f(n)=O(nlogba-e) per qualche costante e allora T(n)=(nlogba) se f(n)=(nlogba ) allora T(n)=(nlogba lg n) se f(n)=(nlogba+e) per qualche costante e allora se af(n/b)<cf(n) per qualche c<1 e per n>n0 allora T(n)=(f(n))
Significato intuitivo Si confronta f(n) con nlogba. La soluzione è determinata dalla più grande fra le due funzioni se prevale nlogba allora la soluzione è (nlogba) cioè il tempo è dominato dalla soluzione ricorsiva dei sottoproblemi se prevale f(n) allora la soluzione è (f(n)) cioè il tempo è dominato dalle procedure di divisione e ricombinazione delle soluzioni parziali se hanno lo stesso ordine di grandezza allora la soluzione è (f(n)ln n)
Nota Cosa significa che una funzione prevale sull’altra? Nel primo caso non solo si deve avere f(n)< nlogba ma deve esserlo in modo polinomiale, cioè deve esserlo per un fattore ne per un qualche e Nel terzo caso f(n) deve essere polinomialmente più grande di nlogba e inoltre devono valere le condizioni di regolarità af(n/b)<cf(n) ATTENZIONE. Se f(n) è più piccola (o più grande) di nlogba ma non in modo polinomiale allora non si può utilizzare il teorema per risolvere la ricorrenza
Applicazione del Teorema Principale T(n)=2T(n/2)+n f(n)=n, a=2, b=2 nlogba = nlog22 =n pertanto, dato che f(n)=(nlogba )=(n) allora (caso 2) T(n)=(n lg n) T(n)=9T(n/3)+n f(n)=n, a=9, b=3 nlogba = nlog39 =n2 pertanto, dato che f(n)=O(nlogba-1)=O(n1) allora (caso 1) T(n)= (n2)
Applicazione del Teorema Principale T(n)=T(2n/3)+1 f(n)=1, a=1, b=3/2 nlogba = nlogb1 = n0 =(1) pertanto, dato che f(n)=(nlogba )=(1) allora (caso 2) T(n)= (lg n) T(n)=3T(n/4)+n lg n f(n)=n lg n, a=3, b=4 nlogba = nlog43 =n0.79.. pertanto, dato che f(n)=(nlogba+e)= (n1) allora se f(n) è regolare siamo nel caso 3 af(n/b)<cf(n) ovvero 3 n/4 lg n/4< c n lg n, ok se c=3/4 T(n)= (n lg n)
Applicazione del Teorema Principale T(n)=2T(n/2)+n lg n f(n)=n lg n, a=2, b=2 nlogba = nlog22 =n pertanto, dato che f(n) è più grande di n ovvero f(n)=(nlogba+e)= (n1) allora se f(n) è polinomialmente minore allora potremo essere nel caso 3 deve essere f(n) / nlogba > ne per qualche e ma f(n)/ nlogba =(n lg n)/n=lg n lg n è sempre minore di ne per qualsiasi e pertanto non possiamo usare il Teorema
Pile e code Pile e Code sono insiemi dinamici sono definite le operazioni di inserzione e cancellazione l’operazione di cancellazione elimina un elemento predefinito pile e code si distinguono per le politiche di eliminazione
Pila o Stack Il termine Pila (Stack) ed il nome delle operazioni di inserzione (push) e cancellazione (pop) derivano da quelli usati per descrivere le operazioni di caricamento per le pile di piatti a molla. Solo il piatto in cima può essere rimosso. Nuovi piatti vengono posti in cima. La politica di cancellazione è del tipo ultimo arrivato primo servito o LIFO (Last In First Out)
Implementazione di una Pila Una Pila si può realizzare con un vettore di n elementi S Un attributo top[S] indica la posizione dell’elemento inserito più di recente che è anche quello da cancellare quando top[S] è 0 si dice che la pila è vuota
Pseudo codice per le operazioni su Pila Stack-Empty(S) 1 if top[S]=0 2 then return TRUE 3 else return FALSE PUSH(S,x) 1 top[S] top[S]+1 2 top[S] x POP(S) 1 if not Stack-Empty(S) 2 then top[S] top[S]-1 3 return S[top[S]+1]
Codice C++ #include <iostream> #include <cassert> using namespace std; template<class T> class ADT_Stack{ public: virtual void push(T)=0; virtual T pop()=0; };
Codice C++ template<class T> class Array_Stack: public ADT_Stack<T>{ public: Array_Stack(int usr_size=10):size(usr_size),top(0){data=new T[size];} ~Array_Stack(){delete[] data;} void push(T); T pop(); private: T * data; int size; int top; };
Codice C++ template<class T> void Array_Stack<T>::push(T item){ assert(top<size); data[top]=item; top++; } template<class T> T Array_Stack<T>::pop(){ top--; assert(top>=0); return data[top];
Codice C++ int main(){ const int DIM=10; Array_Stack<int> array_stack(DIM); int v[]={1,2,3,4,5,6,7,8,9,10,11}; ADT_Stack<int> *stack=&array_stack; for(int i=0;i<DIM;i++) stack.push(v[i]); cout<<stack.pop()<<" "; cout<<endl; return 0; }
Coda La politica di cancellazione per una coda è di tipo FIFO (First In First Out), primo arrivato primo servito, cioè il primo elemento ad essere stato inserito è anche il primo elemento ad essere eliminato La operazione di inserzione viene chiamata Enqueue e l’operazione di eliminazione Dequeue Quando un nuovo elemento viene inserito prende posto in fondo alla coda
Implementazione di una coda E’ possibile realizzare una coda con un vettore Q La coda ha attributi head[Q] e tail[Q] che rappresentano gli indici degli elementi in testa e in coda alla struttura L’attributo tail[Q] indica la posizione in cui sarà inserito il prossimo elemento Gli elementi della coda sono alle locazioni head[Q], head[Q]+1,…,tail[Q]-1 La locazione 1 segue la locazione n (ordine circolare) quando head[Q]=tail[Q] la coda è vuota quando head[Q]=tail[Q]+1 la coda è piena
Pseudocodice coda ENQUEUE(Q,x) 1 Q[tail[Q]] x 2 if tail[Q]=length[Q] 3 then tail[Q] 1 4 else tail[Q] tail[Q]+1 DEQUEUE(x) 1 x Q[head[Q]] 2 if head[Q]=length[Q] 3 then head[Q] 1 4 else head[Q] head[Q]+1 5 return x
Visualizzazione operazioni su coda 1 2 3 4 5 6 7 8 9 10 5 4 3 8 head[Q]=6 tail[Q]=10 1 2 3 4 5 6 7 8 9 10 4 6 5 4 3 8 3 tail[Q]=3 Inserzione degli elementi 3,4,6 head[Q]=6 1 2 3 4 5 6 7 8 9 10 4 6 3 8 3 Cancellazione di 2 elementi tail[Q]=3 head[Q]=8