! ! Alberi radicati Algoritmi e Strutture Dati ✦ Albero: definizione informale ✦ ! ✦ Capitolo 5 - Alberi Albero: definizione ricorsiva ✦ ! ! ! Alberto Montresor E' un insieme dinamico i cui elementi hanno relazioni di tipo gerarchico ✦ Insieme vuoto di nodi, oppure Una radice T e 0 o più sottoalberi, con la radice di ogni sottoalbero collegata a T da un arco (orientato) T Università di Trento ! ! ! es.: radice T con n sottoalberi This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/2.5/ or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA. 1 © Alberto Montresor Alberi ordinati T Nodi interni = Nodi - Foglie Radice (root) Padre (parent) dei nodi j e k Figlio di T Radice del proprio sottoalbero Tn 2 © Alberto Montresor ✦ j In un albero ✦ Sottoalbero ✦ a ✦ Profondità di un nodo: la lunghezza del percorso dalla radice al nodo (i.e., numero archi attraversati) Livello: l'insieme dei nodi alla stessa profondità Altezza dell'albero: massimo livello delle sue foglie k ... © Alberto Montresor T2 Alberi: definizioni Figlio (child) di T Foglie (leaf) T1 p=0 p=1 p=2 p=3 Livello 3 Altezza albero: 3 Nodi fratelli (figli di a) 3 © Alberto Montresor 4 Coniferali, Gnetali, Angiosperme, Dicotiledoni, Monocotiledoni; possibile Postvisita: Alghe, cheAlberi: permettonouna diCianoficee, leggere e scrivereBatteri, ilspecifica contenuto dei nodi.Mixomiceti, Funghi, Tallofite, Briofite, Pteridofite, Archegoniate, Crittogame, Cicadali, Ginkgoali, Coniferali, Gnetali, Gimnosperme, Dicotiledoni, T REE % Costruisce un nuovo albero, costituito daFanerogame, un solo nodo e contenente v vegetale; Monocotiledoni, Angiosperme, Regno Alberi? Tree(I TEM v) Invisita = 1): Cianoficee, Tallofite, Batteri, Alghe, Mixomiceti, Funghi, Crittogame, Briofi% Legge(i il valore I TEM read () te, Archegoniate, Pteridofite, Regno vegetale, Cicadali, Gimnosperme, Ginkgoali, Coniferali, % ScriveFanerogame, v nel nodo Gnetali, Dicotiledoni, Angiosperme, Monocotiledoni; write(I TEM v) DAG Per% livelli: vegetale, Crittogame, Fanerogame, Taliofite, Archegoniate, Gimnosperme, RestituisceRegno il padre; nil se questo nodo è radice T REE parent() Cianoficee, Batteri, Alghe, Mixomiceti, Funghi, Briofite, Pteridofite, Cicadali, Angiosperme, % Restituisce il primo figlio; nil se questo nodo è foglia Ginkgoali, Coniferali, Gnetali, Dicotiledoni, Monocotiledoni. T REE leftmostChild() Radice %In Restituisce il prossimo fratello nodo anticipato a cui è applicato; nil se assente pratica, l’ordine di del visita coincide con T REE rightSibling() Foresta © Alberto Montresor 5 Algoritmi di visita degli alberi ✦ ✦ ✦ leftmostChild() restituisce nil, il nodo è foglia (ovviamente, un nodo può essere radice e foglia contemporaneamente). Se rightSibling() restituisce nil, è stata ispezionata l’intera lista di figli. visitaProfondità(T REE t) Visita (o attraversamento) di un albero: ✦ La creazione di nuovi alberi sfrutta la ricorsività della definizione di questa struttura di dati; oltre al caso base, rappresentato dalla costruzione di alberi costituiti da singoli nodi isolati precondition: t ⇤= nil (descritta in precedenza), è possibile “aggiungere” un sottoalbero come figlio (operazione insertChild ()) o come fratello successivo insertSibling (1) esame “anticipato” del (operazione nodo radice di t ()) di un particolare nodo. La precondizione per applicare questi metodi con successo è che l’albero da aggiungere non sia già sottoalbero un altro albero (parent() =()nil). Infine, sono presenti le operazioni deleteT REE udi ⇥ t.leftmostChild Child() e deleteSibling() che distruggono il sottoalbero radicato nel primo figlio e nel fratello while u ⇤= nil do successivo del nodo su cui si applica l’operazione. Gli effetti delle operazioni insertChild(), Algoritmo per “visitare” tutti i nodi di un albero In profondità (depth-first search, a scandaglio): DFS ✦ Vengono visitati i rami, uno dopo l’altro ✦ Tre varianti visitaProfondità(u) u ⇥ u.rightSibling() (2) In ampiezza (breadth-first search, a ventaglio): BFS ✦ quello del diritto di successione al trono %nell’albero genealogico di una dinastia regnante. Per illustrare la differenza tra gli ordini Inserisce il sottoalbero t come primo figlio di questo nodo di visita anticipato e posticipato, c’è un modo pittoresco che non tira in ballo la ricorsione. Si insertChild (T REE t) precondition: t.parent() = nil immagini che l’albero sia una sorta di isola fatta a fiordo: i cerchietti rappresentano fari e le % Inserisce il sottoalbero t come successivo fratello di questo nodo linee che collegano coppie di fari rappresentano alte scogliere. Una nave fa la circumnavigainsertSibling(T REE t) t.parent () = nil dalla radice e costeggiando sempre le scogliere. Il capitano segna sul zioneprecondition: dell’isola, partendo % Distrugge il sottoalbero radicato nel primo figlio questo nodo incontrato nella navigazione, ma solo quando libro di bordo il nome di ciascun farodiche viene deleteChild() lo incontra per la prima volta. È facile verificare che se la circumnavigazione procede in senso % Distrugge il sottoalbero radicato nel prossimo fratello di questo nodo antiorario, allora si ottiene proprio l’ordine di visita anticipato, mentre se procede in senso deleteSibling() orario si ottiene l’ordine di visita posticipato inverso (cioè letto da destra verso sinistra, ovvero6 © Alberto Montresor Sono presenti le operazioni parent(), leftmostChild() e rightSibling(), che restituiscono ridall’ultimo nodo visitato al primo). Il seguente schema di procedura serve per effettuare sia la spettivamente il padre, il primo figlio e il fratello successivo di un nodo. Le ultime due possono visita anticipata che essere utilizzate per scorrere laquella lista deiposticipata. figli. Se () restituisce nil, il nodo(previsita) è radice; se Visita alberi: in profondità inparent ordine anticipato T a b c e d f d e g esame “posticipato” del nodo radice di t A livelli, partendo dalla radice Sequenza: a © Alberto Montresor 7 © Alberto Montresor b c f g 8 dall’ultimo nodo visitato al primo). Il seguente schema di procedura serve per effettuare sia la visita anticipata quella posticipata. Visita alberi: che in profondità in ordine posticipato (postvisita) visitaProfondità(T REE t) esame “anticipato” del nodo radice di t c e d f f g g esame “posticipato” del nodo radice di t Sequenza: c 87 d b e a nella riga (1), ignorando la riga (2), si ottiene la o nella riga (2), ignorando la riga (1), si ottiene complesso; quando invisita() viene eseguito sul © Alberto Montresor icorsivamente sui primi i figli di t, se presenti; ndi invisita viene richiamata sui restanti figli. Visita()alberi: in ampiezza 9 visitaAmpiezza(T REE t) precondition: t ⇥= nil Q UEUE Q Queue() Q.enqueue(t) while not Q.isEmpty() do T REE u Q.dequeue() esame “per livelli” del nodo u u u.leftmostChild() while u ⇥= nil do Q.enqueue(u) u u.rightSibling() T a Sequenza: a o visitaAmpiezza() utilizza un approccio comuna visita ricorsiva (che implicitamente utilizza una coda. All’inizio, si inserisce la radice nella inseriti tutti i suoi figli nella coda, in ordine. È del©livello i-esimo vengono esaminati prima dei Alberto Montresor b e d e 5.3 Realizzazione con puntatori padre/primo-figlio/fratello g f c La visita per livelli illustrata nell’algoritmo visitaAmpiezza() utilizza un approccio comSequenza (i=1): c ricorsiva b (che d implicitamente a f eutilizza g pletamente diverso. Invece di essere basata su una visita una pila), è una procedura† iterativa basata su una coda. All’inizio, si inserisce la radice nella coda. Quando un nodo viene estratto, vengono inseriti tutti i suoi figli nella coda, in ordine. È facile dimostrare per induzione che tutti i nodi del livello i-esimo vengono esaminati prima dei10 © Alberto Montresor nodi del livello (i + 1)-esimo. Nelle tre procedure di visita profondità descritte viene effettuata esattamente una chiaRealizzazione con vettore deiinfigli mata ricorsiva per ciascun nodo, per un totale di n chiamate, dove n è il numero di nodi del/ l’albero. Nella visita in ampiezza, ogni nodo è/inserito in coda esattamente una volta. Pertanto, la complessità delle quattro procedure di visita è (n), purché si fornisca una realizzazione in cui le operazioni utilizzate nelle procedure di visita richiedano tempo O(1). / / / b c T Q UEUE Q Queue() a Q.enqueue(t) b while not Q.isEmpty () do e T REE u Q.dequeue() d f nodogu esamec“per livelli” del u u.leftmostChild() while u ⇥= nil do Q.enqueue(u) u u.rightSibling() T REE u t.leftmostChild() integer k 0 while u ⇥= nil and k < i do k k+1 invisita(u) u u.rightSibling() esame “simmetrico” del nodo t while u ⇥= nil do invisita(u) u u.rightSibling() a b precondition: t ⇥= nil precondition: t ⇥= nil T T REE u ⇥ t.leftmostChild() while u ⇤= nil do visitaProfondità(u) u ⇥ u.rightSibling() (2) visitaAmpiezza(T REE t) invisita(T REE t) precondition: t ⇤= nil (1) nodo t, la stessa procedura viene richiamata ricorsivamente sui primi i figli di t, se presenti; viene effettuata simmetrica di t, e simmetrico quindi invisita(invisita) () viene richiamata sui restanti figli. Visita alberi:lainvisita profondità in ordine d / / / / f g 11 / / / / / / / / / / / / / / / // / / / / La realizzazione più naturale, detta a puntatori padre/primo-figlio/fratello, segue fedelmente la specifica, memorizzando per ogni nodo il valore, il puntatore al padre, il puntatore al primo figlio e il puntatore al fratello successivo. La Fig. 5.6 illustra un albero realizzato in / / /memorizzati /direttamente / / / / / / questo modo. I nodi /sono in /strutture di tipo T REE, che vengono Padre create come nodi isolati contenenti un singolo valore. Le operazioni per spostarsi lungo l’albero (parent(), leftmostChild() e rightSibling Array ()), cosı̀ Rischio di sprecare memoria se Nodo Figli realizcome le operazioni di lettura e scrittura del valore del nodo (read() e write()) di vengono molti nodi hanno grado minore del grado massimo k. © Alberto Montresor 12 CAPITOLO 5. ALBERI Realizzazione con puntatori padre/primo-figlio/fratello Realizzazione con puntatori padre/primo-figlio/fratello 89 / T REE / N ODE parent N ODE child N ODE sibling I TEM value / T REE N ODE parent N ODE child / N ODE sibling I TEM value Tree(I TEM v) T REE t t.value t.parent return t / new T/ REE / v t.child t.sibling % Puntatore al padre % Puntatore al/primo / / figlio / % Puntatore al successivo fratello % Valore del nodo / / child T REE t t.value t.parent return t / nil Nodo t.parent this t.sibling child child t Primo Figlio Fratello t.parent this t.sibling sibling % Inserisce t prima dell’attuale fratello Realizzazione con puntatori padre/primo-figlio/fratello sibling t deleteChild() N ODE newChild 13 % Inserisce t prima dell’attuale fratello deleteChild() 14 N ODE newChild child.rightSibling() delete(child) Realizzazione con vettore dei padri child newChild deleteSibling() ✦ child.rightSibling() L'albero rappresentato da un vettore()i cui elementi contengono N ODE ènewBrother sibling. rightSibling l'indice padre deletedel (sibling) sibling newChild ✦ Esempio: newBrother delete(T REE t) deleteSibling() 0 a N ODE u t.leftmostChild 1 b () while u ⇥= nil do 1 e T REE next u.rightSibling () delete(u) 2 c u next 2 d delete t N ODE newBrother sibling.rightSibling() delete(sibling) sibling newBrother delete(T REE t) N ODE u t.leftmostChild() while u ⇥= nil do T REE next u.rightSibling() delete(u) u next delete t © Alberto Montresor % Inserisce t prima dell’attuale primo figlio © Alberto Montresor delete(child) child nil t.parent this t.sibling sibling sibling t % Inserisce t prima dell’attuale primo figlio insertSibling(T REE t) % Crea un nuovo nodo insertSibling(T REE t) t © Alberto Montresor new T REE v t.child t.sibling insertChild(T REE t) / Padre insertChild(T REE t) Soluzione: usare una lista t.parent this di t.sibling figli (fratelli). child Tree(I TEM v) % Crea un nuovo nodo / % Puntatore al padre % Puntatore al primo figlio % Puntatore al successivo fratello % Valore del nodo 3 f T a b c e d f g 3 g 15 © Alberto Montresor 16 di seguito una semplice realizzazione, tralasciando per semplicità di memorizzare i valori dei Realizzazione con vettore dei padri nodi. T REE integer[ ] p Alberi binari Definizione ✦ % Vettore dei padri % Costruisce una “foresta” con n nodi isolati Tree(integer n) p new integer[1 . . . n] for i 1 to n do p[i] 0 ✦ Un albero binario è un albero ordinato in cui ogni nodo ha al più due figli e ✦ si fa distinzione tra il figlio sinistro ed il figlio destro di un nodo. Nota: ✦ ✦ % Restituisce il padre del nodo i; restituisce 0 se i è radice integer parent(integer i) return p[i] due alberi T e U aventi gli stessi nodi, gli stessi figli per ogni nodo e la stessa radice, sono CAPITOLO 5. distinti ALBERI qualora un nodo u sia designato come figlio sinistro di 91un nodo v in T e come figlio destro del medesimo nodo in U livello % Rende il nodo i un figlio del nodo j setParent(integer i, integer j) p[i] j 2 92 5.5 Alberi binari 17 © Alberto Montresor Un albero binario è un particolare albero ordinato in cui ogni nodo ha al più due figli e si faAlberi distinzione tra il figlio sinistro ed il figlio destro di un nodo. Questa proprietà è assai sottile, binari poiché impone che due alberi T e U aventi gli stessi nodi, gli stessi figli per ogni nodo e la Figliodidestro Figlio sinistro stessa radice, siano distinti qualora un nodo u sia designato come figlio sinistro un nodo v Radice Radice del in T e come figlio destro del medesimo nodo in U . Radice del sottoalbero destro 5 T 1 Algoritmi e Strutture di Dati 2 5 3 T 2 3 Una possibile specifica per gli alberi binari include le operazioni per leggere e scrivere i18 Figura 5.7: Alberi ordinati e alberi binari. figli destro e sinistro, che sostituiscono le operazioni per leggere e scrivere il primo figlio e i Alberisuccessivi. binari: specifica fratelli precondition: t.parent = nil L’importanza degli alberi binari risiede sia sulla semplicità della loro struttura che sulle a innumerevoli applicazioni in cui possono essere impiegati. si precondition: t.parent = nil a<b no c c <nodo b % Distrugge il sottoalberob <sinistro (destro) di questo si no si no deleteLeft() a<c c,b,a a<c deleteRight() a,b,c k si © Alberto Montresor 4 3 © Alberto Montresor insertRight(T REE t) destro a.left() 4 1 Esempio 5.7 (Albero delle scelte). Le possibili sequenze di confronti (a due alternative) effettuate da un algoritmo per risolvere un problema possono essere visualizzate con un albero T REEbinario, detto albero delle scelte. In tale albero, i nodi con figli rappresentano confronti tra % Restituisce il figlio sinistro (destro)i risultati di questo restituisce nilsoluzioni se assente dati del problema, le coppie padri-figli deinodo; confronti, e le foglie possibili T REE () del left problema. Un percorso radice-foglia rappresenta la sequenza di confronti effettuati per individuare T REE right() una soluzione e il livello massimo di una foglia dà il numero di confronti effettuati nel caso pessimo. Un albero delle scelte che minimizzi il livello massimo delle foglie fornisce % Inserisce il sottoalbero t come figlio sinistro (destro) di questo nodo con tale valore una limitazione inferiore al numero di decisioni che ogni algoritmo che risolva insertLeft (T REE il problema devet)effettuare nel caso peggiore. sottoalbero sinistro Esempio 5.6 (Alberi binari). La Fig. 5.7 mostra tre alberi T1 , . . . , T3 . Visti come alberi ordinati, essi sono tutti identici tra loro. T1 nonj.parent() è binario, perché non è chiara la convenzione per distinguere i figli sinistri dai destri. Considerando i figli disegnati a sinistra (destra) come figli Padre del sinistri (destri), allora T2 e T3 sono binari; inoltre essi sono tra loro distinti, poiché non hanno nodo j (e k) gli stessi figli destri e sinistri. Sottoalbero Sottoalbero sinistro 0 2 3 5 T 1 2 4 3 (n log n). j 1 1 a.right() a,c,b 19 no c,a,b si b,a,c no b,c,a Una realizzazione ragionevole fa uso di puntatori, come nel caso degli alberi ordinati, i cui Figura 5.8: Albero per l’ordinamento di tre numeri. campi child e sibling sono sostituiti dadelle leftscelte e right. La complessità di tutte le operazioni è O(1), con esclusione di quella di cancellazione, che agendo ricorsivamente può avere complessità La Fig. 5.8 illustra un albero delle scelte per il problema di ordinare tre numeri a, b, c. Per motivi di spazio, le operazioni parent(), left(), right(), read() e write() non sono20 ©O(n). Alberto Montresor Poiché un qualsiasi algoritmo di ordinamento di n numeri deve fare una sequenza di scelte mostrate; semplicemente, restituiscono il valore della variabile corrispondente. Figura 5.9: Rappresentazione di un albero ordinato T con un albero binario B. Alberi binari: realizzazione Alberi binari: realizzazione T REE T REE Tree(I TEM v) deleteLeft() La sostanziale equivalenza di queste due realizzazioni non deve poiché nel if left ⇥= nil then T REE sorprendere, t = new T REE t.parent nil left.deleteLeft() proporre quella per gli alberi ordinati si è implicitamente trasformato l’albero ordinato in uno t.left t.right nil left.deleteRight() binario! Come illustrato nella Fig. 5.9, è sempre possibile rappresentare un albero ordinato T t.value v delete left t left nil / / B avente gli / / / albero binario / con un stessi nodi e la stessa radice, in return cui ogni nodo di B ha come insertLeft(T REE T ) figlio sinistro il primo figlio che ha in T e come figlio destro il fratello successivo che ha in TdeleteRight . () T.parent this if right ⇥= nil then leftcoincidono T È facile verificare che le sequenze dei nodi esaminati su T e su B se T e B sono right.deleteLeft() / / / / Padre insertRight (T REE T ) right.deleteRight() entrambi visitati in ordine anticipato, oppure se T è visitato in ordine posticipato e B è visitato T.parent this Figlio Figlio delete right Sinistro simmetrico. Destro in ordine right T Nodo right nil Utilizzando le operazioni introdotte, la visita di un albero binario viene effettuata molto semplicemente da visitaProfondità(). La previsita si ottiene esaminando il nodo t soltanto nella 21 la invisita o la postvisita si hanno riga (1) e ignorando le righe (2) e (3). Analogamente, esaminando ilprofondità nodo t solo nella riga (2) o in (3), rispettivamente. Alberi binari: visite in Semplici esercizi basati su visite Per motivi di spazio, le operazioni parent(), left(), right(), read() e write() non sono mostrate; semplicemente, restituiscono il valore della variabile corrispondente. © Alberto Montresor © Alberto Montresor visitaProfondità(T REE t) (1) (2) (3) if t = nil then esame “anticipato” del nodo radice di t visitaProfondità(t.left()) esame “simmetrico” del nodo radice di t visitaProfondità(t.right()) esame “posticipato” del nodo radice di t ✦ Es. 5.1 - Dato un albero radicato T, calcolare la sua altezza ✦ Dato un albero radicato T, calcolare il numero totale di nodi ✦ Dato un albero radicato T, stampare tutti i nodi a profondità h 22 www.xkcd.com 5.6 Altre realizzazioni © Alberto Montresor 23 © Alberto Montresor 24