Trees Page 1 Μάθημα 22: Δυαδικά δέντρα (Binary Trees) Ένα δένδρο είναι δυαδικό αν όλοι οι κόμβοι του έχουν βαθμό (degree) <= 2. Ορισμός: Δυαδικό δένδρο λέγεται ένα δένδρο το οποίο: είναι κενό αποτελείται από μια ρίζα και δύο δυαδικά υπόδενδρα Το βάθος (depth) ενός κόμβου είναι ο αριθμός των ακμών από τον κόμβο μέχρι τη ρίζα. Η ρίζα έχει βάθος 0. Το ύψος (height) ενός κόμβου είναι ο αριθμός των ακμών στο μεγαλύτερο μονοπάτι από τον κόμβο σε ένα φύλλο. Το ύψος (height) ενός δυαδικού δένδρου με n κόμβους μπορεί να είναι το πολύ n-1 και το λιγότερο log(n). Ένα δυαδικό δένδρο είναι γεμάτο (full), αν κάθε κόμβος έχει μηδέν ή δύο απογόνους. Ένα δυαδικό δένδρο είναι τέλειο (perfect), αν είναι γεμάτο και όλα τα φύλλα έχουν το ίδιο βάθος.
Trees Page 2 Αναπαράσταση δένδρων Αφού κάθε κόμβος σε ένα δυαδικό δένδρο έχει το πολύ δύο παιδιά, μπορούμε να κρατούμε δείκτες στο καθένα από αυτά. Δηλαδή, ένας κόμβος μπορεί να υλοποιηθεί ως μια δομή με τρία πεδία. 1.data, όπου αποθηκεύουμε το κλειδί του κόμβου, 2.left, τύπου pointer, το οποίος δείχνει το αριστερό υπόδενδρο 3.right, τύπου pointer, το οποίος δείχνει το δεξιό υπόδενδρο struct node { int data; struct node *left; struct node *right; Δυαδικά Δένδρα Αναζήτησης - ΔΔΑ (Binary Search Trees - BST) Ένα δυαδικό δένδρο αναζήτησης (ΔΔΑ) είναι ένα δυαδικό δένδρο κάθε κόμβος u του οποίου ικανοποιεί τα εξής: 1. τα κλειδιά του αριστερού υποδένδρου του u είναι μικρότερα από το κλειδί του u 2. τα κλειδιά του δεξιού υποδένδρου του u είναι μεγαλύτερα από το κλειδί του u. Παράδειγμα Κτισίματος ενός ΔΔΑ 1. Με τα στοιχεία 50, 60, 40, 30, 20, 45, 65:
Trees Page 3 Με τα στοιχεία: 10, 20, 30, 40, 50: Συνάρτηση εύρεσης τυχαίου στοιχείου (find) Απλή αναδρομική στρατηγική: συγκρίνουμε το στοιχείο που μας ενδιαφέρει α με το στοιχείο της ρίζας του δένδρου β (αν υπάρχει) και: 1.αν α=β, σταματούμε 2.αν α<β, προχωρούμε στο αριστερό υπόδενδρο 3.αν α>β προχωρούμε στο δεξιό υπόδενδρο int find(struct node *node, int target) { if (node == NULL) { return 0; else {
Trees Page 4 if (target == node->data) return 1; else { if (target < node->data) return(find(node->left, target)); else return(find(node->right, target)); Συνάρτηση εισαγωγής νέου κόμβου (Insert) Διασχίζουμε το δέντρο, όπως θα κάναμε με τη find Εάν το X βρεθεί, δεν κάνουμε καμία ενέργεια Διαφορετικά, εισάγουμε το X στο τελευταίο σημείο του μονοπατιού που διασχίστηκε struct node *newnode(int data) { struct node *node = new(struct node); node->data = data; node->left = NULL; node->right = NULL; return node; struct node *insert(struct node *node, int data) { if (node == NULL) { return newnode(data); else { if (data < node->data) node->left = insert(node->left, data); else node->right = insert(node->right, data); return node; Διαγραφή κόμβων (Delete) Παράδειγμα διαγραφής στοιχείου: Α. Διαγραφή του 20 1. Βρίσκουμε τον κόμβο u που περιέχει το i. Ας υποθέσουμε πως ο v είναι ο πατέρας του u. 2. Αν ο u είναι φύλλο, τότε αλλάζουμε τον δείκτη του v που δείχνει στο u, ώστε να γίνει null. Β. Διαγραφή του 60 Αν ο u έχει ένα παιδί, τότε αλλάζουμε τον δείκτη του v που δείχνει τον u, ώστε να δείχνει στο παιδί του u. Γ. Διαγραφή του 50 Αν ο u έχει δύο παιδιά,
Trees Page 5 παιδί του u. Γ. Διαγραφή του 50 Αν ο u έχει δύο παιδιά, αλλάζουμε το κλειδί του u ώστε να γίνει το μεγαλύτερο από τα κλειδιά όλων των απογόνων του που έχουν κλειδιά μικρότερα του i. διαγράφουμε τον κόμβο με το μεγαλύτερο κλειδι στο αριστερό υπόδεντρο του u. Διάσχιση ΔΔΑ Αν θέλουμε να επισκεφθούμε όλους τους κόμβους ενός δένδρου, μπορούμε να χρησιμοποιήσουμε ένα από τους πιο κάτω τρόπους: 1. 2. 3. Προθεματική Διάσχιση: (Preorder Traversal) επισκεπτόμαστε πρώτα κάποιο κόμβο και μετά τα παιδιά του. Μεταθεματική Διάσχιση: (Postorder Traversal) επισκεπτόμαστε πρώτα τα παιδιά και ύστερα τον κόμβο. Ενδοθεματική Διάσχιση: (Inorder Traversal) επισκεπτόμαστε πρώτα τα αριστερά παιδιά, μετά τον κόμβο και μετά τα δεξιά παιδιά. PreOrder: 2 1 4 3 5 PostOrder: 1 3 5 4 2 InOrder: 1 2 3 4 5 Δέντρα στην STL STL - Standard Template Library Ένα σημαντικό πλεονέκτημα της C++ έναντι άλλων γλωσσών είναι ότι παρέχει πλήθος δομικών στοιχείων για ανάπτυξη κώδικα. Αυτά τα στοιχεία συμπεριλαμβάνονται στη βιβλιοθήκη STL. H STL περιέχει τρεις βασικές συνιστώσες: Containers: δομές για αποθήκευση και διαχείριση δεδομένων, υποκατάστατα των πινάκων αλλά με περισσότερες δυνατότητες. Μεταξύ άλλων έχουμε containers που περιλαμβάνουν αυτόματη ταξινόμηση (set, map) ή ταχύτατη ανάκτηση δεδομένων με ακέραιο δείκτη (vector). Iterators: ένα είδος δείκτη για τις θέσεις των στοιχείων ενός container. Έχουν την ίδια μορφή για όλα τα containers. Αλγόριθμους: υλοποιούν τμήματα κώδικα όπως ταξινόμηση, αναζήτηση ή αντικατάσταση στοιχείου. Στην STL δεν υπάρχει container με το όνομα tree. Υπάρχουν όμως τα map και set που δημιουργούν ένα ισοζυγισμένο δέντρο (self-balancing tree) με το όνομα red-black tree που υποστηρίζει αναζήτηση, εισαγωγή και διαγραφή σε χρόνο O(logn) όπου n είναι το συνολικό πλήθος των στοιχείων στο δέντρο.
Trees Page 6 1. set και multiset <set> Αποθηκεύουν τα δεδομένα τους με συγκεκριμένη σειρά και η ταξινόμηση γίνεται αυτόματα. Η βασική τους διαφορά είναι ότι το multiset επιτρέπει περισσότερα από ένα στοιχείο με την ίδια τιμή ενώ το set όχι. Άρα τα στοιχεία στο set είναι μοναδικά. Προσθήκη στοιχείων s.insert(a) Εισάγει το στοιχείο a s.insert(it,a) Εισάγει το στοιχείο a στη θέση του iterator it Διαγραφή στοιχείων s.erase(a) Διαγράφει το στοιχείο a s.erase(it) Διαγράφει το στοιχείο στη θέση του iterator it s.clear() Επιπλέον συναρτήσεις count(a) find(a) lower_bound(a) Διαγράφει όλα τα στοιχεία του s Επιστρέφει το πλήθος των στοιχείων με τιμή a Επιστρέφει iterator στη θέση του στοιχείου a ή αν δεν υπάρχει επιστρέφει end() Επιστρέφει τη θέση του πρώτου στοιχείου που δεν είναι μικρότερο από το a upper_bound(a) Επιστρέφει τη θέση του πρώτου στοιχείου που είναι μεγαλύτερο από το a Παράδειγμα 1: #include <iostream> #include <set> using namespace std; int main(){ char ch; set<char> s; multiset<char> ms; cout << "Please enter a sentence: "; for (;;){ cin.get(ch); if (ch == '\n') break; s.insert(ch); ms.insert(ch); cout << endl << "The set: "; for (set<char>::iterator it = s.begin(); it!= s.end(); it++) cout << *it << ','; cout << endl; cout <<endl<< "The multiset: "; for (multiset<char>::iterator it = ms.begin(); it!= ms.end(); it++) cout << *it << ','; cout << endl; return 0;
Trees Page 7 Input: A babboon blew up a balloon Output: The set:,a,a,b,e,l,n,o,p,u,w, The multiset:,,,,,a,a,a,a,b,b,b,b,b,e,l,l,l,n,n,o,o,o,o,p,u,w, 2. map και multimap (<map> Με τα container map και multimap αποθηκεύουμε ζεύγη στοιχείων στα οποία το πρώτο μέλος (first) έχει το ρόλο του κλειδιού (key) και το δεύτερο μέλος (second) είναι η αντίστοιχη τιμή του. Η βασική διαφορά με το set είναι ότι το map αποθηκεύει κλειδί και τιμή ενώ το set μόνο το κλειδί. Το multimap μπορεί να δεχτεί περισσότερα από ένα ζεύγη με το ίδιο κλειδί ενώ το map δεν επιτρέπει να εισάγουμε κλειδί που ήδη υπάρχει. pair (<utility>) H STL παρέχει containers που αποθηκεύουν ζεύγη στοιχείων, κάτι που θα μας φανεί ιδιαίτερα χρήσιμο με τα map. Το pair μπορεί να περιέχει δύο στοιχεία με διαφορετικούς τύπους και ορίζεται ως εξής: pair<int, double> p1; // p1 == (0, 0.0) pair<int, double> p2(3, 2.0); Η πρόσβαση στα στοιχεία γίνεται ως εξής: pair<int, double> p(3, 2.0); cout << "First: " << p.first << " "<< "Second: " << p.second; Η κατασκευή ενός pair μπορεί να γίνει με τη συνάρτηση make_pair(). pair<int, double> p; p = make_pair(4, 3.0); Πως δημιουργείται το δυαδικό δέντρο αναζήτησης; map<string, int> friends; friends["anne"] = 0; friends["bob"] = 1; friends["chris"] = 2; Παράδειγμα 2:
Trees Page 8 #include <iostream> #include <string> #include <map> #include <utility> using namespace std; int main() { map <string, int> m; m.insert(make_pair("john", 1940)); m.insert(make_pair("paul", 1942)); m.insert(make_pair("nick", 1943)); cout << "John was born in " << m["john"] << endl; // insert new data m["mike"] = 1941; // wrong value, change it m["mike"] = 1940; // print all pairs for (map<string,int>::iterator it = m.begin(); it!= m.end(); ++it) cout << it->first << " was born in "<< it->second << endl; return 0; Παράδειγμα 3: #include <iostream> #include <map> #include <string> using namespace std; int main(){ map<string, string> address; address["andreou"] = "5 Dimitras, Aglatzia"; address["costa"] = "8 Konstantinoupoleos, Geri"; address["damianou"] = "23 Melpomenis, Strovolos"; address["nikolaou"] = "17 Dorieon, Aglatzia"; address["georgiou"] = "6 Salaminos, Strovolos"; address["christou"] = "8 Artemisias, Agios Dometios"; address["menelaou"] = "34 Mnasiadou, Lefkosia"; address["vasiliou"] = "23 Onasagorou, Lemesos"; address["ioannou"] = "11 Lemesou, Lefkosia "; address["michael"] = "12 Athinas, Pafos"; address["zikakis"] = "15 Iras, Larnaka"; for (;;){ cout << "Enter the name of student ('q' to quit): "; string student; getline(cin, student); if (student == "q") break; map<string, string>::iterator it = address.find(student); if (it!= address.end()) cout << "--> " << it->second << endl; else
Trees Page 9 cout << student << " is not a student" << endl; return 0;