Κατ οίκον Εργασία 3 Σκελετοί Λύσεων Άσκηση 1 (α) Έστω Α(n) και Κ(n) ο αριθμός των ακμών και ο αριθμός των κόμβων ενός αυστηρά δυαδικού δένδρου με n φύλλα. Θέλουμε να αποδείξουμε για κάθε n 1 την πρόταση Π(n) Α(n) = 2n 2 και Κ(n) = 2n 1 Η απόδειξη μπορεί να γίνει με μαθηματική επαγωγή. Βασική περίπτωση n=1 Προφανώς η περίπτωση αυτή αφορά το δένδρο που περιέχει μόνο μία ρίζα. Αυτό το δένδρο έχει Α(1) = 0 και Κ(1) = 1. Επομένως το ζητούμενο έπεται. Υπόθεση της επαγωγής: Έστω Π(k) για κάθε k < m για κάποιο m > 1. Βήμα της επαγωγής: Θα δείξουμε ότι η Π(m) αληθεύει. Έστω ένα αυστηρά δυαδικό δένδρο με m φύλλα. Όπως φαίνεται και στο σχήμα, το δένδρο αυτό αποτελείται από μια ρίζα και δύο μη κενά υπόδενδρα που ριζώνουν σ αυτή. Έστω ότι το αριστερό υπόδενδρο έχει m 1 φύλλα και το δεξί m 2 φύλλα. Τότε ισχύει ότι m = m 1 + m 2. Ο αριθμός των ακμών του δένδρου είναι ίσος με τον αριθμό των ακμών των δύο υποδένδρων και ακόμα δύο ακμές που συνδέουν τη ρίζα με τα δύο υπόδενδρα. Δηλαδή, Α(m) = A(m 1 ) + A(m 2 ) + 2 = 2m 1 2 + 2m 2 2 + 2 από την υπόθεση της επαγωγής, αφού m 1, m 2 < m = 2(m 1 + m 2 ) 2 = 2m 2 Ο αριθμός των κόμβων του δένδρου είναι ίσος με τον αριθμό των κόμβων των δύο υποδένδρων και τη ρίζα, δηλαδή, Κ(m) = Κ(m 1 ) + Κ(m 2 ) + 1 = 2m 1 1 + 2m 2 1 + 1 από την υπόθεση της επαγωγής, αφού m 1, m 2 < m = 2(m 1 + m 2 ) 1 = 2m 1 Αυτό συμπληρώνει την απόδειξη. Άσκηση 2 Χρησιμοποιούμε τις δομές typedef struct BSTnode{ int data; struct node *left; struct node *right; bstnode; 1
και typedef struct BSTree{ bstnode *root; tree; (i) Για να βρούμε και να αφαιρέσουμε τον κόμβο με το μέγιστο στοιχείο ενός ΔΔΑ πρέπει να κινηθούμε προς το δεξιότερο κόμβο του δένδρου και να τον εξάγουμε ενημερώνοντας κατάλληλα τον πατέρα του κόμβου (αναθέτοντας του ως δεξί του παιδί το αριστερό παιδί του κόμβου με το μέγιστο στοιχείο). Σημειώστε ότι σε περίπτωση που το μέγιστο στοιχείο βρίσκεται στη ρίζα του δένδρου, ρίζα του δένδρου πρέπει να γίνει το αριστερό παιδί του κόμβου προς εξαγωγή. RemoveMax(tree *t) { p = t->root; if (p == NULL) return; if (p->right == NULL) t->root = p->left; free(p); father = p; node = p->right; while (node->right!= NULL) father = node; node = node->right; father->right = node ->left; free(p); (ii) H διαδικασία που προτείνεται είναι αναδρομική και εντοπίζει τρεις περιπτώσεις. Αν το δένδρο που μας δίνεται είναι κενό (ή αν το στοιχείο που μας δίνεται ως παράμετρος είναι μικρότερο ή ίσο από τα στοιχεία του δένδρου) τότε το ζητούμενο στοιχείο δεν υπάρχει και επιστρέφεται η τιμή 1. Αν το δένδρο δεν είναι κενό, τότε: (1) Αν το στοιχείο της ρίζας είναι μεγαλύτερο ή ίσο από το στοιχείο k τότε ψάχνουμε το μεγαλύτερο στοιχείο μικρότερο του k στο αριστερό υπόδενδρο του δένδρου. (2) Αν το στοιχείο της ρίζας είναι μικρότερο από το k, τότε το μεγαλύτερο στοιχείο μικρότερο του k είναι το μέγιστο εκ των (ι) στοιχείο της ρίζας και (ιι) το μέγιστο στοιχείο μικρότερο του k στο δεξί υπόδενδρο της ρίζας. Predecessor(tree *t, int k) { RecPred(t->root, k); RecPred(bstnode *p, int k) { if (p == NULL) return 1 if (k < p->data) return RecPred (p->left, k) if (k > p->data) return max(p->data, RecPred (p->right,k)); 2
Άσκηση 3 Χρησιμοποιούμε τις δομές typedef struct Node{ type key1; type key2; struct node *left; struct node *right; node; και typedef struct 2D-Tree{ bstnode *root; tree; (α) Η εισαγωγή στοιχείου είναι παρόμοια με την εισαγωγή σε ένα δυαδικό δένδρο αναζήτησης. Η μόνη διαφορά είναι ότι οι μετακινήσεις από κάθε κόμβο σε κάποιο παιδί του γίνονται βάσει διαφορετικού κλειδιού σε διαφορετικά επίπεδα, δηλαδή, σε περιττά επίπεδα βάσει του κλειδιού key1 και σε άρτια επίπεδα βάσει του κλειδιού key2. Η διαδικασία έχει ως εξής: Αν δεν έχεις φτάσει σε NULL δείκτη Αν βρίσκεσαι σε κόμβο περιττού επίπεδου, τότε σύγκρινε το πρώτο από τα κλειδιά σου με το πεδίο key1 του κόμβου και κάλεσε αναδρομικά τη διαδικασία αριστερά ή δεξιά ανάλογα. Αν βρίσκεσαι σε κόμβο άρτιου επίπεδου, τότε σύγκρινε το δεύτερο από τα κλειδιά σου με το πεδίο key2 του κόμβου και κάλεσε αναδρομικά τη διαδικασία αριστερά ή δεξιά ανάλογα. Και στις δύο περιπτώσεις οι αναδρομικές κλήσεις θα πρέπει η μεταβλητή επίπεδο να ενημερώνεται, δηλαδή να αυξάνεταικατά 1 Αν έχεις φτάσει σε NULL δείκτη, δέσμευσε μνήμη και εκτέλεσε την εισαγωγή στον καινούριο κόμβο Insert(tree *t, type x1, type x2){ p->root = RecInsert(p->root, x1, x2, 1) RecInsert(node *p, type x1, type x2, int epipedo) if (p == NULL) p=(node *)malloc(sizeof(node)); p->key1 = x1; p->key2 = x2; return p; if (epipedo mod 2 == 1) if (x1 < p->key1) p -> left = RecInsert(p->left, x1, x2, epipedo + 1); p -> right = RecInsert(p->right, x1, x2, epipedo + 1); if (x2 < p->key2) 3
p -> left = RecInsert(p->left, x1, x2, epipedo + 1); p -> right = RecInsert(p->right, x1, x2, epipedo + 1); (β) Η πιο κάτω αναδρομική διαδικασία πετυχαίνει το ζητούμενο. Παρατηρούμε ότι περιέχει την παράμετρο epipedo, η οποία συγκρατεί το επίπεδο στο οποίο βρισκόμαστε για τους σκοπούς μετακίνησης μέσα στο δένδρο. Κατά την πρώτη κλήση της διαδικασίας (από τη ρίζα του δένδρου) θα πρέπει να δώσουμε στην παράμετρο την τιμή 1. Find(tree *t, type l1, l2, h1, h2){ RecFind(p->root, l1, l2, h1, h2, 1) RecFind(node *p, type l1, l2, h1, h2, int epipedo){ if (p!= NULL) if (l1 p->key1 h1 AND l2 p->key2 h2) print(p->key1, p->key2); if (epipedo mod 2 == 1) if (h1 p->key1) if (l1 p->key1) { if (h2 p->key2) if l2 p->key2 { (γ) Το προτεινόμενο δένδρο πλεονεκτεί από άποψη αποδοτικότητας για το πρόβλημα της εύρεσης στοιχείων με συνθήκες σε δύο κλειδιά: όπως φαίνεται στο μέρος (β) πιο πάνω, στη συγκεκριμένη υλοποίηση το πρόβλημα μπορεί να λυθεί μέσω διαδικασίας η οποία επισκέπτεται μόνο υπόδενδρα του δένδρου με στοιχεία που βρίσκονται εντός των ορίων που δίνονται σαν παράμετροι. Ως εκ τούτου, μπορούμε να επιβεβαιώσουμε ότι, αν υπάρχουν m στοιχεία που ικανοποιούν τις προδιαγραφές του προβλήματος, η διαδικασία έχει χρόνο εκτέλεσηςο(m + h) όπου h το ύψος του δένδρου. Αντίθετα σε ΔΔΑ λύση του προβλήματος απαιτεί χρόνο εκτέλεσης Ο(n) όπου n το πλήθος των στοιχείων του δένδρου. Άσκηση 4 Για να τυπώσουμε αναδρομικά όλα τα άρτια στοιχεία ενός 2-3 δένδρου σε αύξουσα σειρά θα πρέπει για κάθε κόμβο να τυπώνουμε τα άρτια στοιχεία του αριστερού του υποδένδρου σε 4
αύξουσα σειρά, το πρώτο στοιχείο του κόμβου, αν είναι άρτιο, τα άρτια στοιχεία του μεσαίου υποδένδρου σε αύξουσα σειρά, και, αν υπάρχει και δεύτερο στοιχείο, το δεύτερο στοιχείο, αν είναι άρτιο, και τέλος τα άρτια στοιχεία του δεξιού υποδένδρου σε αύξουσα σειρά. Χρησιμοποιούμε τη δομή typedef struct 2-3node{ int numkeys; int key1; int key2; struct node *left; struct node *center; struct node *right; node; και υποθέτουμε πως ένα 2-3 δένδρο είναι υλοποιημένο ως δείκτης στη ρίζα του δένδρου, δηλαδή, έχει τύπο *node. Το ζητούμενο υλοποιείται με την πιο κάτω αναδρομική διαδικασία: PrintEvenInc(node *p){ if (p!= NULL) PrintEvenInc(p->left); if (p->key1 mod 2 == 0) print p->key1; PrintEvenInc(p>center); if (p->numkeys == 2) if (p->key2 mod 2 == 0) print p->key2; PrintEvenInc(P->right); Η αναδρομική διαδικασία καλείται μια φορά σε κάθε κόμβο, επομένως ο χρόνος εκτέλεσής της είναι Ο(n) όπου n είναι ο αριθμός των κόμβων του δένδρου. Η μη-αναδρομική εκδοχή της διαδικασία απαιτεί τη χρήση στοίβας. I. Έστω p δείκτης στον κόμβο του δένδρου όπου βρισκόμαστε. Τοποθετούμε στη στοίβα το ζεύγος (p->key2, p->right) (αν numkeys =2) και στη συνέχεια το ζεύγος (p->key1, p- >center). II. Εφόσον ο κόμβος έχει αριστερό παιδί προχωρούμε αριστερά και επαναλαμβάνουμε από το βήμα Ι. III. Διαφορετικά, εφόσον η στοίβα δεν είναι κενή, ανασύρουμε τον κόμβο κορυφής της που πρέπει να είναι ζεύγος της μορφής (int x, node *q). Αν το x είναι άρτιος αριθμός τον τυπώνουμε και προχωρούμε στο βήμα Ι με τον δείκτη q. PrintEvenInc(node *p){ stack S; MakeEmpty(S); while (p!= NULL AND!IsEmpty(S)) if (p!= NULL) if (p->numkeys == 2) Push((p->key2, p->right),s); Push((p->key1, p->center),s); 5
p = p ->left; (x,q) = Pop(S); if (x mod 2 == 0) print x p = q; Η διαδικασία επισκέπτεται κάθε κόμβο κάποιο σταθερό αριθμό φορών επομένως ο χρόνος εκτέλεσής της είναι Ο(n) όπου n είναι ο αριθμός των κόμβων του δένδρου. 6