Λίστες Λίστες - Απλά Συνδεδεμένες Λίστες - Διπλά Συνδεδεμένες Λίστες
Είδη Γραμμικών Λιστών Σειριακή Λίστα Καταλαμβάνει συνεχόμενες θέσεις κύριας μνήμης Συνδεδεμένη Λίστα Οι κόμβοι βρίσκονται σε απομακρυσμένες θέσεις, συνδεδεμένες όμως μεταξύ τους με δείκτες Στατικές Λίστες Ο μέγιστος αριθμός στοιχείων είναι εξ αρχής γνωστός (υλοποίηση με σειριακές λίστες) Δυναμικές Λίστες Ο μέγιστος αριθμός στοιχείων δεν είναι γνωστός. Επιτρέπεται η επέκταση ή και συρρίκνωση της λίστας κατά την εκτέλεση του προγράμματος (υλοποίηση με συνδεδεμένες λίστες)
Μονά Διασυνδεδεμένη Λίστα Μία μονά διασυνδεδεμένη λίστα είναι μία συμπαγής δομή δεδομένων που αποτελείται από μία ακολουθία κόμβων Κάθε κόμβος περιέχει στοιχείο σύνδεσμο προς τον επόμενο κόμβο στοιχείο επόμενο κόμβος A B C D
Θέση (Position) ΑΔΤ Τα Position ADT μοντέλα μοντελοποιούν την έννοια της θέσης μέσα σε μια δομή δεδομένων όπου ένα μοναδικό αντικείμενο αποθηκεύεται Η ειδική θέση null αναφέρεται σε απουσία αντικειμένου Οι θέσεις παρέχουν μια ενοποιημένη αναπαράσταση των διαφορετικών τρόπων αποθήκευσης δεδομένων, όπως το κελί ενός πίνακα ο κόμβος μιας διασυνδεδεμένης λίστας Συναρτήσεις: Object& element(): επιστρέφει το στοιχείο που είναι αποθηκευμένο στην θέση αυτή bool isnull(): επιστρέφει αλήθεια αν είναι null θέση
Λίστα ΑΔΤ Το μοντέλο μιας Λίστα ΑΔΤ μοντελοποιεί μια ακολουθία από θέσεις που αποθηκεύουν αυθαίρετα αντικείμενα Εγκαθιστά μία πριν/μετά σχέση μεταξύ των θέσεων Μέθοδοι ερωτήσεων: isfirst(p), islast(p) Μέθοδοι πρόσβασης: first(), last() before(p), after(p) Μέθοδοι ενημέρωσης: replaceelement(p, o), swapelements(p, q) insertbefore(p, o), insertafter(p, o), insertfirst(o), insertlast(o) remove(p)
Γραμμικές Λίστες (linear lists) Μια γραμμική λίστα είναι μια πεπερασμένη, ταξινομημένη ακολουθία δεδομένων Βασική αρχή: Τα στοιχεία της λίστας έχουν μια θέση μέσα στην ακολουθία (1 ο,2 ο,3 ο κοκ.) Συμβολισμός: <e 1, e 2,,e n > Ποιες λειτουργίες πρέπει να υλοποιήσουμε: Δημιουργία μιας γραμμικής λίστας Καθορισμός του αν η λίστα είναι άδεια, καθορισμός του του μήκους της λίστας Εύρεση του k-οστού στοιχείου, διαγραφή του k-οστού στοιχείου, εισαγωγή νέου στοιχείου ακριβώς μετά το k-οστό Αναζήτηση ενός στοιχείου x μέσα στη λίστα
Αρχικοποίηση listptr addr of list count 0 headptr NULL void initializelist(list* listptr) { listptr->headptr = NULL; listptr->count = 0; }
Τοποθέτηση σε Συγκεκριμένη Θέση Έλεγχος αν η θέση (position) είναι μέσα στα όρια. Αρχίζουμε από τον κόμβο head Θέτουμε index =0 while index < position Ακολουθούμε το δείκτη next Αυξάνουμε τη μεταβλητή index Επιστρέφουμε τον κόμβο
Τοποθέτηση σε Συγκεκριμένη Θέση headptr 0 1 2 0x2030 0x30a8 2 position 0 i nodeptr
Τοποθέτηση σε Συγκεκριμένη Θέση headptr 0 1 2 0x2030 0x30a8 2 position 1 i 0x2030 nodeptr
Τοποθέτηση σε Συγκεκριμένη Θέση headptr 0 1 2 0x2030 0x30a8 2 position 2 i 0x30a8 nodeptr
Εισαγωγή headptr Εισαγωγή στη θέση 0. 0 1 2 headptr 0 1 2 3 newnodeptr
Εισαγωγή στη θέση 1. headptr 0 1 2 newnodeptr headptr 0 2 3 newnodeptr 1
Εισαγωγή στη Αρχή headptr 0x2008 0x30a8 0x2000 newnodeptr position 0x2000 0
Εισαγωγή στην Αρχή headptr 0x2008 0x30a8 0x2000 newnodeptr position 0x2000 0
Εισαγωγή στην Αρχή headptr 0x2008 0x30a8 0x2000 newnodeptr position 0x2000 0
Εισαγωγή Ενδιάμεσα headptr 0x3050 prevptr newnodeptr 0x2000 0x2000 position 1
Εισαγωγή Ενδιάμεσα headptr 0x3050 prevptr newnodeptr 0x2000 0x2000 position 1
Εισαγωγή Ενδιάμεσα headptr 0x3050 prevptr newnodeptr 0x2000 0x2000 position 1
Διαγραφή headptr 0 1 Διαγραφή θέσης 0. 2 3 0 1 2 headptr
headptr Διαγραφή θέσης 2. 0 headptr 1 2 3 0 1 2
Διαγραφή 1ου Κόμβου headptr 0x2030 0x30a8 0 position oldnodeptr
Διαγραφή 1ου Κόμβου 0x2030 0x30a8 0 position headptr oldnodeptr
Διαγραφή 1ου Κόμβου 0x2030 0x30a8 0 position headptr oldnodeptr
Διαγραφή Ενδιάμεσου Κόμβου headptr 0x2030 0x30a8 1 position 0x2030 0x2030 prevptr oldnodeptr
Διαγραφή Ενδιάμεσου Κόμβου headptr 0x2030 0x30a8 1 position 0x2030 0x2030 prevptr oldnodeptr
Διαγραφή Ενδιάμεσου Κόμβου headptr 0x30a8 1 position 0x2030 0x2030 prevptr oldnodeptr
Παράδειγμα Ο ΑΤ λίστα ορίζεται ως µια ακολουθία στοιχείων συνοδευόµενη από πράξεις που επιτρέπουν εισαγωγή και εξαγωγή στοιχείων σε οποιαδήποτε θέση της λίστας. Να υλοποιήσετε τις πιο κάτω βασικές πράξεις λίστας: a)access(l, i) επέστρεψε το i-οστό στοιχείο της L b)insert_after(l, x, i) εισήγαγε το x µετά από το i-οστό στοιχείο της L. c)delete(l, i) αφαίρεσε το i-οστό στοιχείο της L Να εξηγήσετε µε σαφήνεια πως µπορούµε να υλοποιήσουµε αυτό τον ΑΤ χρησιµοποιώντας a)(ι) πίνακες και b)(ιι) συνδεδεµένες λίστες. Συγκεκριµένα, να γράψετε µια καθαρή περιγραφή των µεταβλητών που χρειάζονται για τη δοµή και την υλοποίηση των πράξεων σε ψευδοκώδικα. Να υπολογίσετε το χρόνο εκτέλεσης των διαδικασιών και να συγκρίνετε τις δύο υλοποιήσεις.
Ο ζητούµενος ΑΤ µπορεί να υλοποιηθεί ως εξής: ιαδοχική χορήγηση µνήµης (Πίνακας) Υποθέτουµε ότι οι λίστες µας έχουν µέγιστο µέγεθος max και χρησιµοποιούµε τη δοµή typedef struct List{ type elements[max]; int size; } list; Οι διαδικασίες υλοποιούνται ως εξής: type Access (list *L, int i){ if (i< L->size) return L[i-1]; } Χρόνος Εκτέλεσης: O(1) InsertAfter(list *L, int i, type x){ if ( L->size < max AND i < L->size ){ for ( k = L->size; k > i; k++) L->elements[k] = L->elements[k-1]; L->elements[i] = x; L->size++; } Χρόνος Εκτέλεσης: n i O(n), όπου n είναι το µέγεθος της λίστας. Delete(list *L, int i){ if (i < L->size ){ for ( k = i-1; k < L->size; k++) L->elements[k] = L->elements[k+1]; L->size--; } Χρόνος Εκτέλεσης: n i ε O(n), όπου n είναι το µέγεθος της λίστας.
Συνδετική χορήγηση µνήµης Χρησιµοποιούµε τις πιο κάτω δοµές typedef struct Node{ type data; struct node *next; } node; typedef struct List{ node *top; int size; } list; Οι διαδικασίες υλοποιούνται ως εξής: type Access (list *L, int i){ if i> L->size return; p = L->top; for (j=0; j < i; j++) p= p->next; return p->data; } Χρόνος Εκτέλεσης: i, O(n), όπου n είναι το µέγεθος της λίστας. InsertAfter(list *L, int i, type x){ if (i > L->size) return; p = L->top; for (j=0; j < i; j++) p= p->next; q = (node *) malloc (sizeof(node)); q->data = x; q->next = p->next; p->next = q; } Χρόνος Εκτέλεσης: i ε O(n), όπου n είναι το µέγεθος της λίστας. Delete(list *L, int i){ node * ptr; if (i > L->size) return; p = L->top; for (j=0; j < i; j++) p= p->next; ptr = p->next; p->next = p->next->next; free(ptr); } Χρόνος Εκτέλεσης: i εo(n), όπου n είναι το µέγεθος της λίστας.
Σύγκριση Η υλοποίηση του ΑΤ µε διαδοχική χορήγηση µνήµης αποτελείται από απλούστερες προγραµµατιστικά διαδικασίες και από αυτές η Access είναι αποδοτικότερη από την αντίστοιχη διαδικασία της υλοποίησης µε δυναµική δέσµευση µνήµης. Το βασικό πλεονέκτηµα της υλοποίησης µε δυναµική δέσµευση µνήµης είναι ότι δεν επιβάλλει περιορισµούς σχετικά µε το πλήθος των στοιχείων της λίστας, και ανά πάσα στιγµή χρησιµοποιεί µνήµη ανάλογη µε το πλήθος των στοιχείων της λίστας. Αντίθετα η υλοποίηση µε πίνακα δεσµεύει µνήµη για τις max θέσεις του πίνακα καθ όλη τη διάρκεια της εκτέλεσης του προγράµµατος, άσχετα µε το πλήθος των στοιχείων της λίστας. Παρατηρούµε όµως, πως στην υλοποίηση µε δυναµική δέσµευση µνήµης χρειάζεται να αποθηκεύουµε και µνήµη για του δείκτες next κάθε κόµβου της λίστας.
Γραμμική Λίστα με Πίνακα class LinearList { public: LinearList(int MaxListSize = 10); // constructor ~LinearList() {delete [] element;} // destructor bool IsEmpty() const {return length == 0;} int Length() const {return length;} bool Find(int k, T& x) const; // return the k'th element of list in x int Search(const T& x) const; // return position of x LinearList<T>& Delete(int k, T& x); // delete k'th element and return in x LinearList<T>& Insert(int k, const T& x); // insert x just after k'th element void Output(ostream& out) const; private: int length; int MaxSize; T *element; // dynamic 1D array };
Γραμμική Συνδεδεμένη Λίστα class Chain { public: Chain() {first = 0;} ~Chain(); bool IsEmpty() const {return first == 0;} int Length() const; class ChainNode { T data; ChainNode<T> *link; }; bool Find(int k, T& x) const; int Search(const T& x) const; Chain<T>& Delete(int k, T& x); Chain<T>& Insert(int k, const T& x); void Output(ostream& out) const; private: ChainNode<T> *first; // pointer to first node };
Διπλά Διασυνδεδεμένη Λίστα Μία διπλά διασυνδεδεμένη λίστα παρέχει μια φυσική υλοποίηση της Λίστας ΑΔΤ Οι κόμβοι υλοποιούν την θέση και περιέχουν: στοιχείο σύνδεσμο στον προηγούμενο κόμβο σύνδεσμο στον επόμενο κόμβο Ειδικοί κόμβοι header και trailer header prev elem κόμβοι/θέσεις next κόμβος trailer στοιχεία
Διπλά Συνδεδεμένη Λίστα Επίσης γραμμικές διατάξεις Δυο σύνδεσμοι σε κάθε κόμβο, προς τον επόμενο και προς τον προηγούμενο Γενική μορφή, π.χ. για υλοποίηση ουράς: first last
Διπλά Συνδεδεμένη Λίστα Πήγαινε σε συγκεκριμένη θέση Εισαγωγή σε συγκεκριμένη θέση Διαγραφή από συγκεκριμένη θέση Ανάγνωση δεδομένων συγκεκριμένης θέσης Αντικατάσταση Διάσχιση λίστας και στις δύο κατευθύνσεις.
Διπλά Συνδεδεμένη Λίστα 0 1 2 3 4 currentptr
struct DoubleLinkNodeRec { float value; struct DoubleLinkNodeRec* nextptr; struct DoubleLinkNodeRec* previousptr; }; typedef struct DoubleLinkNodeRec Node; struct DoubleLinkListRec { int count; Node* currentptr; int position; }; typedef struct DoubleLinkListRec DoubleLinkList;
Εισαγωγή στο Τέλος 0x2030 0x2000 0x2030 NULL NULL NULL NULL currentptr 0x2030 prevptr newnodeptr 0x2000
Εισαγωγή στο Τέλος 0x2030 0x2000 0x2030 0x2000 NULL NULL NULL currentptr 0x2030 prevptr newnodeptr 0x2000
Εισαγωγή στο Τέλος 0x2030 0x2000 0x2030 0x2000 NULL NULL 0x2030 currentptr 0x2030 prevptr newnodeptr 0x2000
Εισαγωγή Ενδιάμεσα 0x2030 NULL currentptr 0x2030 prevptr 0x2000 NULL NULL NULL 0x2000 newnodeptr
Εισαγωγή Ενδιάμεσα 0x2030 NULL currentptr 0x2030 prevptr 0x2000 2030 NULL NULL 0x2000 newnodeptr
Εισαγωγή Ενδιάμεσα 0x2030 NULL currentptr 0x2030 prevptr 0x2000 2030 3080 NULL 0x2000 newnodeptr
Εισαγωγή Ενδιάμεσα 0x2030 NULL currentptr 0x2030 prevptr 0x2000 2030 3080 NULL 0x2000 0x2000 newnodeptr
Εισαγωγή Ενδιάμεσα 0x2030 NULL currentptr 0x2000 prevptr 0x2000 2030 3080 NULL 0x2000 0x2000 newnodeptr
Διαγραφή από το Τέλος 0x2030 0x2030 NULL NULL currentptr oldnodeptr 0x2030 prevptr
Διαγραφή από το Τέλος 0x2030 NULL NULL NULL currentptr oldnodeptr 0x2030 prevptr
Διαγραφή από το Τέλος NULL NULL currentptr oldnodeptr 0x2030 prevptr
Διαγραφή Ενδιάμεσου 0x2030 0x2030 NULL NULL currentptr oldnodeptr prevptr
Διαγραφή Ενδιάμεσου 0x2030 0x2030 0x2030 NULL NULL currentptr oldnodeptr prevptr
Διαγραφή Ενδιάμεσου 0x2030 0x2030 0x2030 NULL NULL currentptr oldnodeptr prevptr
Διαγραφή Ενδιάμεσου 0x2030 0x2030 NULL NULL currentptr oldnodeptr prevptr
Απόδοση Υλοποιώντας μία List ADT με διπλά διασυνδεδεμένη λίστα Ο χώρος που χρησιμοποιείται από μία λίστα με n στοιχεία είναι O(n) Ο χώρος που χρησιμοποιείται από κάθε θέση της λίστας είναι O(1) Όλες οι λειτουργίες της List ADT εκτελούνται σε χρόνο O(1) Η διεργασία element() της Position ADT εκτελείται σε χρόνο O(1)
Παράδειγμα Να γράψετε µία αναδροµική και µία µη-αναδροµική διαδικασία InsertAfter(L, x, i) οι οποίες να παίρνουν ως δεδοµένο εισόδου µια κυκλική διπλά συνδεδεµένη λίστα L, και να εισάγουν το στοιχείο x µετά από το i-οστό στοιχείο της λίστας. Να συγκρίνετε τις δύο διαδικασίες ως προς τον χρόνο εκτέλεσής τους και τον χώρο που χρησιµοποιούν. Υποθέτουµε πως οι λίστες είναι υλοποιηµένες χρησιµοποιώντας τις πιο κάτω δοµές. typedef struct Node{ type data; struct node *next; struct node *prev; } node; typedef struct List{ node *head; } list; Υποθέτουµε ότιτοπεδίοtop της λίστας δείχνει το πρώτο στοιχείο της λίστας. Επίσης υποθέτουµε ότιαντοi είναι µεγαλύτερο από το µέγεθος της λίστας, τότε το στοιχείο προς εισαγωγή εισάγεται στην τελευταία θέση της λίστας.
Μη-αναδροµική εκδοχή InsertAfter(list *L, type x, int i){ p = (node *) malloc (sizeof(node)); p->data = x; q = L->top; if (q == NULL) p->next = p; p->prev = p; L->top = p; return; if (i == 0) L->top = p; j = 1; while (j<=i AND q->next!= L->top) q = q->next; j++; p->next = q->next; p->next)->prev = p; q->next = p; p->prev = q; }
Aναδροµική εκδοχή RecInsertAfter (list *L, type x, int i){ if (L->top == NULL) p = (node *) malloc (sizeof(node)); p->data = x; p->next = p; p->prev = p; L->top = p; elseif (i == 0) p = (node *) malloc (sizeof(node)); p->data = x; p->next = L->top; p->prev = L->top->prev; p->next->prev = p; p->prev->next = p L->top = p; else RecInsAfter(L, L->top, x, i); } RecInsAfter(list *L, node *q, type x, int i){ if (i == 0 OR q->next == L->top) p = (node *) malloc (sizeof(node)); p->data = x; p->next = q- >next; (p->next)->prev = p; q->next = p; p->prev = q; else RecInsAfter(L, q- >next, i-1) }
Σύγκριση Προφανώς και οι δύο αλγόριθµοι είναι της τάξης Ο(n), όπου n είναι ο αριθµός κόµβων της λίστας. O αναδροµικός αλγόριθµος είναι όµως λιγότερο αποδοτικός από άποψη χώρου (και χρόνου). Αυτό οφείλεται στο ότι για κάθε αναδροµική κλήση της διαδικασίας δεσµεύεται επιπλέον χώρος για τις παραµέτρους και τις τοπικές µεταβλητές των κλήσεων. Κατά συνέπεια, κατά την επεξεργασία του τελευταίου κόµβου της λίστας θα υπάρχουν σε κατάσταση αναµονής n 1 κλήσεις (όπου n είναι το µέγεθος της λίστας) που αφορούν όλους τους προηγούµενους κόµβους και για κάθε κλήση θα υπάρχει δεσµευµένη µνήµη για όλες τις παραµέτρους, τοπικές µεταβλητές, κλπ.