Alberi binari di ricerca Gli alberi binari di ricerca sono ottime strutture dati per memorizzare coppie di elementi (k, e) chiave – elemento di un dizionario. Un albero binario di ricerca T è un albero binario in cui in ogni nodo, ad esclusione di quelli esterni, sono memorizzate delle coppie (k, e) chiave – elemento. Sia v uno di questi nodi che memorizza la chiave k. Allora deve essere che: 1) le chiavi memorizzate nei nodi del sottoalbero sinistro del nodo v sono minori o uguali della chiave k contenuta nel nodo stesso 2) le chiavi memorizzate nei nodi del sottoalbero destro del nodo v sono maggiori o uguali della chiave k contenuta nel nodo stesso Si stabilisce d’ora in avanti che i nodi esterni di un albero, cioè le sue foglie siano rappresentati da nodi nulli che non contengono niente. Algoritmo di ricerca in un albero binario Per effettuare la ricerca in un dizionario D rappresentato attraverso l’utilizzo di un albero binario, dobbiamo immaginare l’albero come un albero di decisione come quello riportato in figura: In questo caso la questione è: su ogni nodo interno visitato la chiave contenuta nell’elemento (k,e) del nodo è minore o uguale o maggiore di quella cercata? Se la risposta è la prima (minore) allora la ricerca continua nel sottoalbero di sinistra. Se la risposta è la seconda (uguale) allora la ricerca ha avuto esito positivo e si ferma. Se la risposta è la terza (maggiore) allora la ricerca continua nel sottoalbero di destra. Il frammento di codice sotto riportato segue questo procedimento. La ricerca è fatta su un albero binario T a partire da un nodo v dell’albero. La ricerca avviene in base ad una chiave k da confrontare. Nel caso la procedura arrivi a raggiungere una foglia esterna (null) significa che la ricerca ha avuto esito negativo. In ogni caso l’algoritmo ritorna il riferimento o posizione dell’ultimo nodo raggiunto: Algorithm TreeSerach(k, v) Se v è un nodo esterno allora ritorna v //v è un nodo foglia cioè nullo Se k = k(v) allora ritorna v //ricerca terminata con successo altrimenti Se k < k(v) allora ritorna TreeSerach(k, T.leftChild(v)); //chiamata ricorsiva altrimenti // k > k(v) ritorna TreeSerach(k, T.rightChild(v)); //chiamata ricorsiva La complessità di questo algoritmo è O(h) con h altezza dell’albero. Operazioni di modifica dell’albero binario di ricerca Inserimento di un nuovo elemento Per eseguire l’inserimento di un nuovo elemento in un albero binario di ricerca bisogna sempre partire facendo eseguire l’algoritmo di ricerca sull’albero in questione. Si possono presentare due casi: 1) l’algoritmo di ricerca non ha trovato l’elemento nell’albero e quindi ha restituito un riferimento ad un nodo nullo. A questo punto non bisogna fare altro che sostituire l’elemento nullo con il nuovo nodo da inserire, aggiungendo anche i due figli nulli del nodo stesso (expandExternal) 2) L’algoritmo di ricerca ha trovato il nodo cercato v. Bisogna ripetere l’algoritmo di ricerca sul sottoalbero che ha come radice uno dei due figli del nodo trovato fino a trovare un nodo esterno. A questo punto è sufficiente inserire il nuovo nodo esterno con i suoi due figli nulli (expandExternal) La figura seguente mostra l’effetto dell’inserimento di un nuovo elemento su di un albero di ricerca: Rimozione di un elemento esistente La procedura di rimozione da un albero binario di ricerca è un po’ più complessa di quella di inserimento. Come al solito si inizia sempre con l’applicare l’algoritmo di ricerca per recuperare il riferimento al nodo che conserva la chiave il cui elemento vogliamo eliminare. Si presentano i seguenti casi: 1) l’algoritmo di ricerca non ha trovato l’elemento e quindi ha ritornato un riferimento ad una foglia nulla. In questo caso non c’è da fare nulla; 2) l’algoritmo ha ritrovato il nodo w e ritorna un riferimento a tale nodo che è per forza un nodo interno. Allora bisogna considerare i seguenti casi: a) se uno dei due figli z del nodo w è esterno allora è sufficiente eliminare tale nodo esterno z e il nodo w, sostituendo w con il fratello (sibling) di z. (Questa procedura prende il nome di removeAboutExternal). b) se entrambe i figli di w sono interni allora bisogna seguire questo procedimento: • recuperare il primo nodo interno y che segue w applicando un algoritmo di attraversamento Inordine. Cioè partendo dal nodo w bisogna andare prima a destra e poi sempre a sinistra fino ad incontrare un nodo foglia x nullo. Il genitore di x sarà il nodo y cercato. • Sostituire l’elemento (k,e) del nodo da eliminare w con quello di y appena trovato. Il valore del nodo w da eliminare va temporaneamente custodito in una variabile (solo l’elemento e non la chiave) • Eliminare il nodo y (removeAboutExternal) • Restituire il valore della variabile che conserva l’elemento di w eliminato. e suo figlio x come al punto a)