Il problema del dizionario Oggetto key chiave … … … … dati “satellite” x = puntatore all’oggetto Dizionario = insieme dinamico di oggetti che consente di effettuare le operazioni di ricerca, inserimento, cancellazione. Search(S,k) – dato un insieme S ed un valore chiave k restituisce un puntatore x ad un elemento in S tale che key[x] = k Insert(S,x) – inserisce in S l’elemento puntato da x Delete(S,k) - rimuove da S l’oggetto con chiave k Ci chiediamo qual è la migliore realizzazione possibile per un dizionario. Esempio Dizionario per la gestione del corso di algoritmi Nome (chiave) Bravino Lasfanga Svogliatelli Secchioni Modesto … Voto Parziale (dati satellite) 23 21 15 30 9 Metodo più semplice array non ordinato In questo caso: Insert Search Delete costa O(1) - inserisco dopo Modesto costa O(n) – devo scorrere la tabella costa O(n) - delete = search + trasferimento Possiamo ridurre il tempo di esecuzione della operazione di ricerca? Il problema della ricerca di un elemento in un array non ordinato ha delimitazione inferiore (n) INFATTI Ogni algoritmo deve, nel caso peggiore, guardare tutti gli elementi dell’insieme per accertarsi o meno della presenza dell’elemento cercato. L’operazione di ricerca costa meno in un array ordinato. Posso usare il metodo di ricerca per dimezzamenti successivi La ricerca per dimezzamenti successivi ha un costo O(log(n)). Posso fare meglio? NO! Ogni algoritmo per la ricerca di un elemento in un insieme di n elementi richiede (log(n)) confronti. Infatti: - Ogni algoritmo deve restituire la posizione dell’elemento tra le n possibili; - Ad ogni algoritmo posso associare un albero di decisione; - L’albero di decisione deve avere n foglie; - L’altezza dell’albero fornisce un lower bound alla complessità di un generico algoritmo; - Il numero di foglie di un albero di altezza h è al più 2h Quindi: n < 2h h > log2(n) Per un generico algoritmo T(n) = ( log2(n)) Il metodo di ricerca per dimezzamenti successivi è ottimale. Gestione del dizionario tramite array ordinato Search – O(log(n)) Insert – O(n) Ho bisogno di: O(log(n)) confronti per trovare la giusta posizione in cui inserire l’elemento O(n) trasferimenti per mantenere l’array ordinato (Ricorda che O(n) + O(log(n)) = O(n)) Delete - O(n) (come per Insert) E con le LISTE? Lista non Ordinata Search – O(n) Insert – O(1) Delete - O(n) Lista Ordinata Search – O(n) Costerebbe comunque n (non posso fare dim. successivi) Insert – O(n) Devo mantenere ordinata la lista Delete - O(n) Ognuno dei 4 metodi banali costa O(n). Voglio fare meglio… Alberi binari di ricerca (ABR) key chiave p left right Nodo x = puntatore al nodo Albero binario insieme di nodi (record) caratterizzato da quattro campi: key[x] (chiave), p[x] (puntatore al padre), left[x] (puntatore al figlio sinistro), right[x] (puntatore al figlio destro). root[T] Puntatore alla radice dell’albero x = puntatore nodo left[x] = NIL (il nodo non ha figlio sinistro) right[x] = NIL (il nodo non ha figlio destro) Un albero binario di ricerca (ABR) è una albero binario in cui le chiavi dei vari nodi soddisfano la seguente proprietà: PROPRIETA’ dell’ABR Sia x un nodo generico di un ABR. Se y è un nodo del sotto-albero sinistro di x allora: key[y] < key[x] Se y è un nodo del sotto-albero destro di x allora: key[y] > key[x] Alberi binari di ricerca Ordinamento decrescente 15 Ordinamento crescente 6 3 18 7 17 20 massimo 2 13 4 minimo 9 Visita di un ABR Visita in ordine simmetrico – dato un nodo x, elenco prima il sottoalbero sinistro di x, poi il nodo x, poi il sotto-albero destro. Inorder-tree-walk(x) If (x NIL) then Inorder-tree-walk(left[x]) stampa key[x] Inorder-tree-walk(right[x]) Proprietà: La visita in ordine simmetrico di un ABR stampa i nodi dell’ABR in ordine crescente rispetto alla chiave! Verifica di correttezza – Supponiamo,per semplicità, che l’albero sia completo. Indichiamo con h l’altezza dell’albero. Vogliamo mostrare che Inorder-tree-walk(x) restituisce la sequenza ordinata Per induzione sull’altezza dell’ABR: h=1 root[T] Left[root(T)] NIL NIL right[root(T)] NIL NIL key[left[root[T]]] < key[root[T]] < key[right[root[T]]] Verifica correttezza (continua …) h = generico (ipotizzo che la procedura sia corretta per h-1) root[T] Left[root(T)] Albero di altezza h-1. Tutti i suoi elementi sono minori o uguali della radice right[root(T)] Albero di altezza h-1. Tutti i suoi elementi sono maggiori o uguali della radice Analisi complessità La complessità della procedura considerata è T(n) = (n). Matematicamente …… T(n) = T(n’) + T(n’’) + c con n’+n’’=n-1 Intuitivamente …… La procedura è chiamata un numero di volte pari al numero di nodi dell’albero (e ad ogni chiamata effettua un numero costante di operazioni). Esempio 15 6 3 18 7 17 20 massimo 2 13 4 minimo 9