Informatica 3 LEZIONE 15: Implementazione di alberi binari - BST Modulo 1: Implementazione degli alberi binari Modulo 2: BST
Informatica 3 Lezione 15 - Modulo 1 Implementazione degli alberi binari
Introduzione Implementazione: alberi binari basati su puntatori discussione sullo spazio necessario alberi binary basati su array
Implementazione (1) Scelta: stessa definizione per i nodi interni e le foglie?
Implementazione (2) Esempi: albero delle espressioni, albero di Huffmann, PR quadtree,... 4x(2x+a)-c
Implementazione tramite union (1) enum Nodetype {leaf, internal}; class VarBinNode { // Generic node class public: Nodetype mytype; // Store type for node union { struct { // Internal node VarBinNode* left; // Left child VarBinNode* right; // Right child Operator opx; // Value } intl; }; Operand var; // Leaf: Value only
Implementazione tramite union (2) }; // Leaf constructor VarBinNode(const Operand& val) { mytype = leaf; var = val; } // Internal node constructor VarBinNode(const Operator& op, VarBinNode* l, VarBinNode* r) { mytype = internal; intl.opx = op; intl.left = l; intl.right = r; } bool isleaf() { return mytype == leaf; } VarBinNode* leftchild() { return intl.left; } VarBinNode* rightchild() { return intl.right; }
Implementazione tramite union (3) // Preorder traversal void traverse(varbinnode* subroot) { } if (subroot == NULL) return; if (subroot->isleaf()) cout << "Leaf: << subroot->var << "\n"; else { cout << "Internal: << subroot->intl.opx << "\n"; traverse(subroot->leftchild()); traverse(subroot->rightchild()); }
Implementazione tramite ereditarietà (1) class VarBinNode { // Abstract base class public: virtual bool isleaf() = 0; }; class LeafNode : public VarBinNode { // Leaf private: Operand var; // Operand value public: LeafNode(const Operand& val) { var = val; } // Constructor }; bool isleaf() { return true; } Operand value() { return var; }
Implementazione tramite ereditarietà (2) // Internal node class IntlNode : public VarBinNode { private: VarBinNode* left; // Left child VarBinNode* right; // Right child Operator opx; // Operator value public: IntlNode(const Operator& op, VarBinNode* l, VarBinNode* r) { opx = op; left = l; right = r; } bool isleaf() { return false; } VarBinNode* leftchild() { return left; } VarBinNode* rightchild() { return right; } Operator value() { return opx; } };
Implementazione tramite ereditarietà (3) // Preorder traversal void traverse(varbinnode *subroot) { } if (subroot == NULL) return; // Empty if (subroot->isleaf()) // Do leaf node cout << "Leaf: " << ((LeafNode *)subroot)->value() << endl; else { // Do internal node cout << "Internal: " << ((IntlNode *)subroot)->value() << endl; traverse((intlnode *)subroot)->leftchild()); traverse(((intlnode *)subroot)->rightchild()); }
Ereditarietà - 2^ versione (1) class VarBinNode { // Abstract base class public: virtual bool isleaf() = 0; virtual void trav() = 0; }; class LeafNode : public VarBinNode { // Leaf private: Operand var; // Operand value public: LeafNode(const Operand& val) { var = val; } // Constructor bool isleaf() { return true; } Operand value() { return var; } void trav() { cout << "Leaf: " << value() << endl; } };
Ereditarietà - 2^ versione (2) class IntlNode : public VarBinNode { private: VarBinNode* lc; // Left child VarBinNode* rc; // Right child Operator opx; // Operator value public: IntlNode(const Operator& op, VarBinNode* l, VarBinNode* r) { opx = op; lc = l; rc = r; } bool isleaf() { return false; } VarBinNode* left() { return lc; } VarBinNode* right() { return rc; } Operator value() { return opx; } void trav() { cout << "Internal: " << value() << endl; if (left()!= NULL) left()->trav(); if (right()!= NULL) right()->trav(); } };
Ereditarietà - 2^ versione (3) // Preorder traversal void traverse(varbinnode *root) { if (root!= NULL) root->trav(); }
Differenze Prima versione: semplice aggiungere nuovi metodi per l attraversamento dell albero Seconda versione: l aggiunta di nuovi metodi per l albero richiede di aggiungere dei metodi a livello di nodo Prima versione: occorre considerare sempre il tipo di nodo Seconda versione: il tipo di nodo viene determinato a tempo di esecuzione (late binding) Prima versione: preferibile se le sottoclassi dei tipi di nodi vengono tenute nascoste alla classe albero Seconda versione: preferibile se i nodi hanno anche vita indipendente
Spazio necessario (1) Overhead = spazio necessario per mantenere la struttura dati (spazio non utilizzato per memorizzare i dati) dipende da diversi fattori: nodi con stessa definizione oppure definizioni diverse per nodi interni e nodi foglia albero esteso con puntatori al padre albero pieno o non pieno
Spazio necessario (2) Esempio: n numero dei nodi p spazio occupato dal puntatore d spazio occupato dai dati Tutti i nodi sono uguali e hanno i puntatori ai figli: Spazio totale: n(2p + d) Overhead: 2pn Se p = d l overhead è pari a 2p/(2p + d) = 2/3 I nodi foglia non hanno i puntatori e l albero è pieno: n/2 (2p) p n/2 (2p) + dn p+d Se p = d l overhead è pari a 1/2 Se i dati vengono memorizzati solo nelle foglie: 2p / (2p + d) per p = d dà un overhead di 2/3
Implementazione tramite array (1) Alberi completi: Posizione 0 1 2 3 4 5 6 7 8 9 10 11 Padre 0 0 1 1 2 2 3 3 4 4 5 Figlio di sinistra 1 3 5 7 9 11 Figlio di destra 2 4 6 8 10 Fratello di sinistra 1 3 5 7 9 Fratello di destra 2 4 6 8 10
Implementazione tramite array (2) Padre (i) = (i-1) / 2 se i!= 0 Figlio di sinistra (i) = 2i + 1 se 2i + 1 < n Figlio di destra (i) = 2i + 2 se 2i + 2 < n Fratello di sinistra (i) = i - 1 se i è pari Fratello di destra (i) = i + 1 se i è dispari e i + 1 < n
Implementazione tramite cursori Alcuni linguaggi di programmazione non dispongono di puntatori Indice Dati Sinistra Destra 1 A 2 3 2 B 0 4 3 C 5 6 4 D 0 0 5 E 0 0 6 F 0 0............ MAX.........
Informatica 3 Lezione 15 - Modulo 2 Binary Search Tree (BST)
Introduzione Definizione di Binary Search Tree (BST) albero binario che soddisfa le seguenti proprietà: ad ogni nodo è associata una chiave le chiavi di tutti i nodi che si trovano nel sotto-albero di sinistra di un nodo con chiave K hanno valore inferiore a K le chiavi di tutti i nodi che si trovano nel sotto-albero di destra di un nodo con chiave K hanno valore maggiore o uguale a K
Introduzione (2) La funzione di ordinamento è gratuita : l attraversamento in ordine simmetrico ( in order ) produce come risultato l enumerazione di tutti i nodi dal più piccolo al più grande
Ricerca di un elemento Input: Radice di un sotto-albero R Chiave dell elemento da cercare K Algoritmo: Se R ha valore di chiave pari a K la ricerca è terminata (search hit) Se la chiave K è inferiore alla chiave del nodo radice R si prosegue la ricerca nel sotto-albero di sinistra Se la chiave K è maggiore della chiave del nodo radice R si prosegue la ricerca nel sotto-albero di destra Il processo continua finchè si trova la chiave oppure si arriva ad un nodo foglia Se si raggiunge un nodo foglia senza incontrare K l elemento non esiste nel BST (search miss)
Ricerca di un elemento (2) Esempio di ricerca di un elemento presente nel BST: ricerca dell elemento 7 7 è minore di 37 > visita del sotto-albero di sinistra 7 è minore di 24 > visita del sotto-albero di sinistra elemento trovato
Ricerca di un elemento (3) Esempio di ricerca di un elemento non presente nel BST: ricerca dell elemento 34 34 è minore di 37 > visita del sotto-albero di sinistra 34 è maggiore di 24 > visita del sotto-albero di destra 34 è maggiore di 32-32 è un nodo foglia > ricerca terminata: l elemento non è presente
Inserimento di un elemento L inserimento è una ricerca con esito negativo seguita dalla sostituzione del link NULL (di sinistra o di destra di un nodo esterno) con il puntatore al nodo da inserire Esempio: inserimento del nodo 34: 34 è minore di 37 > visita del sotto-albero di sinistra 34 è maggiore di 24 > visita del sotto-albero di destra 32 è un nodo foglia - 34 è maggiore di 32 > inserimento a destra 34
Inserimento di un elemento già Osservazione: presente se occorre inserire un nodo la cui chiave è già presente nel BST il nuovo elemento viene sistemato nel sotto-albero di destra del nodo già presente Un effetto collaterale di questo modo di procedere è che i nodi con chiavi replicate non sono necessariamente contigui all interno del BST Esempio: inserimento di un elemento con chiave 24 24 Per cercare tutti gli elementi con una determinata chiave si parte dal primo nodo trovato e si procede con la ricerca nel sotto-albero di destra
Cancellazione di un nodo Caso più semplice: eliminazione del nodo con chiave minima (nell esempio rimozione del nodo 5) sotto-radice Si visita l albero partendo dalla radice e continuando a scendere nel sotto-albero di sinistra fino a quando non si arriva al nodo il cui puntatore di sinistra è NULL (nodo di chiave minima) Chiamiamo questo nodo S Per rimuovere S è sufficiente fare in modo che il puntatore a sinistra del padre di S punti al figlio destro di S
Cancellazione di un nodo (2) Eliminazione di un nodo R con chiave non minima Si visita l albero partendo dalla radice fino a trovare l elemento da cancellare R Se R non ha figli allora il padre di R dovrà puntare a NULL Se R ha un figlio allora il padre di R dovrà puntare al figlio di R Se R ha due figli si può adottare il seguente approccio: si fa puntare R ad uno dei due sotto-alberi di R si applica la funzione di inserimento per tutti i nodi dell altro sotto-albero
Cancellazione di un nodo (3) Approccio alternativo: si cerca un valore in uno dei sotto-alberi che possa sostituire il valore di R (preservando le proprietà del BST) R può essere sostituito dal nodo con: la più piccola chiave maggiore del nodo rimosso la più grande chiave minore del nodo rimosso
Cancellazione di un nodo (4) Esempio di cancellazione del nodo 37 può essere sostituito dal nodo con: la più piccola chiave maggiore del nodo rimosso: 40 la più grande chiave minore del nodo rimosso: 32
Cancellazione di un nodo Osservazioni generali: Per semplificare gli algoritmi di ricerca le chiavi di ricerca sono integrate nella struttura dati Ciò richiede tipicamente un implementazione più complicata per le operazioni di cancellazione dei nodi Metodi di cancellazione alternativi: cancellazione lazy (pigra): i nodi vengono marcati con un flag come cancellati ma non vengono eliminati dalla struttura dati» gli algoritmi di ricerca devono tenere in considerazione questi flag» svantaggi: eccessive cancellazioni portano a strutture dati che sprecano spazio di memoria e tempo nella ricerca» per attenuare questi svantaggi si possono effettuare ricostruzioni periodiche della struttura oppure utilizzare i nodi cancellati per futuri inserimenti
Prestazioni dei BST I tempi di esecuzione degli algoritmi definiti sui BST dipendono dalla forma dell albero Quando l albero è perfettamente bilanciato ci sono circa log N nodi tra la radice e ciascun nodo esterno La forma dell albero dipende dall ordine con il quale gli elementi vengono inseriti Un BST di N nodi può essere costituito da una catena di nodi di altezza N Ciò accade quando gli elementi vengono inseriti ordinati 24 32 Nel caso peggiore la ricerca di un elemento in un BST con N chiavi richiede N confronti 35
Prestazioni dei BST (2) RICERCA di un elemento: Un albero bilanciato ha un costo pari a Θ(log n) nel caso medio Se l albero non è bilanciato il costo nel caso peggiore è Θ(n) INSERIMENTO di N nodi: Se l albero è bilanciato ogni inserimento richiede un costo pari a Θ(log n) > per n nodi: Θ(n log n) Se i nodi vengono inseriti in ordine (albero sbilanciato) il costo dell inserimento diventa Θ(n 2 ) VISITA DELL ALBERO: Costa Θ(n) indipendentemente dal bilanciamento dell albero
BST Search template <class Key, class Elem> bool BST<Key, Elem>:: findhelp(binnode<elem>* subroot, const Key& K, Elem& e) const { if (subroot == NULL) return false; else if (K < subroot->val()) return findhelp(subroot->left(), K, e); else if (K > subroot->val())) return findhelp(subroot->right(), K, e); else { e = subroot->val(); return true; } }
BST Insert template <class Key, class Elem> BinNode<Elem>* BST<Key,Elem>:: inserthelp(binnode<elem>* subroot, const Elem& val) { if (subroot == NULL) // Empty: create node return new BinNodePtr<Elem>(val,NULL,NULL); if (val < subroot->val()) subroot->setleft(inserthelp(subroot->left(), val)); else subroot->setright( inserthelp(subroot->right(), val)); // Return subtree with node inserted return subroot; }
Remove Minimum Value template <class Key, class Elem> BinNode<Elem>* BST<Key, Elem>:: deletemin(binnode<elem>* subroot, BinNode<Elem>*& min) { if (subroot->left() == NULL) { min = subroot; return subroot->right(); } else { // Continue left subroot->setleft( deletemin(subroot->left(), min)); return subroot; } }
BST Remove template <class Key, class Elem> BinNode<Elem>* BST<Key,Elem>:: removehelp(binnode<elem>* subroot, const Key& K, BinNode<Elem>*& t) { if (subroot == NULL) return NULL; else if (K < subroot->val()) subroot->setleft( removehelp(subroot->left(), K, t)); else if (K > subroot->val()) subroot->setright( removehelp(subroot->right(), K, t));
BST Remove (2) else { // Found it: remove it BinNode<Elem>* temp; t = subroot; if (subroot->left() == NULL) subroot = subroot->right(); else if (subroot->right() == NULL) subroot = subroot->left(); else { // Both children are non-empty subroot->setright( deletemin(subroot->right(), temp)); Elem te = subroot->val(); subroot->setval(temp->val()); temp->setval(te); t = temp; } } return subroot; }
Conclusioni I BST sono semplici da implementare e sono efficienti quando l albero è bilanciato Se non controllata questa struttura dati tende a non essere perfettamente bilanciata, portando a peggioramenti delle prestazioni Esistono strutture dati alternative (AVL tree, Splay tree, ecc.) che al prezzo di un maggiore costo per gli inserimenti e le cancellazioni cercano di mantenere bilanciato l albero