Άσκηση 1 Χρησιµοποιούµε τη δοµή Κατ οίκον Εργασία 3 Σκελετοί Λύσεων typedef struct Node int data; struct node *lchild; struct node *rbro; node; και υποθέτουµε πως ένα τυχαίο δένδρο είναι υλοποιηµένο ως δείκτης στη ρίζα του δένδρου, δηλαδή, έχει τύπο *node. (i) Το ύψος ενός δένδρου ισούται µε το µέγιστο ύψος των παιδιών του +1. Αναδροµικά έχουµε την πιο κάτω διαδικασία: int Height(node *p) h = -1; if (p == NULL) return h; p = p->lchild; while (p!= NULL) h = max(h, Height(p)); p = p -> rbro; return h+1; Xρόνος εκτέλεσης: Θ(n) όπου n είναι ο αριθµός κόµβων του δένδρου. (ii) Το επίπεδο ενός κόµβου ισούται µε τον αριθµό των προγόνων του +1. Για την υλοποίηση της διαδικασίας θα χρειαστούµε τη βοηθητική δοµή ουρά, όπου θα φυλάγονται οι κόµβοι για να τύχουν επεξεργασίας αργότερα. Σε κάθε κόµβο της ουράς φυλάγεται µια δυάδα, ο κόµβος του δένδρου και το επίπεδο στο οποίο βρίσκεται, έτσι ώστε όταν αφαιρέσουµε τον κόµβο από την ουρά να ξέρουµε το επίπεδό του. void elements(node *t, int level) int clevel = 1; node *q,*p = t; Queue Q; MakeEmptyQueue(Q); if (t==null) exit(0) while (p!= null) // Αν είµαστε στο σωστό επίπεδο τυπώνουµε τον κόµβο 1
// και τα αδέλφια του. if(clevel == level) print (p->data); q = p->rbro; while (q!= null) print (q->data); q = q->rbro; // εν υπάρχει λόγος επεξεργασίας των παιδιών του // κόµβου, αφού θα έχουν µεγαλύτερο επίπεδο. Γι αυτό // αφαιρούµε ένα κόµβο από την ουρά και τον // επεξεργαζόµαστε if (!IsEmptyQueue(Q) (p,clevel) = DeQueue(Q); // Αν δεν είµαστε στο σωστό επίπεδο και ο κόµβος δεν // έχει παιδιά επεξεργαζόµαστε ένα κόµβο από την ουρά. // Αν δεν υπάρχει κόµβος στην ουρά τερµατίζουµε if (p->lchild == null) if (!IsEmptyQueue(Q) (p,clevel) = DeQueue(Q); exit(0); // Αν δεν είµαστε στο σωστό επίπεδο και ο κόµβος έχει // παιδιά φυλάγουµε τα αδέλφια του στην ουρά και // επεξεργαζόµαστε το αριστερό του παιδί, αυξάνοντας το // επίπεδο κατά 1 q = p->rbro; while (q!= null) EnQueue((q->rbro, clevel), Q); p = p->lchild; clevel += 1; Χρόνος εκτέλεσης: Θ(n) όπου n είναι ο αριθµός κόµβων του δένδρου. Άσκηση 2 Εισαγωγή των χαρακτήρων µε αλφαβητική σειρά έχει ως αποτέλεσµα τη δηµιουργία του δένδρου µε τον µεγαλύτερο αριθµό κόµβων: 15 κόµβοι, δηλαδή κάθε κόµβος έχει ένα κλειδί. Μια άλλη σειρά που παράγει το ίδιο δένδρο είναι η Π, Υ, Ε, Β, Ν, Σ, Χ, Α,, Μ, Ο, Ρ, Τ, Φ και Ω. Μια σειρά που παράγει µε τον µικρότερο αριθµό κόµβων είναι η Π, Α, Ω, Ο, Ρ,, Φ, Ν, Τ, Υ, Β, Ε, Μ, Σ και Υ. Η σειρά αυτή παράγει δένδρο µε 9 κόµβους. 2
Άσκηση 3 Χρησιµοποιούµε τη δοµή typedef struct AVLNode int data; int height struct AVLNode *left; struct AVLNode *right; int greater; avlnode; και υποθέτουµε πως ένα AVL δένδρο είναι υλοποιηµένο ως δείκτης στη ρίζα του δένδρου, δηλαδή, έχει τύπο *avlnode. Στο πεδίο greater είναι αποθηκευµένος ο αριθµός των κόµβων του αριστερού υποδένδρου του κόµβου. (i) Η κύρια ιδέα είναι η εξής: αν ο κόµβος x είναι µεγαλύτερος από i-1 στοιχεία τότε είναι το i-οστό στοιχείο του δένδρου, άρα επιστρέφουµε το στοιχείο x.data. ιαφορετικά, (1) αν είναι µεγαλύτερος ή ίσος από λιγότερα των i στοιχείων, έστω j στοιχεία, τότε πρέπει να βρούµε το (i-(j+1))-οστό µεγαλύτερο στοιχείο του δεξιού υποδένδρου του κόµβου x, και, τέλος, (2) αν είναι µεγαλύτερος ή ίσος από περισσότερα από i-1 (έστω j), τότε πρέπει να βρούµε το i-οστό στοιχείο του αριστερού υποδένδρου. int Find(avlnode *t, int i) avlnode *q = t; (ii) while (i!= q->greater+1 && q!= null) if (q->greater < i) i = i - q->greater+1; q = q->right; if (q->greater > i) q = q->left if (q==null) return error return q->data; int Size(avlnode *t) avlnode *q = t; int size = 0; while (q!= null) size = q->greater + 1 + size; q = q->right; return size; 3
Άσκηση 4 Παρατηρούµε ότι δοθέντος ενός radix-δένδρου που περιέχει τα στοιχεία µας µπορούµε να επιστρέψουµε τη λίστα των στοιχείων ταξινοµηµένη εφαρµόζοντας µια προθεµατική διερεύνηση στο δένδρο. Μια τέτοια διερεύνηση παίρνει χρόνο Θ(n), όπου n είναι ο αριθµός των κόµβων του δένδρου, ενώ επίσης παρατηρούµε (και µπορούµε εύκολα να αποδείξουµε) ότι ο αριθµός κόµβων του δένδρου είναι του αθροίσµατος των ψηφίων που αποθηκεύει. ηλαδή, το βήµα της προθεµατικής διερεύνησης του δένδρου είναι της τάξης Ο(n) όπου n είναι το άθροισµα των ψηφίων που αποθηκεύει. Παραµένει να µελετήσουµε πως µπορούµε να κτίσουµε το radix-δένδρο που αντιστοιχεί σε ένα σύνολο δυαδικών αριθµών. Χρησιµοποιούµε τη γνωστή υλοποίηση δυαδικών δένδρων, δηλαδή, υποθέτουµε ότι οι κόµβοι του δένδρου είναι υλοποιηµένοι ως εγγραφές µε τρία πεδία, και ένα δένδρο ως δείκτης στη ρίζα του: struct Node string word; Node *left; Node *right; struct radix Node *start Οι διαδικασίες MakeEmpty(t) και Ιnsert(t, A, n) (η οποία εισαγάγει στο δένδρο t την συµβολοσειρά A, η οποία περιέχει n ψηφία) έχουν ως εξής: MakeEmpty(struct radix t) t->start = null Insert(radix t, string A, int n) if (t->start == NULL) s = malloc(sizeof(struct Node)); s->word = s->left = s->right = NULL; t->start = s; i=1; s = t->start; while (i n) if (A[i]==0) if (s->left == null) p = malloc(sizeof(struct Node)); p->word = p->left = p->right = NULL; s->left = p; s = p->left; if (s->right == null) p = malloc(sizeof(struct Node)); p->word = p->left = p->right = NULL; s->right = p; 4
s = p->right; i++ s->word = A; H διαδικασία Insert ξεκινώντας από τη ρίζα του δένδρου προχωρά µέσα στο δένδρο δεξιά ή αριστερά ανάλογα µε τον αριθµό που θέλει να προσθέσει, δηµιουργώντας καινούριους κόµβους κατάλληλα. Σε ενδιάµεσους νέους κόµβους αποθηκεύει δείκτες NULL που δηλώνουν ότι οι κόµβοι είναι άδειοι, ενώ όταν φτάσει στον κατάλληλο κόµβο εισάγει την συµβολοσειρά, Α. Παρατηρούµε ότι ο χρόνος εκτέλεσης της Ιnsert(t, A, n) είναι της τάξης Θ(n) όπου n είναι ο αριθµός των ψηφίων της συµβολοσειράς που εισάγεται. Συνεπώς ο αλγόριθµος ταξινόµησης αποτελείται από τα εξής βήµατα: (α) δηµιούργησε ένα άδειο radix-δένδρο, (β) εισήγαγε σε αυτό όλες τις συµβολοσειρές του S και (γ) εκτέλεσε προθεµατική διερεύνηση στο δένδρο (τυπώνοντας όλα τις συµβολοσειρές του δένδρου). Αφού το βήµα (α) είναι τάξης Ο(1), το βήµα (β) τάξης Θ(n) και το βήµα (γ) τάξης Ο(n), συνολικά ο προτεινόµενος αλγόριθµός έχει χρόνο εκτέλεσης Θ(n) όπως χρειάζεται. 5