Προγραμματισμός Ι (ΗΥ120) Διάλεξη 20: Δυαδικό Δέντρο Αναζήτησης
Δυαδικό δέντρο Κάθε κόμβος «γονέας» περιέχει δύο δείκτες που δείχνουν σε δύο κόμβους «παιδιά» του ιδίου τύπου. Αν οι δείκτες προς αυτούς τους κόμβους είναι NULL, δηλαδή ένας κόμβος δεν έχει παιδιά, τότε ο κόμβος ονομάζεται φύλλο (leaf). Τα παιδιά ενός κόμβου μπορεί και αυτά με την σειρά τους να έχουν παιδιά κλπ. Το δέντρο είναι μια ακόμα αναδρομική δομή: ένα δέντρο είναι είτε : Κενό, είτε Αποτελείται από ένα κόμβο που δείχνει σε δύο υποδέντρα. 2
D 3 B F A C E G
Συνθήκη δυαδικού δέντρου αναζήτησης Η ιδιότητα του δέντρου αναζήτησης είναι: εξετάζοντας έναν οποιονδήποτε κόμβο με τιμή v: όλοι οι κόμβοι που βρίσκονται στο αριστερό υποδέντρο του έχουν τιμή μικρότερη του v και όλοι οι κόμβοι που βρίσκονται στο δεξί υποδέντρο έχουν τιμή μεγαλύτερη του v 4 Σημείωση: υποθέτουμε ότι δεν επιθυμούμε να υπάρχουν «διπλά» στοιχεία (με ίδια τιμή).
5 x elements < x elements > x
Ενδεικτική υλοποίηση Κάθε κόμβος περιέχει δύο δείκτες που δείχνουν στο επόμενο αριστερό και δεξί υποδέντρο, αντίστοιχα. Αναζήτηση: αρχίζουμε από την ρίζα και κατευθυνόμαστε προς το αριστερό ή δεξί υποδέντρο, ανάλογα με την τιμή των κόμβων που βρίσκουμε στο μονοπάτι μας. Εισαγωγή: ο νέος κόμβος εισάγεται σαν φύλλο του κόμβου στον οποίο φτάνουμε αναζητώντας την τιμή που επιθυμούμε να προσθέσουμε. Απομάκρυνση: ο κόμβος εντοπίζεται με αναζήτηση, και αντικαθίσταται με τον «μεγαλύτερο» κόμβο του αριστερού υποδέντρου ή τον «μικρότερο» κόμβο του δεξιού υποδέντρου. 6
Επιτυχής αναζήτηση root find 23 7 25 15 45 20 30 50 23 35
Αποτυχημένη αναζήτηση root find 37 8 25 15 45 20 30 50 23 35
struct btree { int v; struct btree *l,*r; }; 9 struct btree *root; int btree_find(int v) { struct btree *curr; } curr = root; while ((curr!= NULL) && (curr->v!= v)) { if (curr->v > v) curr = curr->l; else /* curr->v < v */ curr = curr->r; } return(curr!= NULL);
Εισαγωγή root add 5 25 15 45 20 30 50 23 35
Εισαγωγή root add 5 11 25 15 45 20 30 50 5 23 35
int btree_insert(int v) { struct btree *curr,*prev; prev = NULL; curr = root; while ((curr!= NULL) && (curr->v!= v)) { prev = curr; if (curr->v > v) curr = curr->l; else /* curr->v < v */ curr = curr->r; } if (curr!= NULL) return(-1); 12 curr = (struct btree *)malloc(sizeof(struct btree)); if (curr == NULL) return(0); curr->v = v; curr->r = NULL; curr->l = NULL; /* διασύνδεση με τον κόμβο γονέα, αν υπάρχει */ if (prev == NULL) root = curr; else if (prev->v > v) prev->l = curr; else /* prev->v < v */ prev->r = curr; return(1);
Απομάκρυνση root remove 45 13 25 15 45 20 30 50 5 23 35
Απομάκρυνση root remove 45 14 25 15 45 20 30 50 5 23 35
Απομάκρυνση (εναλλακτικά) root remove 45 15 25 15 45 20 30 50 5 23 35
Πως; Τι; Αν απομακρυνθεί ένας κόμβος που δεν είναι φύλλο, τότε πρέπει να αντικατασταθεί με έναν κόμβο από τα υποδέντρα του έτσι ώστε να εξακολουθεί να ισχύει η συνθήκη αναζήτησης του δέντρου. Ο κόμβος που απομακρύνεται (με τιμή v) πρέπει να αντικατασταθεί από Το μεγαλύτερο κόμβο του αριστερού υποδέντρου (που φέρει την μεγαλύτερη τιμή που είναι μικρότερη v) ή Το μικρότερο κόμβο του δεξιού υποδέντρου (που φέρει την μικρότερη τιμή που είναι μεγαλύτερη v). Σημείωση: αφού ο κόμβος που απομακρύνεται δεν είναι φύλλο, είναι εγγυημένο ότι τουλάχιστον ένα από τα δύο υποδέντρα του δεν είναι άδεια. 16
void btree_remove(int v) { struct btree *curr,*prev,*subst; prev = NULL; curr = root; while ((curr!= NULL) && (curr->v!= v)) { prev = curr; if (curr->v > v) curr = curr->l; else /* curr->v < v */ curr = curr->r; } 17 if (curr!= NULL) { subst = getreplacement(curr); /* ανεύρεση αντικαταστάτη */ /* ο αντικαταστάτης κληρονομεί τα υποδέντρα του κόμβου curr που απομακρύνεται */ if (subst!= NULL) { subst->l = curr->l; subst->r = curr->r; } /* σύνδεση αντικαταστάτη με τον γονέα του κόμβου curr */ if (prev == NULL) root = subst; else if (prev->l == curr) prev->l = subst; else /* prev->r == curr */ prev->r = subst; } } free(curr);
struct btree *getreplacement(struct btree *tree) { struct btree *curr,*prev; } if (tree->l!= NULL) { /* βρες και απομάκρυνε τον πιο "μεγάλο" κόμβο του αριστερού υποδέντρου */ for(prev=tree,curr=tree->l; curr->r!=null; prev=curr,curr=curr->r); if (prev == tree) prev->l = curr->l; else prev->r = curr->l; return(curr); } else if (tree->r!= NULL) { /* βρες και απομάκρυνε τον πιο "μικρό" κόμβο του δεξιού υποδέντρου */ for(prev=tree,curr=tree->r; curr->l!=null; prev=curr,curr=curr->l); if (prev == tree) prev->r = curr->r; else prev->l = curr->r; return(curr); } else /* ο κόμβος tree είναι φύλλο και δεν έχει παιδιά */ return(null); 18
αφαίρεση 7 4 2 19 1 3 7 11 1) Εύρεση 7 2) Εύρεση αντικαταστάτη 7 6 9 5 8
αφαίρεση 7 4 2 20 1 3 7 11 1) Εύρεση 7 2) Εύρεση αντικαταστάτη 7 3) «Τροποποίηση» υποδέντρου του 7 6 9 5 8
αφαίρεση 7 4 2 21 1 3 6 11 1) Εύρεση 7 2) Εύρεση αντικαταστάτη 7 3) «Τροποποίηση» υποδέντρου του 7 4) «Αντικατάσταση» του 7 5 8 9
αφαίρεση 4 2 22 1 3 7 11 1) Εύρεση 2) Εύρεση αντικαταστάτη 6 9 5 8
αφαίρεση 4 2 23 1 3 7 11 1) Εύρεση 2) Εύρεση αντικαταστάτη 3) «Τροποποίηση» υποδέντρου του 6 9 5 8
αφαίρεση 4 2 9 24 1 3 7 11 1) Εύρεση 2) Εύρεση αντικαταστάτη 3) «Τροποποίηση» υποδέντρου του 4) «Αντικατάσταση» του 6 5 8
Παρατηρήσεις Η δεντρική δομή δημιουργεί προϋποθέσεις για αποδοτική αναζήτηση Κατά μέσο όρο σε log 2 N βήματα, όπως και στην δυαδική αναζήτηση σε ταξινομημένο πίνακα. Αυτό ισχύει μόνο αν το δέντρο είναι ισορροπημένο (balanced), δηλαδή αν για κάθε κόμβο ο αριθμός των κόμβων του αριστερού υποδέντρου είναι (περίπου) ίσος με τον αριθμό των κόμβων στο δεξί υποδέντρο. Παρατήρηση: αυτό είναι μια αναδρομική ιδιότητα. Αυτή η ιδιότητα επιτυγχάνεται εφόσον οι πράξεις εισαγωγής και απομάκρυνσης εξισορροπούν το δέντρο, όποτε αυτό κρίνεται απαραίτητο. 25