Κατ οίκον Εργασία 2 Σκελετοί Λύσεων Άσκηση 1 Υπάρχουν διάφοροι τρόποι για να υλοποιήσουμε πράξεις ουράς για την προτεινόμενη εγγραφή. To πρόβλημα που δημιουργείται με οποιαδήποτε από αυτές είναι ότι είναι αδύνατο να ξεχωρίσουμε την άδεια ουρά από αυτήν που έχει n στοιχεία: Για παράδειγμα αν θέσουμε αρχικές τιμές των πεδίων (*q).front, και (*q).back το 0, τότε και οι δύο τύποι ουρών (η άδεια και η γεμάτη) ικανοποιούν την συνθήκη (*q).front = (*q).back, και καμιά άλλη συνθήκη με βάση την προτεινόμενη υλοποίηση δεν μας επιτρέπει να διακρίνουμε την άδεια ή τη γεμάτη ουρά. Εντούτοις με βάση αυτή την πρόταση μπορούμε να υλοποιήσουμε ουρές που περιέχουν το πολύ n-1 στοιχεία. Με αυτό το τρόπο η άδεια ουρά διακρίνεται από τη συνθήκη (*q).front == (*q).back ενώ η γεμάτη ουρά από τη συνθήκη (*q).βack 1 = (*q).front Οι διαδικασίες ουράς μπορούν να οριστούν εύκολα. Μειονέκτημα της πρόταση είναι ότι αποθηκεύονται το πολύ n-1 στοιχεία, δηλαδή, ένα λιγότερο από τη χωρητικότητα της λίστας. Άσκηση 2 Ο ζητούμενος ΑΤΔ μπορεί να υλοποιηθεί ως μια ακολουθία από στοιχεία τύπου job, συνοδευόμενη από τις πράξεις: NewJob (L, j) η οποία τοποθετεί την εργασία j στο τέλος της Process(L) Kill(L, j) MakeUrgent(L, j) ακολουθίας L η οποία επεξεργάζεται την εργασία που βρίσκεται στην κορυφή της L η οποία διαγράφει την εργασία j από τη λίστα L η οποία τοποθετεί την εργασία j στην κορυφή της λίστας L Παραθέτουμε την υλοποίηση του ΑΤΔ με δυναμική χορήγηση μνήμης. Αρχικά παρατηρούμε ότι για επεξεργασία των εργασιών είναι απαραίτητο να γνωρίζουμε για κάθε εργασία πόσος χρόνος εκτέλεσης απομένει μέχρι τη συμπλήρωσή της. Επομένως, υποθέτουμε ότι ο τύπος job περιέχει πεδίο time στο οποίο φυλάγεται αυτός ο χρόνος ως εξής: typedef struct Job{ int time; job; 1
Παρατηρούμε ότι για υλοποίηση της λίστας εργασιών χρειαζόμαστε ΑΤΔ όμοιο με τον ΑΤΔ ουρά: η διαδικασία NewJob ουσιαστικά υλοποιεί εισαγωγή στο τέλος της ακολουθίας ενώ η διαδικασία Process, υλοποιεί εξαγωγή από την αρχή και εισαγωγή στο τέλος. Για υλοποίηση ουρά χρησιμοποιούμε τις πιο κάτω δομές typedef struct Node{ typedef struct List{ job *data; node *front; struct node *next; node *back; node; int size; list; size front back Οι ζητούμενες διαδικασίες υλοποιούνται ως εξής: NewJob (list *L, job x){ r = (node *) malloc (sizeof(node)); r->data = x; if (L->size == 0) L->top = L->back = r; (L->back)->next = r L->back = r; L->size++; Χρόνος Εκτέλεσης: Θ(1) /*όπου οι διαδικασίες =, == και!= σε στοιχεία τύπου job είναι κατάλληλα γραμμένες ώστε να συγκρίνουν τα πεδία των στοιχείων */ Process (list *L){ p = L->front; if (p!= NULL) (p->data)->time = (p->data)->time 2; L->front = p->next; if ((p->data)->time == 0 ) if (p == L->back) L->back = NULL; L->size--; free(p); if ((p->data)->time > 0 AND p!= L->back) (L->back)->next = p; L->back = p; Χρόνος Εκτέλεσης: Θ(1) 2
Kill (list *L, job x){ if (L->size == 0) return; % η λίστα είναι κενή if (L->front)->data == x p = L->front; L->front = p->next; free(p); L->size--; p = FindPrev(L, x); if ( p!= NULL) q = p->next; p->next = q->next; if (L->back == q) L->back = p; free(q); L->size--; Η βοηθητική διαδικασίας FindPrev ορίζεται ως εξής: node *FindPrev (list *L, job x){ if (L->size == 0 OR (L->front)->data == x ) return NULL; p = L->front; while((p->next)->data!= x AND p->next!= NULL) p = p->next; if ((p->next)!= NULL) return p; return NULL; Χρόνος Εκτέλεσης: O(n), όπου n είναι το μήκος της λίστας L. (Πρέπει να εντοπίσουμε το στοιχείο x). MakeUrgent (list *L, job x){ p = FindPrev(L, x); if p == NULL return q = p->next; p->next = q->next; q->next = L->front; L->front = q; if (L->back == q) L->back = p; Χρόνος Εκτέλεσης: O(n), όπου n είναι το μήκος της λίστας L. (Πρέπει να εντοπίσουμε το στοιχείο x). 3
Άσκηση 3 (α) Για την υλοποίηση της διπλά συνδεδεμένης λίστας θα χρησιμοποιήσουμε τις πιο κάτω δομές: Ένας κόμβος ορίζεται από το πιο κάτω structure: typedef struct dlnode { int data; struct dlnode *prev; struct dlnode *next; DLNODE; O κόμβος που ορίζει τη διπλά συνδεδεμένη λίστα είναι: typedef struct dllist { DLNODE *top; DLLIST; Η συνάρτηση merge παίρνει ως παραμέτρους δύο διπλά συνδεδεμένες λίστες και καλεί την αναδρομική συνάρτηση rmerge για να ενωθούν οι δύο λίστες και να δημιουργηθεί μια τρίτη λίστα, σύμφωνα με την εκφώνηση της άσκησης Ακολουθεί η ζητούμενη υλοποίηση. DLLIST *merge(dllist * list1, DLLIST * list1){ DLNODE *new_list; new_list = (DLLIST *) malloc (sizeof(dllist)); if ((list1->top == NULL) && (list2->top == NULL)) new_list->top = NULL; new_list->top = rmerge(list1->top, list2->top, list1->top, list2->top); return new_list; Η αναδρομική συνάρτηση rmerge εκτελείται μέχρι να φτάσουμε στο τέλος των δύο λιστών. Όταν μια από τις δύο λίστες φτάσει στο τέλος της προσθέτονται τα στοιχεία της άλλης μέχρι να φτάσει και αυτή στο τέλος της, διαφορετικά σε κάθε κλήση προσθέτονται δύο νέοι κόμβοι, ένας από κάθε αρχική λίστα. Οι δύο τελευταίες παράμετροι χρησιμοποιούνται για να ξέρουμε ότι φτάσαμε στην αρχή της κάθε λίστας. DLNODE *rmerge(dlnode *list1, DLNODE *list2, DLNODE *clist1, DLNODE *clist2){ // Ο κόμβος που θα δημιουργήσουμε DLNODE *new_node; // Αν έχουμε φτάσει στο τέλος των δύο λιστών τότε τερματίζουμε if ((list1 == clist1) && (list2 == clist2)) return NULL; // Αν δεν έχουμε φτάσει στο τέλος της πρώτης λίστας προσθέτουμε 4
// τον επόμενο κόμβο της στη νέα λίστα και καλούμε αναδρομικά τη // διαδικασία αντιστρέφοντας τους ρόλους των δύο λιστών εφόσον η // δεύτερη λίστα έχει ακόμη στοιχεία if(list1!= clist1) new_node = (DLNODE *) malloc (sizeof(dlnode)); new_node->data = list1->data; if (list2!= clist2) new_node->next = rmerge(list2, list1->next, clist2,clist1); new_node->next = rmerge(list1->next, list2, clist1,clist2); return newnode; // Διαφορετικά, αν έχουμε εξαντλήσει τα στοιχεία της πρώτης λίστας // προσθέτουμε τον επόμενο κόμβο της δεύτερης λίστας στη νέα λίστα // και καλούμε αναδρομικά τη διαδικασία αντιστρέφοντας τους ρόλους // των δύο λιστών new_node = (DLNODE *) malloc (sizeof(dlnode)); new_node->data = list2->data; new_node->next = rmerge(list2->next, list1,clist2,clist1) return newnode; Χρονική πολυπλοκότητα: Ο(n+m) όπου n και m τα μήκη των δύο λίστών. (β) Για την υλοποίηση της διπλά συνδεδεμένης λίστας με κεφαλή θα χρησιμοποιήσουμε τις πιο κάτω δομές: Ένας κόμβος ορίζεται από το πιο κάτω structure: typedef struct dlnode { int data; struct dlnode *prev; struct dlnode *next; DLNODE; O κόμβος που ορίζει τη διπλά συνδεδεμένη λίστα είναι: typedef struct dllist { DLNODE *head; DLLIST; Κατ αρχή, η διαδικασία μας εντοπίζει τα δύο άκρα της λίστας και στη συνέχεια καλεί μια δεύτερη, αναδρομική συνάρτηση η οποία αναλαμβάνει να αποφασίσει κατά πόσο η λέξη που σχηματίζεται στη λίστα είναι παλίνδρομο. int *palindrome(list *list) { start = (list->head)->next if (start == NULL) return 1; end == start; 5
while (end->next!= NULL) end = end->next; return rpalindrome(start, end); Σε κάθε αναδρομική κλήση της συνάρτησης rpalindrome τα στοιχεία των δύο θέσεων της λίστας. Η συνάρτηση εκτελείται μέχρι τα στοιχεία αυτά να μην είναι τα ίδια, όπου επιστρέφει 0 (η λέξη δεν είναι παλίνδρομο) ή μέχρι οι δύο θέσεις που κοιτάζουμε να συμπίπτουν, ή τα στοιχεία της λίστας μας να έχουν εξαντληθεί. Στις δύο τελευταίες περιπτώσεις η λέξη είναι παλίνδρομο και επιστρέφουμε την τιμή 1. int rpalindrome(node *start, NODE *end); { // Aν τα στοιχεία των δύο θέσεων διαφέρουν επιστρέφουμε ότι η // λέξη δεν είναι παλίνδρομο if (start->data!= end->data) return 0; // Διαφορετικά, αν οι κόμβοι της λίστας έχουν εξαντληθεί // επιστρέφουμε ότι η λέξη είναι παλίνδρομο elsif (start == end OR start->next == end) return 1 // Διαφορετικά, προχωρούμε στους επόμενους κόμβους της λίστας rpalindrome(start->next, end->prev) Χρονική πολυπλοκότητα: Ο(n) όπου n το μήκος της λίστας. 6