You are on page 1of 176

ΤΕΧΝΟΛΟΓΙΚΟ ΕΚΠΑΙΔΕΥΤΙΚΟ ΙΔΡΥΜΑ ΜΕΣΟΛΟΓΓΙΟΥ

ΠΑΡΑΡΤΗΜΑ ΝΑΥΠΑΚΤΟΥ
ΤΜΗΜΑ ΤΗΛΕΠΙΚΟΙΝΩΝΙΑΚΩΝ ΣΥΣΤΗΜΑΤΩΝ & ΔΙΚΤΥΩΝ

ΣΗΜΕΙΩΣΕΙΣ ΜΑΘΗΜΑΤΟΣ

ΑΛΓΟΡΙΘΜΟΙ ΚΑΙ ΔΟΜΕΣ ΔΕΔΟΜΕΝΩΝ

Β’ ΕΞΑΜΗΝΟ ΣΠΟΥΔΩΝ

Επιμέλεια:

Δημήτρης Σοφοτάσιος
Ιωάννης Τσακνάκης

Δρ Μηχανικοί Η/Υ και Πληροφορικής

Ναύπακτος, Σεπτέμβριος 2009


Αλγόριθμοι και Δομές Δεδομένων

ΠΕΡΙΕΧΟΜΕΝΑ
ΠΡΟΛΟΓΟΣ ........................................................................................................... 6

ΚΑΤΑΛΟΓΟΣ ΣΧΗΜΑΤΩΝ ................................................................................... 8

ΚΕΦΑΛΑΙΟ 1: ΑΛΓΟΡΙΘΜΟΙ ΚΑΙ ΔΟΜΕΣ ΔΕΔΟΜΕΝΩΝ ............................... 10


1.1 Εισαγωγή.................................................................................................................. 10
1.2 Ορισμός ενός προβλήματος .................................................................................... 10
1.3 Επίλυση προβλημάτων – Ανάπτυξη αλγόριθμων ................................................... 11
1.3.1 Η έννοια του αλγόριθμου ........................................................................................... 11
1.3.2 Αναπαράσταση αλγόριθμων ..................................................................................... 12
1.3.3 Μορφή ακολουθιών εντολών στους αλγόριθμους...................................................... 14
1.3.4 Παράδειγμα ανάπτυξης αλγόριθμου .......................................................................... 14
1.4 Ανάλυση Αλγόριθμων .............................................................................................. 17
1.4.1 Υπολογιστική πολυπλοκότητα αλγόριθμων............................................................... 17
1.4.2 Moντέλα υπολογισμού ............................................................................................... 18
1.4.3 Ασυμπτωτικοί συμβολισμοί ...................................................................................... 19
1.4.4 Τύποι πολυπλοκότητας.............................................................................................. 23
1.4.5 Είδη αλγόριθμων ....................................................................................................... 23
1.5 Δομές Δεδομένων.................................................................................................... 24
1.5.1 Η έννοια της δομής δεδομένων .................................................................................. 24
1.5.2 Παραδείγματα βασικών δομών δεδομένων ............................................................... 25
Πίνακας (Array) ...........................................................................................................................25
Εγγραφή (Structure) ....................................................................................................................26
Στοίβα (Stack)..............................................................................................................................26
Ουρά (Queue) .............................................................................................................................26
Γραμμική Λίστα (Linear List)........................................................................................................26
Ουρά Προτεραιότητας (Priority Queue) .......................................................................................27
Δένδρο (Tree) ..............................................................................................................................27
Γράφος (Graph) ...........................................................................................................................27
Δομή UNION-FIND ......................................................................................................................27
Ερωτήσεις Κεφαλαίου .................................................................................................... 28

ΚΕΦΑΛΑΙΟ 2: ΓΡΑΜΜΙΚΕΣ ΔΟΜΕΣ ΔΕΔΟΜΕΝΩΝ ........................................ 29


2.1 Πίνακες (Αrrays) ....................................................................................................... 29
2.1.1 Μερικοί τύποι πινάκων .............................................................................................. 30
2.1.2 Συμβολοσειρές........................................................................................................... 32
2.1.3 Σάρωση στοιχείων πίνακα......................................................................................... 33
2.2 Στοίβες (Stacks)........................................................................................................ 34
2.2.1 Υλοποίηση στοίβας ................................................................................................... 36
Η πράξη Push() ...........................................................................................................................36
Η πράξη Pop() .............................................................................................................................37
2.2.2 Εφαρμογές στοίβας ................................................................................................... 38
Yλοποίηση αναδρομής................................................................................................................38

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 2


Αλγόριθμοι και Δομές Δεδομένων

Πολωνικός συμβολισμός .............................................................................................................40


2.3 Ουρές (Queues) ....................................................................................................... 43
2.3.1 Μέθοδοι υλοποίησης ουράς ...................................................................................... 45
Υλοποίηση ουράς pipeline με χρήση πινάκων............................................................................45
Υλοποίηση ουράς δακτυλίου.......................................................................................................49
2.4 Λίστες (Lists)............................................................................................................. 51
2.4.1 Εφαρμογές ................................................................................................................. 51
2.4.2 Είδη λιστών ............................................................................................................... 51
2.4.3 Βασικές πράξεις σε λίστες ......................................................................................... 52
2.4.4 Αλγόριθμοι βασικών πράξεων ................................................................................... 53
Εισαγωγή στοιχείου ....................................................................................................................55
Διαγραφή στοιχείου.....................................................................................................................56
Σάρωση (διαπέραση) λίστας.......................................................................................................58
Αναζήτηση στοιχείου...................................................................................................................59
Συνένωση λιστών ........................................................................................................................60
Αντιστροφή λίστας.......................................................................................................................60
2.4.4 Δυναμική υλοποίηση στοίβας και ουράς ................................................................... 61
Ερωτήσεις Κεφαλαίου .................................................................................................... 64

ΚΕΦΑΛΑΙΟ 3: ΤΑΞΙΝΟΜΗΣΗ, ΑΝΑΖΗΤΗΣΗ ΚΑΙ ΕΠΙΛΟΓΗ ............................ 65


3.1 Το πρόβλημα της Ταξινόμησης ............................................................................... 65
3.1.1 Ταξινόμηση Φυσαλίδας (Bubblesort)......................................................................... 66
3.1.2 Ταξινόμηση με Εισαγωγή (Insertionsort) ................................................................... 70
3.1.3 Ταξινόμηση με Επιλογή (Selectionsort) ..................................................................... 71
3.1.4 Ταξινόμηση Σωρού (Heapsort) .................................................................................. 73
3.1.5 Γρήγορη Ταξινόμηση (Quicksort)............................................................................... 79
3.1.6 Ταξινόμηση με Συγχώνευση (Mergesort) ................................................................... 83
3.1.7 Ταξινόμηση με Μέτρηση (Countingsort)..................................................................... 86
3.1.8 Radixsort.................................................................................................................... 88
3.1.9 Εξωτερική Ταξινόμηση (External Sorting) .................................................................. 89
3.1.9 Πόσο γρήγορα μπορούμε να ταξινομούμε; ................................................................ 90
3.2 Αναζήτηση στοιχείων ............................................................................................... 92
3.2.1 Γενικός αλγόριθμος αναζήτησης................................................................................ 92
3.2.2 Γραμμική (ακολουθιακή) Αναζήτηση (Linear Search) ................................................ 93
3.2.3 Δυαδική Aναζήτηση (Binary Search) ......................................................................... 94
3.2.4 Αναζήτηση Παρεμβολής (Ιnterpolation Search) ......................................................... 95
3.2.5 Αναζήτηση κατά Oμάδες (Block Search) ................................................................... 97
3.3 Eπιλογή του i-οστού μεγαλύτερου στοιχείου ........................................................... 98
3.3.1 Ο αλγόριθμος Find..................................................................................................... 98
3.3.2 Ο αλγόριθμος Select................................................................................................ 100
Ερωτήσεις Κεφαλαίου .................................................................................................. 101

ΚΕΦΑΛΑΙΟ 4: ΜΗ ΓΡΑΜΜΙΚΕΣ ΔΟΜΕΣ ΔΕΔΟΜΕΝΩΝ................................ 103


4.1 Δένδρα.................................................................................................................... 103
4.1.1 Γενική περιγραφή - Ορισμοί .................................................................................... 103
4.1.3 Δυαδικά δένδρα....................................................................................................... 105

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 3


Αλγόριθμοι και Δομές Δεδομένων

4.1.4 Ιδιότητες δυαδικών δένδρων.................................................................................... 106


4.1.5 Υλοποίηση δυαδικού δένδρου................................................................................. 106
Υλοποίηση με χρήση πίνακα ....................................................................................................106
Υλοποίηση με τη χρήση εγγραφών και δεικτών ........................................................................107
4.1.6 Δυαδικά δένδρα αναζήτησης................................................................................... 108
4.1.7 Μέθοδοι διαπέρασης δυαδικών δένδρων................................................................ 109
Παραδείγματα διαπεράσεων.....................................................................................................110
Αλγόριθμοι διαπέρασης στη C..................................................................................................111
4.1.8 Βασικοί υπολογισμοί σε δυαδικά δένδρα................................................................ 113
Ύψος δένδρου...........................................................................................................................113
Πλήθος κόμβων δένδρου...........................................................................................................113
Πλήθος φύλλων δένδρου...........................................................................................................114
Ελάχιστο και μέγιστο στοιχείο δυαδικού δένδρου αναζήτησης ................................................114
Δημιουργία ισοζυγισμένου κομβοπροσανατολισμένου δυαδικού δένδρου αναζήτησης.........115
Απεικόνιση δένδρου..................................................................................................................116
4.1.9 Νηματοειδή δένδρα.................................................................................................. 117
Εύρεση επόμενου κατά τη συμμετρική διάταξη .........................................................................118
Εύρεση προηγούμενου κατά τη συμμετρική διάταξη.................................................................120
4.1.10 Αλγόριθμοι βασικών πράξεων σε κομβοπροσανατολισμένα δυαδικά δένδρα
αναζήτησης....................................................................................................................... 121
Η πράξη Search() ......................................................................................................................122
Η πράξη Ιnsert().........................................................................................................................123
Η πράξη Delete() .......................................................................................................................124
Aνάλυση της συμπεριφοράς των δυαδικών δένδρων αναζήτησης ...........................................126
4.1.11 Ισοζυγισμένα δένδρα............................................................................................. 128
Υψοζυγισμένα δένδρα...............................................................................................................129
AVL δένδρα.......................................................................................................................................130
Kόκκινα-μαύρα δένδρα.....................................................................................................................132
Βαροζυγισμένα δένδρα .............................................................................................................133
Πολυδιακλαδισμένα τέλεια ισοζυγισμένα δένδρα .....................................................................134
4.1.12 Πολυδιάστατα δένδρα αναζήτησης ........................................................................ 136
Το δένδρο περιοχής ..................................................................................................................136
Δένδρα περιοχής σε μεγαλύτερες διαστάσεις...........................................................................138
4.1.13 Άλλα δένδρα........................................................................................................... 139
Ψηφιακά δένδρα........................................................................................................................139
Τετραδικά δένδρα......................................................................................................................141
4.2 Γράφοι .................................................................................................................... 144
4.2.1 Τι είναι γράφος ........................................................................................................ 144
4.2.2 Βασικοί κανόνες απεικόνισης γράφων .................................................................... 145
4.2.3 Αναπαράσταση γράφου στον υπολογιστή ............................................................. 146
4.2.4 Κατηγορίες γράφων ................................................................................................. 148
Πολλαπλοί γράφοι.....................................................................................................................148
Ισομορφικοί γράφοι...................................................................................................................148
Επίπεδος γράφος ......................................................................................................................149
Πλήρης γράφος .........................................................................................................................149
Διμερής γράφος.........................................................................................................................149
Συμπληρωματικοί γράφοι..........................................................................................................149
4.2.5 Διαπερασιμότητα..................................................................................................... 150

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 4


Αλγόριθμοι και Δομές Δεδομένων

Κύκλος Euler και γράφος Euler .................................................................................................151


Κύκλος Hamilton και γράφος Hamilton......................................................................................151
4.2.6 Συνεκτικότητα........................................................................................................... 152
4.2.7 Κατευθυνόμενοι γράφοι ........................................................................................... 153
4.2.6 Αναζήτηση κορυφών γράφου .................................................................................. 154
Aλγόριθμος BFS(G, s) ...............................................................................................................155
Aλγόριθμος DFS(G, s)...............................................................................................................156
4.2.9 Βασικά γραφοθεωρητικά προβλήματα .................................................................... 156
Προβλήματα που επιλύονται σε πολυωνυμικό χρόνο...............................................................156
Υπολογιστικά δύσκολα προβλήματα ........................................................................................162
4.3 Διαχείριση ξένων συνόλων και δομές UNION-FIND ............................................. 166
4.3.1 Υλοποίηση UNION - FIND δομής με λίστες ............................................................. 168
4.3.2 Υλοποίηση UNION - FIND δομής με δένδρα ........................................................... 169
Ερωτήσεις Κεφαλαίου .................................................................................................. 172

Β Ι Β Λ Ι Ο Γ Ρ Α Φ Ι Α ...................................................................................... 175

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 5


Αλγόριθμοι και Δομές Δεδομένων

ΠΡΟΛΟΓΟΣ
Ο προγραμματισμός επικεντρώνεται στην μετατροπή της ακολουθίας βημάτων επίλυσης
ενός προβλήματος σε μία ακολουθία εντολών άμεσα αναγνωρίσιμη από τον υπολογιστή.
Γενικά θα μπορούσε να θεωρηθεί ότι η επίλυση ενός προβλήματος απαιτεί 3 στάδια, τον
σαφή ορισμό του προβλήματος, την ανάπτυξη της λύσης του προβλήματος μέσω μίας
ακριβούς ακολουθίας βημάτων και την κωδικοποίηση της μεθόδου επίλυσης του προβλήματος
(βημάτων) και την υλοποίησή της στον υπολογιστή (πρόγραμμα). Παρά το γεγονός ότι ο
προγραμματισμός αναφέρεται στο τρίτο στάδιο πρέπει να κατανοηθεί ότι τα πρώτα δύο
στάδια είναι εξίσου σημαντικά. Η μεθοδολογία ανάπτυξης της μεθόδου επίλυσης του
προβλήματος αποτελεί τον Αλγόριθμο (Algorithm) ενώ ο τρόπος με τον οποίο οργανώνονται
τα δεδομένα σε ομάδες ονομάζονται Δομές Δεδομένων (Data Structures). Οι αλγόριθμοι και
οι δομές δεδομένων αποτελούν τα βασικά συστατικά ενός προγράμματος. Οι τεχνικές που
χρησιμοποιούνται στο σχεδιασμό ενός αλγόριθμου και η επιλογή των κατάλληλων δομών
δεδομένων προδιαγράφουν ουσιαστικά την ποιότητα και την απόδοση του προγράμματος
πριν ακόμα αυτό υλοποιηθεί.
Αντικείμενο των σημειώσεων είναι να εισάγει τους σπουδαστές στις βασικές αρχές των
Αλγόριθμων και των Δομών Δεδομένων και να παρουσιάσει τις πιο κυριότερες δομές που
χρησιμοποιούνται τόσο στην κύρια όσο και στην δευτερεύουσα μνήμη. Οι σημειώσεις
εστιάζουν ιδιαίτερα στις βασικές πράξεις που υποστηρίζει κάθε δομή δεδομένων αναλύοντας
τους αλγόριθμους υλοποίησής τους.
Πιο συγκεκριμένα, στο Κεφάλαιο 1 γίνεται μία σύντομη παρουσίαση των βασικών
εννοιών των αλγόριθμων και των δομών δεδομένων. Αρχικά παρουσιάζονται οι βασικές αρχές
ορισμού ενός προβλήματος. Στην συνέχεια ορίζεται η μεθοδολογία ανάπτυξης της λύσης ενός
προβλήματος εισάγοντας την έννοια του αλγόριθμου. Δίνεται ως παράδειγμα ο αλγόριθμος
επίλυσης του προβλήματος εύρεσης του μέγιστου κοινού διαιρέτη δύο θετικών ακεραίων.
Παράλληλα περιγράφεται ο τρόπος ανάλυσης των αλγόριθμων και μέτρησης της
απόδοσης/πολυπλοκότητάς τους ενώ παρουσιάζεται και η μορφή των ακολουθιών εντολών
της αλγοριθμικής γλώσσας που χρησιμοποιούμε παντού στις σημειώσεις. Επίσης,
περιγράφεται ο τρόπος με τον οποίο οργανώνονται τα δεδομένα σε ομάδες που ονομάζονται
δομές δεδομένων καθώς και η χρήση τους στην ανάπτυξη των αλγόριθμων αλλά και στην
διαδικασία του προγραμματισμού στη συνέχεια. Αρχικά περιγράφονται οι συνηθισμένες
πράξεις πάνω στις δομές δεδομένων και στη συνέχεια γίνεται μία εκτενή παρουσίαση των
σημαντικότερων από αυτές.
Στο Κεφάλαιο 2 παρουσιάζονται οι πιο βασικές γραμμικές δομές δεδομένων που
χρησιμοποιούνται τόσο στην κύρια όσο και στην δευτερεύουσα μνήμη. Πιο συγκεκριμένα,
αρχικά περιγράφεται η πιο απλή και συχνά χρησιμοποιούμενη δομή, ο πίνακας. Στη συνέχεια
αναλύεται η στοίβα, οι βασικές αρχές λειτουργίας της και οι βασικές πράξεις push και pop.
Παράλληλα, περιγράφονται μερικές εφαρμογές της στοίβας όπως είναι η υλοποίηση της
αναδρομής και η μετατροπή παραστάσεων στον πολωνικό συμβολισμό. Αμέσως μετά
περιγράφεται η ουρά, οι διαφορές της με την στοίβα και οι υποστηριζόμενες πράξεις Dequeue
και Enqueue σε ουρά pipeline και ουρά δακτυλίου. Τέλος, αναλύεται η δομή της λίστας,
δίνοντας έμφαση στις πράξεις εισαγωγής, αναζήτησης και διαγραφής σε απλά συνδεδεμένη
λίστα. Σε όλες τις παραπάνω δομές δίνονται σε μορφή ψευδοκώδικα οι αλγόριθμοι
υλοποίησης των βασικών πράξεων.
Στο Κεφάλαιο 3 αρχικά περιγράφεται το πρόβλημα της ταξινόμησης και οι μέθοδοι που
χρησιμοποιούνται για την επίλυσή του και οι οποίες διακρίνονται στις βασιζόμενες σε
συγκρίσεις και σε εκείνες που βασίζονται στην αναπαράσταση των στοιχείων εισόδου.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 6


Αλγόριθμοι και Δομές Δεδομένων

Εξετάζονται οι βασικότεροι αλγόριθμοι που εφαρμόζονται τόσο στην κύρια όσο και τη
βοηθητική μνήμη και γίνεται ανάλυση της απόδοσής τους. Στο δεύτερο μέρος περιγράφονται
οι πιο συνηθισμένες μέθοδοι αναζήτησης σε έναν ταξινομημένο πίνακα και στο τρίτο μέρος
παρουσιάζονται δύο αλγόριθμοι επιλογής του i-oστού μεγαλύτερου στοιχείου ενός τυχαίου
πίνακα.
Στο Κεφάλαιο 4 περιγράφονται τρεις βασικές μη γραμμικές δομές δεδομένων, τα δένδρα,
οι γράφοι και οι δομές UNION-FIND. Αρχικά παρουσιάζονται τα δένδρα δεδομένου ότι
αποτελούν τις πλέον αποδοτικές δομές στην οργάνωση και επεξεργασία μεγάλου όγκου
δεδομένων. Στη συνέχεια περιγράφονται και αναλύονται οι βασικές πράξεις σε ένα δυαδικό
κομβοπροσανατολισμένο δένδρο αναζήτησης και επιχειρείται μία σύντομη εισαγωγή στα
ισοζυγισμένα δένδρα καθώς και σε δένδρα που απαντώνται σε ειδικές εφαρμογές. Στο
δεύτερο μέρος του κεφαλαίου ορίζεται η δομή του γράφου, περιγράφονται οι βασικές
κατηγορίες γράφων καθώς και θεμελιώδεις έννοιες όπως η διαπερασιμότητα και η
συνεκτικότητα και στο τέλος παρατίθενται τα σημαντικότερα γραφοθεωρητικά προβλήματα της
επιστήμης των υπολογιστών τα οποία απαντώνται σε πλήθος εφαρμογών. Το κεφάλαιο
κλείνει με την περιγραφή και ανάλυση της δομής UNION-FIND η οποία διαχειρίζεται
αποδοτικά ξένα μεταξύ τους σύνολα στοιχείων.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 7


Αλγόριθμοι και Δομές Δεδομένων

ΚΑΤΑΛΟΓΟΣ ΣΧΗΜΑΤΩΝ
Σχήμα 1: Διάγραμμα ροής για τον αλγόριθμο του Ευκλείδη .................................................. 16
Σχήμα 2: Γραφικές παραστάσεις διαφορετικών τάξεων ασυμπτωτικής πολυπλοκότητας ..... 22
Σχήμα 3: Παραδείγματα πινάκων........................................................................................... 29
Σχήμα 4: Η δομή της στοίβας ................................................................................................. 35
Σχήμα 5: Η στοίβα μετά την ένθεση του στοιχείου P ............................................................. 35
Σχήμα 6: Η στοίβα μετά την απώθηση του στοιχείου της κορυφής........................................ 36
Σχήμα 7: Η δομή της ουράς.................................................................................................... 44
Σχήμα 8: Η ουρά μετά την ένθεση του στοιχείου P ............................................................... 44
Σχήμα 9: Η ουρά μετά την απώθηση του πρώτου της στοιχείου (Χ)..................................... 44
Σχήμα 10: Υλοποίηση ουράς pipeline με χρήση πίνακα ........................................................ 46
Σχήμα 11: Υλοποίηση ουράς δακτυλίου με χρήση πίνακα .................................................... 49
Σχήμα 12: Παράδειγμα απλά συνδεδεμένης λίστας............................................................... 52
Σχήμα 13: Παράδειγμα διπλά συνδεδεμένης λίστας.............................................................. 52
Σχήμα 14: Κυκλική λίστα........................................................................................................ 52
Σχήμα 15: Εισαγωγή του στοιχείου Χ σε μια απλά συνδεδεμένη λίστα ................................ 55
Σχήμα 16: Δυναμική δέσμευση μνήμης για τη δομή του δείκτη node με τη χρήση της
malloc()........................................................................................................................... 56
Σχήμα 17: Διαγραφή του στοιχείου Β από μια απλά συνδεδεμένη λίστα .............................. 57
Σχήμα 18: Αποδέσμευση μνήμης που καταλάμβανε η δομή του δείκτη node μετά την
εκτέλεση της free(node).................................................................................................. 57
Σχήμα 19: Αντιστροφή των στοιχείων μιας λίστας................................................................. 60
Σχήμα 20: Ένας δυαδικός σωρός 10 στοιχείων..................................................................... 74
Σχήμα 21: Το δένδρο μετά την απομάκρυνση του μεγαλύτερου στοιχείου ............................ 77
Σχήμα 22: Διαδικασία βύθισης του στοιχείου 2 και επανακατασκευή του σωρού ................ 77
Σχήμα 23: Λειτουργία του βήματος διαχωρισμού του quicksort............................................. 79
Σχήμα 24: Λειτουργία του αλγόριθμου mergesort.................................................................. 84
Σχήμα 25: Λειτουργία του αλγόριθμου countingsort .............................................................. 87
Σχήμα 26: Το δένδρο αποφάσεων παράγει ο insertionsort για 3 στοιχεία............................. 91
Σχήμα 27: Δένδρο αναζήτησης του αλγόριθμου binary search.............................................. 95
Σχήμα 28: Λειτουργία μεθόδου αναζήτησης με δυαδική παρεμβολή ..................................... 97
Σχήμα 29: Διαχωρισμός σε πεντάδες. Τα στοιχεία του S1 είναι μικρότερα ή ίσα του m και
τα στοιχεία του S2 μεγαλύτερα του m ......................................................................... 100
Σχήμα 30: Επεξήγηση των βασικότερων εννοιών σε ένα (δυαδικό) δένδρο ........................ 104
Σχήμα 31: Παράδειγμα προγονικού δένδρου....................................................................... 105
Σχήμα 32: Δένδρο εκφράσεων για την παράσταση c^d/a+(e+f)*b....................................... 105
Σχήμα 33: Δήλωση ενός δυαδικού δένδρου στη C .............................................................. 108
Σχήμα 34: Δυαδικό δένδρο αναζήτησης .............................................................................. 108
Σχήμα 35: Κομβοπροσανατολισμένο και φυλλοπροσανατολισμένο δυαδικό δένδρο
αναζήτησης για το ίδιο σύνολο στοιχείων.................................................................... 109
Σχήμα 36: Αλγόριθμος preorder διαπέρασης....................................................................... 110
Σχήμα 37: Αλγόριθμος inorder διαπέρασης ......................................................................... 110
Σχήμα 38: Αλγόριθμος postorder διαπέρασης ..................................................................... 110
Σχήμα 39: Η έξοδος της Print_tree() για ένα κομβοπροσανατολισμένο και το αντίστοιχο
φυλλοπροσανατολισμένο δυαδικό δένδρο αναζήτησης............................................... 117
Σχήμα 40: Παράδειγμα δεξιού ενδονηματοειδούς δυαδικού δένδρου .................................. 117
Σχήμα 41: Εύρεση του successor(p) .................................................................................... 119
Σχήμα 42: Εύρεση του predecessor(p) ................................................................................ 120
Σχήμα 43: Λειτουργία αλγόριθμου Search()......................................................................... 122

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 8


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 44: Λειτουργία αλγόριθμου Delete().......................................................................... 124


Σχήμα 45: Η πράξη της περιστροφής σε ένα δυαδικό ισοζυγισμένο δένδρο....................... 130
Σχήμα 46: Παράδειγμα AVL δένδρου................................................................................... 131
Σχήμα 47: Oρισμός του Fibonacci δένδρου ......................................................................... 131
Σχήμα 48: Παράδειγμα κόκκινου-μαύρου δένδρου .............................................................. 132
Σχήμα 49: Παράδειγμα (2, 4) δένδρου ................................................................................. 134
Σχήμα 50: Εύρεση των σημείων με x-συντεταγμένη μεταξύ των xQ1 και xQ2 ........................ 137
Σχήμα 51: Παράδειγμα δένδρου περιοχής για ένα συγκεκριμένο σύνολο σημείων............. 137
Σχήμα 52: Υλοποίηση ψηφιακού δένδρου για τους αριθμούς 123, 129, 140, 143, 148,
151, 155, 167 ............................................................................................................... 139
Σχήμα 53: Το δένδρο που προκύπτει μετά την εισαγωγή του αριθμού 1234....................... 140
Σχήμα 54: Η δομή trie για τα στοιχεία 102, 120, 121, 210, 211 και 212 .............................. 141
Σχήμα 55: Το τετραδικό δένδρο που αντιστοιχεί στον εικόνα του παραδείγματος .............. 143
Σχήμα 56: Πίνακας γειτνίασης και λίστες γειτνίασης για την αποθήκευση ενός γράφου..... 146
Σχήμα 57: Παράδειγμα πολλαπλού γράφου ........................................................................ 148
Σχήμα 58: Παραδειγμα ισομορφικών γράφων ..................................................................... 149
Σχήμα 59: Παράδειγμα διμερή γράφου................................................................................ 149
Σχήμα 60: Παράδειγμα συμπληρωματικών γράφων............................................................ 150
Σχήμα 61: Επεξήγηση των βασικών εννοιών διαπερασιμότητας ενός γράφου.................... 150
Σχήμα 62: Κύκλος Euler και κύκλος Hamilton σε ένα γράφο................................................ 151
Σχήμα 63: Παράδειγμα συνεκτικού γράφου......................................................................... 152
Σχήμα 64: Παράδειγμα δισυνεκτικού γράφου...................................................................... 152
Σχήμα 65: Παράδειγμα κατευθυνόμενου γράφου................................................................. 153
Σχήμα 66: Ισχυρά συνεκτικές συνιστώσες ενός γράφου ..................................................... 154
Σχήμα 67: BFS δένδρο αναζήτησης..................................................................................... 155
Σχήμα 68: DFS δένδρο αναζήτησης..................................................................................... 156
Σχήμα 69: Αναπαράσταση συνόλων με τη χρήση συνδεδεμένων λιστών ........................... 168
Σχήμα 70: Αναπαράσταση συνόλων με τη χρήση δένδρων ................................................ 169
Σχήμα 71: Εφαρμογή του κανόνα path compression στην εκτέλεση μιας Find().................. 171

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 9


Αλγόριθμοι και Δομές Δεδομένων

ΚΕΦΑΛΑΙΟ 1: ΑΛΓΟΡΙΘΜΟΙ ΚΑΙ ΔΟΜΕΣ ΔΕΔΟΜΕΝΩΝ


Σκοπός του κεφαλαίου αυτού είναι να κάνει μία σύντομη παρουσίαση στους σπουδαστές
των βασικών εννοιών των αλγόριθμων και των δομών δεδομένων. Αρχικά τονίζονται οι
βασικές αρχές ορισμού ενός προβλήματος. Στην συνέχεια ορίζεται η μεθοδολογία ανάπτυξης
της μεθόδου επίλυσης ενός προβλήματος εισάγοντας την έννοια του αλγόριθμου. Δίνεται ως
παράδειγμα ο αλγόριθμος επίλυσης του προβλήματος εύρεσης του μέγιστου κοινού διαιρέτη
δύο θετικών ακεραίων. Παράλληλα, περιγράφεται ο τρόπος ανάλυσης των αλγόριθμων και της
μέτρησης της απόδοσης/πολυπλοκότητας τους ενώ παρουσιάζεται και η μορφή των
ακολουθιών εντολών στην αλγοριθμική γλώσσα που χρησιμοποιούμε στις σημειώσεις αυτές..
Επίσης, περιγράφεται ο τρόπος με τον οποίο οργανώνονται τα δεδομένα σε ομάδες που
ονομάζονται δομές δεδομένων καθώς και η χρήση τους στην ανάπτυξη των αλγόριθμων αλλά
και στην διαδικασία του προγραμματισμού στη συνέχεια. Αρχικά περιγράφονται οι
συνηθισμένες πράξεις πάνω στις δομές δεδομένων και στη συνέχεια γίνεται μία συνοπτική
παρουσίαση των σπουδαιότερων από αυτές.

1.1 Εισαγωγή
Ο προγραμματισμός στους υπολογιστές είναι η διαδικασία με την οποία ο χρήστης ορίζει
στον υπολογιστή τον τρόπο με τον οποίο θέλει να επιλύσει ένα συγκεκριμένο πρόβλημα ή
κατάσταση. Επομένως βασικότερη έννοια είναι η επίλυση ενός προβλήματος από τον χρήστη
με τέτοιο τρόπο ώστε να μπορεί στη συνέχεια να εκτελέσει τα βήματα επίλυσης ο
υπολογιστής.
Γενικά θα μπορούσε να θεωρηθεί ότι η επίλυση ενός προβλήματος απαιτεί 3 στάδια:
1. Τον σαφή ορισμό του προβλήματος.
2. Την ανάπτυξη της λύσης του προβλήματος μέσω μίας ακριβούς ακολουθίας βημάτων
3. Την κωδικοποίηση της μεθόδου επίλυσης του προβλήματος (βημάτων) και την υλοποίησή
του στον υπολογιστή
Ο προγραμματισμός επικεντρώνεται στο 3ο στάδιο, δηλαδή στην μετατροπή της
ακολουθίας βημάτων επίλυσης του προβλήματος είτε σε μία ακολουθία εντολών άμεσα
αναγνωρίσιμη από τον υπολογιστή είτε σε μία ενδιάμεση γλώσσα εύχρηστη και φιλική από
τον χρήστη η οποία μπορεί στη συνέχεια να μεταγλωττισθεί σε εντολές που αναγνωρίζει ο
υπολογιστής και ανήκουν στην γλώσσα μηχανής του.
Παρά την πρακτική αξία του 3ου σταδίου, θα πρέπει να γίνει κατανοητό ότι και τα πρώτα
δύο στάδια είναι εξίσου σημαντικά και για το λόγο αυτό αποτελούν το κύριο αντικείμενο
συζήτησης στο παρόν κεφάλαιο.

1.2 Ορισμός ενός προβλήματος


Κάθε φορά που ο χρήστης καλείται να επιλύσει ένα πρόβλημα, δηλαδή μία κατάσταση
που απαιτεί λύση, πρέπει να ακολουθήσει μία σειρά κανόνων ή βημάτων. Οι κανόνες αυτοί
σκοπό έχουν την εξεύρεση μιας καλής (ιδανικά της βέλτιστης) λύσης και είναι:
1. Κατανόηση του προβλήματος
Είναι σαφές ότι βασική παράμετρος σωστής λύσης του προβλήματος είναι η κατανόηση
του. Αυτό σημαίνει ότι αρχικά ο χρήστης πρέπει να το ερμηνεύσει σωστά και στη

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 10


Αλγόριθμοι και Δομές Δεδομένων

συνέχεια να το διατυπώσει προφορικά ή γραπτά με σαφήνεια χρησιμοποιώντας σωστή


ορολογία και σύνταξη.
2. Αποτύπωση της δομής του προβλήματος
Μετά την κατανόηση του προβλήματος ο χρήστης πρέπει να το αναλύσει βρίσκοντας τα
συστατικά του μέρη και στη συνέχεια εξετάζοντας το καθένα χωριστά αλλά και τις σχέσεις
επικοινωνίας μεταξύ τους. Η ανάλυση αυτή ουσιαστικά απλοποιεί την επίλυση του
προβλήματος αφού απαιτεί την επίλυση μικρότερων υποπροβλημάτων και την ένωση μετά
όλων των επιμέρους λύσεων.
3. Καθορισμός απαιτήσεων
Στην περίπτωση αυτή καλείται ο χρήστης να προσδιορίσει τα δεδομένα του προβλήματος
καθώς και τα ζητούμενα της λύσης του, δηλ. την είσοδο και την έξοδο του προβλήματος.
Τα προβλήματα μπορούν να κατηγοριοποιηθούν με πολλούς τρόπους. Έτσι μπορεί να
είναι άλυτα, επιλύσιμα ή ανοιχτά ανάλογα με τη δυνατότητα επίλυσής τους είτε δομημένα,
ημιδομημένα ή αδόμητα ανάλογα με το βαθμό δόμησής τους. Το σημαντικό όμως σε κάθε
περίπτωση είναι ότι ο χρήστης – προγραμματιστής δεν καλείται πάντα να επιλύσει ένα
μαθηματικό ή στατιστικό πρόβλημα που έχει με πράξεις. Είναι σύνηθες ο χρήστης να καλείται
να επιλύσει ένα σύνθετο πρόβλημα με βάση λογικούς συλλογισμούς. Στην περίπτωση αυτή
πρέπει να ακολουθήσει τους παραπάνω κανόνες ώστε να ορίσει το πρόβλημα σωστά.

1.3 Επίλυση προβλημάτων – Ανάπτυξη αλγόριθμων


1.3.1 Η έννοια του αλγόριθμου
Μετά τον ορισμό του προβλήματος ακολουθεί η επίλυσή του. Αυτή επιτυγχάνεται με τον
ορισμό μία πεπερασμένης σειράς ενεργειών-βημάτων που αποτελούν μία σαφή υπολογιστική
διαδικασία και εκτελείται σε πεπερασμένο χρόνο. Η διαδικασία αυτή καλείται αλγόριθμος
(algorithm)1.
Σε κάθε αλγόριθμο πρέπει να καθορίζονται τα παρακάτω στοιχεία:
1. Είσοδος (input). Απεικονίζει τις τιμές των δεδομένων του προβλήματος που δίνονται από
την αρχή ή παράγονται από ενδιάμεσα τμήματα του και αποτελούν είσοδο για τα επόμενα
(και ονομάζονται στιγμιότυπα). Όταν τα δεδομένα εισόδου καλύπτουν κάθε φορά τις
προδιαγραφές του συστήματος τότε η είσοδος του αλγόριθμου καλείται νόμιμη.
2. Έξοδος (output). Η έξοδος του αλγόριθμου είναι η μερική ή τελική λύση του προβλήματος.
3. Πεπερασμένα βήματα. Κάθε αλγόριθμος πρέπει να αποτελείται από συγκεκριμένα βήματα
και να ολοκληρώνεται μετά την εκτέλεση τους.
4. Σαφή απόδοση. Το γεγονός αυτό σημαίνει ότι ο χρήστης – προγραμματιστής πρέπει να
γνωρίζει την ταχύτητα εκτέλεσης του αλγόριθμου του, που είναι ανάλογη του πλήθους των
πράξεων που εκτελεί για την επίλυση του προβλήματος, ώστε να μπορεί να τον
αξιολογήσει σε σύγκριση με άλλους αντίστοιχους για το ίδιο πρόβλημα.

1
O όρος αλγόριθμος αποδίδεται στον Πέρση μαθηματικό Abu Ja’far Mohammed ibn Musa
al Khowarizmi, που έζησε περί το 825 μ.Χ. και συνέγραψε μία μελέτη η οποία θεωρείται ως η
πρώτη πλήρης πραγματεία άλγεβρας.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 11


Αλγόριθμοι και Δομές Δεδομένων

Αρκετές φορές η έννοια του αλγόριθμου συγχέεται με την έννοια του προγράμματος. Ένα
πρόγραμμα δεν είναι τίποτα περισσότερο από μία «μετάφραση» ή «κωδικοποίηση» ενός
αλγόριθμου στη γλώσσα που καταλαβαίνει ο υπολογιστής. Αυτό σημαίνει ότι είναι σχεδόν
αδύνατο να γράψει κανείς ένα πρόγραμμα που να λύνει ένα υπολογιστικό πρόβλημα αν
προηγουμένως δεν έχει αναπτύξει έναν αλγόριθμο για την επίλυσή του. Δυστυχώς, δεν
υπάρχει αλγόριθμος για την εύρεση αλγόριθμων, υπάρχουν όμως μερικές τεχνικές χρήσιμες
στην ανάπτυξή τους. Γεγονός όμως είναι ότι τις περισσότερες φορές η ανάπτυξη ενός
αλγόριθμου για την επίλυση ενός προβλήματος είναι διαδικασία σαφώς δυσκολότερη από τον
προγραμματισμό του αλγόριθμου.

1.3.2 Αναπαράσταση αλγόριθμων


Κάθε αλγόριθμος πρέπει στο τέλος να εκφραστεί από τον προγραμματιστή σε μία γλώσσα
προγραμματισμού ώστε να εισαχθεί και να εκτελεστεί στον υπολογιστή.
Η διαδικασία της άμεσης σύνταξης του αλγόριθμου στη γλώσσα προγραμματισμού
εγκυμονεί πολλούς κινδύνους και δεν ενδείκνυται ανεξάρτητα από την εμπειρία του
προγραμματιστή. Κάθε αλγόριθμος πρέπει να συνταχθεί και να “περάσει” από ενδιάμεσα
στάδια μέχρι να εισαχθεί στον υπολογιστή. Ο τρόπος αυτός περιλαμβάνει και όλα τα στάδια
ελέγχου της ορθότητας του ανάλογα σε πιο επίπεδο δομής βρίσκεται. Οι τρόποι
αναπαράστασης ενός αλγόριθμου με βάση τα στάδια ανάπτυξής του είναι:
1. Ελεύθερο κείμενο. Ο προγραμματιστής αρχικά καταγράφει τους συλλογισμούς του σε
φυσική γλώσσα.
2. Δομημένη φυσική γλώσσα. Στη συνέχεια γίνεται η δόμηση του κειμένου σε βήματα
ακολουθώντας τη δομή επίλυσης του προβλήματος.
3. Αναπαράσταση με διαγράμματα. Αμέσως μετά ο προγραμματιστής αναπαριστά τα
βήματα του αλγόριθμου του χρησιμοποιώντας διαγράμματα ροής (flow charts) ή
λογικά διαγράμματα όπως ονομάζονται αλλιώς. Η σχηματική αυτή αναπαράσταση
βοηθά τον προγραμματιστή να εξετάσει τις αλληλεπιδράσεις των τμημάτων μεταξύ
τους και να έχει μία καλύτερη εποπτική εικόνα του αλγόριθμου.
Σύμφωνα με μία κοινά αποδεκτή διαγραμματική τεχνική οι βασικές εντολές – βήματα
του αλγόριθμου αναπαρίστανται με γεωμετρικά σχήματα ως εξής:
• Η έλλειψη δηλώνει την αρχή και το τέλος του αλγόριθμου,
• Το ορθογώνιο δηλώνει εντολές εκτέλεσης πράξεων,
• Το πλάγιο παραλληλόγραμμο δηλώνει την είσοδο δεδομένων ή την έξοδο των
αποτελεσμάτων του αλγόριθμου,
• Ο ρόμβος δηλώνει μία διαδικασία ερώτησης ή απόφασης.
4. Ψευδοκώδικας ή ψευδογλώσσα. Αποτελείται από μία μίξη εντολών υπαρχουσών
γλωσσών προγραμματισμού και φυσικής γλώσσας χωρίς να απαιτείται λεκτικός,
συντακτικός και σημασιολογικός έλεγχος του κώδικα αυτού. Επικεντρώνεται κυρίως
στην ανάπτυξη του αλγόριθμου αναπαριστώντας με σαφή και καθορισμένο τρόπο
όλες τις ενέργειες (εντολές) επίλυσης του προβλήματος.
Ένας αλγόριθμος είναι σύνηθες να έχει την παρακάτω δομή:
Α. Αρχικά δίνεται το όνομα του αλγόριθμου:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 12


Αλγόριθμοι και Δομές Δεδομένων

Πχ. Αλγόριθμος Επίλυσης Προβλήματος Χ


B. Αμέσως μετά ορίζονται τα δεδομένα τα οποία αποθηκεύονται σε μεταβλητές ή
σταθερές οι οποίες ανάλογα με την εφαρμογή μπορούν να παίρνουν τιμές
πραγματικές ή ακέραιες, δυαδικές (boolean), αλφαριθμητικές κλπ.
Πχ. Δεδομένα // α, β: ακέραιοι; γ: δυαδική //
Γ. Στη συνέχεια ξεκινά ο αλγόριθμος με μία λέξη όπως Αρχή και ολοκληρώνεται
μέχρι την λέξη – εντολή Τέλος. Κάθε αλγόριθμος χρησιμοποιεί ένα πλήθος
εντολών αλλά και τελεστών. Πιο συγκεκριμένα:
• Τελεστές - Οι τελεστές μπορεί να είναι τα αριθμητικά σύμβολα των
πράξεων όπως +, -, *, /, ^, συγκριτικοί όπως <, >, =, ≥, ≤, ≠ αλλά και
λογικοί όπως και (σύζευξη), ή (διάζευξη), όχι (άρνηση).
• Εντολές καταχώρησης μιας τιμής σε μια μεταβλητή όπως:
a = 5, b = c+d κλπ.
• Λογικές εντολές της μορφής:
Αν Συνθήκη Τότε Α
Αλλιώς Β
Τέλος Αν
• Εντολές επανάληψης (loops), επιλογής κ.α.
Πχ. Επανέλαβε
Α
Μέχρι (συνθήκη)

ή Επανέλαβε
Α
Αν Συνθήκη Έξοδος

ή Για μεταβλητή = αρχή Μέχρι μεταβλητή = τέλος με Βήμα τιμή Εκτέλεσε


A
Τέλος Επανάληψης

ή Όσο (συνθήκη) Εκτέλεσε


Α
Τέλος Όσο

ή Επέλεξε (έκφραση)
Περίπτωση 1: A
Περίπτωση 2: B

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 13


Αλγόριθμοι και Δομές Δεδομένων

……
Αλλιώς: X
Τέλος Επιλογής

Είναι γεγονός ότι η εκτέλεση όλων των παραπάνω βημάτων βρίσκεται στην ευχέρεια του
προγραμματιστή και είναι σύνηθες κάποια να παραλαείπονται. Όμως πρέπει να τονιστεί η
σπουδαιότητα τουλάχιστον του τελευταίου βήματος και της σύνταξης ψευδοκώδικα πριν την
μετατροπή του σε μια γλώσσα προγραμματισμού. Η διαδικασία αυτή είναι τόσο σημαντική
που τα τελευταία χρόνια έχει οριστεί ως γνωστικό αντικείμενο με θέμα τη “μηχανική των
αλγόριθμων” (algorithm engineering).

1.3.3 Μορφή ακολουθιών εντολών στους αλγόριθμους


Ο τρόπος με τον οποίο δομούνται οι εντολές ενός αλγόριθμου και ορίζονται οι αντίστοιχες
ακολουθίες τους βασίζονται σε συγκεκριμένες τεχνικές είτε απλές, όπως στην περίπτωση
σειριακών εντολών είτε πολύπλοκες όπως στην περίπτωση αναδρομικών υλοποιήσεων και
εμφωλιασμού.
Πιο συγκεκριμένα οι εντολές μπορούν να δομηθούν με τους παρακάτω τρόπους:
1. Σειριακή ακολουθία εντολών
Χρησιμοποιείται για την επίλυση απλών προβλημάτων .
2. Επιλογή εντολών
Είναι πολύ συνηθισμένη η περίπτωση να πρέπει να εκτελεστεί μία εντολή ανάλογα με
κάποια δεδομένα εισόδου τα οποία καλούνται κριτήρια.
3. Πολλαπλές επιλογές
Στην περίπτωση αυτή, με βάση μία τιμή ενός δεδομένου ο έλεγχος-εκτέλεση του
αλγόριθμου επιλέγεται από μία λίστα συγκεκριμένων περιπτώσεων να μεταφερθεί στο
αντίστοιχο σημείο.
4. Εμφωλιασμένες εντολές
Πρόκειται για εντολές που ανήκουν σε μία ομάδα και καλούνται να εκτελεστούν σε ένα
επίπεδο πιο χαμηλά σε σχέση με ένα άλλο επίπεδο ομάδας εντολών.
5. Επανάληψη εντολών
Σε πολλές περιπτώσεις απαιτείται η εκτέλεση μίας ή περισσοτέρων εντολών
παραπάνω από μια φορά.

1.3.4 Παράδειγμα ανάπτυξης αλγόριθμου


Έστω έχουμε το πρόβλημα:
«Δοθέντων 2 θετικών ακεραίων αριθμών x, y να βρεθεί ο Μέγιστος Κοινός Διαιρέτης
(ΜΚΔ) αυτών z»
Α. Αρχικά προσπαθούμε να κατανοήσουμε το πρόβλημα καταγράφοντας τους
συλλογισμούς μας σε φυσική γλώσσα. Βρίσκουμε λύσεις και εκτελούμε

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 14


Αλγόριθμοι και Δομές Δεδομένων

παραδείγματα ώστε να δούμε αν αυτές είναι σωστές. Τέλος επιλέγουμε την λύση που
μαθηματικά ή έστω διαισθητικά καταλαβαίνουμε ότι είναι η αποδοτικότερη.
Στο παράδειγμα επομένως μπορούμε να θεωρήσουμε τις εξής λύσεις:
1. Από τους δύο αριθμούς μπορούμε να επιλέξουμε τον μικρότερο έστω x και να
εξετάσουμε από το 1 έως το x, όλους τους αριθμούς κρατώντας αυτούς που
διαιρούν και το x και το y. O μεγαλύτερος από αυτούς είναι και ο ζητούμενος.
Είναι σαφές ότι σε κάθε περίπτωση θα εκτελεστεί μία εντολή επανάληψης για x
φορές όπου κάθε φορά θα εξετάζεται αν ο τρέχον αριθμός διαιρεί τον y, και αν ναι
τον κρατούμε και πάμε στον επόμενο αριθμό.
2. Μία άλλη λύση είναι μία παραλλαγή της παραπάνω μόνο που μπορούμε να
ξεκινήσουμε την επανάληψη από τον αριθμό x κατεβαίνοντας μέχρι το 1. Στην
περίπτωση αυτή έχουμε καλύτερη λύση όμως ο χρόνος της χειρότερης
περίπτωσης όπως θα δούμε παρακάτω είναι ίδιος.
3. Μία τρίτη λύση είναι η εξής: “Διαιρούμε το x με το y, και έστω z το υπόλοιπο. Αν z
= 0 τότε ο ΜΚΔ είναι ο y. Αν z ≠ 0 τότε επανάλαβε το βήμα με τους ακεραίους y
και z αντί για x και y”. Η λύση αυτή είναι γνωστή ως αλγόριθμος του Ευκλείδη2.
Για να κατανοήσουμε καλύτερα τη λύση αυτή και να ορίσουμε τα πεπερασμένα
βήματα του αλγόριθμου που προκύπτουν από αυτήν μπορούμε να εκτελέσουμε
βήμα - βήμα ένα συγκεκριμένο παράδειγμα.
Παράδειγμα
Έστω έχουμε τους αριθμούς 26 και 65 και ψάχνουμε τον ΜΚΔ τους.
Ο πίνακας που προκύπτει από τη διαδοχική εφαρμογή των βημάτων του
αλγόριθμου είναι ο εξής:
Επανάληψη Εκτέλεση Βημάτων x y z
Αρχή 1. z = y άρα z = 65 26 65 65
1η 1. z = x mod y άρα z = 26 65 26 26
2. x = y άρα x = 65
3. y = z άρα y = 26
2η 1. z = x mod y άρα z = 13 26 13 13
2. x = y άρα x = 26
3. y = z άρα y = 13
3η 1. z = x mod y άρα z = 0 13 0 0
2. x = y άρα x = 13
3. y = z άρα y = 0

2
Ο συγκεκριμένος αλγόριθμος θεωρείται ως ο πρώτος διάσημος αλγόριθμος και δόθηκε από
τον Έλληνα μαθηματικό Ευκλείδη (~ 325 π.Χ. – 265 μ.Χ.). Ο αρχικός αλγόριθμος, όπως
περιγράφηκε από τον Ευκλείδη στο έργο του «Στοιχεία» Βιβλίο VII, αντιμετώπιζε το
πρόβλημα γεωμετρικά, χρησιμοποιώντας επαναλαμβανόμενες αφαιρέσεις αντί για το
υπόλοιπο της διαίρεσης.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 15


Αλγόριθμοι και Δομές Δεδομένων

Αποτέλεσμα
ΜΚΔ = x = 13

Στην αρχή θέτουμε z = y γεγονός που σημαίνει ότι αν εισέλθουμε στην επανάληψη
για όσο το z ≠ 0 θα έχουμε y ≠ 0 και άρα μπορεί να εκτελεστεί η πράξη x mod y.
Είναι σαφές ότι η παραπάνω λύση είναι αποδοτικότερη από τις δύο
προηγούμενες.
Β. Σύμφωνα με την παράγραφο 1.1, το επόμενο βήμα είναι η αναπαράσταση των
πεπερασμένων βημάτων επίλυσης του αλγόριθμου. Έστω ότι για το σκοπό αυτό
χρησιμοποιούμε ένα διάγραμμα ροής. Για τη λύση του συγκεκριμένου προβλήματος
εύρεσης του ΜΚΔ κατά Ευκλείδη μπορεί να χρησιμοποιηθεί το παρακάτω διάγραμμα:

ΑΡΧΗ

ΔΙΑΒΑΣΕ
Χ,Υ

Z=Y

ΝΑΙ
Z=0

ΟΧΙ ΤΥΠΩΣΕ
Χ
Z = X MOD Y

X=Y
ΤΕΛΟΣ

Y=Z

Σχήμα 1: Διάγραμμα ροής για τον αλγόριθμο του Ευκλείδη

Γ. Το επόμενο βήμα είναι σύνταξη του αλγόριθμου με την μορφή ψευδοκώδικα ή


ψευδογλώσσας. Με τον τρόπο αυτό θα έχουμε τον παρακάτω αλγόριθμο:
Αλγόριθμος ΜΚΔ_Ευκλείδη
Δεδομένα //x, y : ακέραιοι αριθμοί; z: ακέραια μεταβλητή//
Αρχή
z=y

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 16


Αλγόριθμοι και Δομές Δεδομένων

Όσο z <> 0 Εκτέλεσε


z = x mod y
x=y
y=z
Τέλος Όσο
Αποτελέσματα // τύπωσε x : O x είναι ο ΜΚΔ//
Τέλος ΜΚΔ_Ευκλείδη
Δ. Το επόμενο και τελευταίο βήμα είναι η μετατροπή του αλγόριθμου σε μία γλώσσα
προγραμματισμού υψηλού επιπέδου (πρόγραμμα).

1.4 Ανάλυση Αλγόριθμων


1.4.1 Υπολογιστική πολυπλοκότητα αλγόριθμων
Αρκετές φορές υπάρχουν περισσότεροι από έναν αλγόριθμο για το ίδιο πρόβλημα.
Υποθέστε για παράδειγμα ότι θέλουμε να υπολογίσουμε τη n-οστή δύναμη ενός ακεραίου x
για κάποιο φυσικό n (θεωρείστε ότι ο n είναι άρτιος). Για το σκοπό αυτό μπορούμε να
χρησιμοποιήσουμε τους παρακάτω αλγόριθμους:

Αλγόριθμος Α1
Βήμα 1: Διάβασε τα n, x
Βήμα 2: Έστω i=n και y=1
Bήμα 3: Εκτέλεσε τα παρακάτω βήματα μέχρι το i να γίνει 0
3.1 y = y*x
3.2 i = i-1
Bήμα 4: Eκτύπωσε την τιμή του y

Αλγόριθμος Α2
Βήμα 1: Διάβασε τα n, x
Βήμα 2: Υπολόγισε τον αριθμό a = xn/2 με βάση τον Α1
Bήμα 3: Yπολόγισε τον αριθμό y = a*a
Bήμα 4: Eκτύπωσε την τιμή του y

Ποιος από τους παραπάνω αλγόριθμους είναι καλύτερος (προτιμότερος) για τον
υπολογισμό της n-οστής δύναμης ενός ακεραίου;
Για να απαντήσουμε στο ερώτημα αυτό πρέπει να ορίσουμε κάποιο κριτήριο: ένας καλός
αλγόριθμος κάνει αυτό ακριβώς που σχεδιάστηκε να κάνει με το λιγότερο δυνατό κόστος.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 17


Αλγόριθμοι και Δομές Δεδομένων

Ας μετρήσουμε τον αριθμό των πολλαπλασιασμών που χρειάζονται οι δύο αλγόριθμοι του
παραδείγματος:
▪ Για τον Α1 έχουμε n πολ/μούς
▪ Για τον Α2 έχουμε n/2+1 πολ/μούς
Επομένως, με δεδομένο ότι ο πολλαπλασιασμός είναι χρονοβόρα πράξη για τον υπολογιστή,
συμφέρει να διαλέξουμε τον δεύτερο αλγόριθμο προκειμένου να γράψουμε ένα πρόγραμμα για
τον υπολογισμό της δύναμης ακεραίου.
Στο προηγούμενο παράδειγμα είναι προφανές ότι το κριτήριο για την επιλογή αλγόριθμου
είναι η ταχύτητα (ο χρόνος δηλαδή για την εκτέλεση του αλγόριθμου). Είναι όμως ο
αλγόριθμος Α2 ο καλύτερος δυνατός; Ποια κριτήρια μπορούν να το εξασφαλίσουν;
Η υπολογιστική πολυπλοκότητα (computational complexity) είναι το βασικό μέτρο
αξιολόγησης ενός αλγόριθμου. Καθορίζει την απόδοση του αλγόριθμου η οποία εξαρτάται από
τους πόρους που απαιτεί ο αλγόριθμος από το υπολογιστικό σύστημα όταν αυτός υλοποιηθεί
σε πρόγραμμα. Οι πόροι αυτοί μπορεί να είναι το μέγεθος της μνήμης που χρησιμοποιείται
για την αποθήκευση των δεδομένων καθώς και ο χρόνος που χρειάζεται για την εκτέλεση των
εντολών του αλγόριθμου. Από αυτές τις μονάδες μέτρησης ορίζονται η πολυπλοκότητα
χρόνου (time complexity) και χώρου (space complexity) ως δείκτες απόδοσης του
αλγόριθμου και χαρακτηρίζονται συχνά με τον όρο δυναμική πολυπλοκότητα.
Ο πιο απλός και εύκολος τρόπος για να μετρήσουμε την πολυπλοκότητα ενός
αλγόριθμου είναι ο εμπειρικός ή εκ των υστέρων (a posteriori) τρόπος: ο αλγόριθμος
εφαρμόζεται σε ένα σύνολο δεδομένων εισόδου και μετράμε τον χρόνο επεξεργασίας και την
απαιτούμενη μνήμη. Προφανώς, ο τρόπος αυτός δεν είναι πρακτικός γιατί:
• Η συμπεριφορά του αλγορίθμου μπορεί να αλλάξει, αν αλλάξουν τα δεδομένα εισόδου
και,
• Ο χρόνος επεξεργασίας εξαρτάται από το υλικό (hardware), τη γλώσσα
προγραμματισμού στην οποία έχει κωδικοποιηθεί ο αλγόριθμος, τις δεξιότητες και την
εμπειρία του προγραμματιστή κλπ.
Ένας άλλος τρόπος υπολογισμού της πολυπλοκότητας ενός αλγορίθμου είναι ο
θεωρητικός ή εκ των προτέρων (a priori) τρόπος όπου μας ενδιαφέρει να εκφράσουμε την
πολυπλοκότητα ως συνάρτηση του μεγέθους της εισόδου του αλγόριθμου. Το τί σημαίνει
μέγεθος εισόδου εξαρτάται από το εκάστοτε πρόβλημα που επιλύει ο αλγόριθμος. Για
παράδειγμα, σε έναν αλγόριθμο που χρησιμοποιείται για την ταξινόμηση στοιχείων, το
μέγεθος εισόδου είναι το πλήθος των προς διάταξη αντικειμένων.

1.4.2 Moντέλα υπολογισμού


Η διαδικασία του θεωρητικού υπολογισμού της πολυπλοκότητας ενός αλγόριθμου
ονομάζεται ανάλυση του αλγόριθμου και γίνεται με τη βοήθεια μοντέλων υπολογισμού τα
οποία θεωρούν ότι ο αλγόριθμος εκτελείται σε μία συγκεκριμένη υπολογιστική μηχανή
(αφαιρετική θεώρηση του υλικού στο οποίο εκτελείται ο αλγόριθμος στην πράξη). Παράδειγμα
μοντέλου υπολογισμού αποτελεί η μηχανή Turing καθώς και το μοντέλο μηχανής τυχαίας
προσπέλασης (random access machine, RAM) που αναφέρεται σε συστήματα του ενός
επεξεργαστή γενικού σκοπού, δίχως τη δυνατότητα ταυτόχρονης εκτέλεσης πράξεων. Στην
ανάλυση αλγόριθμων η μηχανή RAM είναι το πιο συχνά χρησιμοποιούμενο μοντέλο και
παριστάνει τη δομή των σημερινών μονοεπεξεργαστικών υπολογιστών.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 18


Αλγόριθμοι και Δομές Δεδομένων

Η μηχανή RAM αποτελείται από ένα πεπερασμένο πλήθος καταχωρητών και άπειρες
θέσεις μνήμης που αριθμούνται 0, 1, 2, … Οι καταχωρητές και οι θέσεις μνήμης μπορούν να
αποθηκεύσουν έναν ακέραιο ή πραγματικό αριθμό. Οι εντολές είναι μονοδιευθυντικές και
εκτελούνται ακολουθιακά, η μία μετά την άλλη, μία σε κάθε βήμα. Μία εντολή είναι μία απλή
αριθμητική ή λογική πράξη στα περιεχόμενα κάποιων καταχωρητών ή η άμεση προσπέλαση
για εγγραφή σε ή ανάγνωση από μια θέση μνήμης. Πρακτικά, η RAM αντιστοιχεί σε
υλοποιήσεις αλγόριθμων με πίνακες σε αντίθεση με την μηχανή δείκτη (pointer machine,
PM) που είναι ασθενέστερη της RAM δεδομένου ότι η προσπέλαση στη μνήμη γίνεται μέσω
δεικτών, δηλ. αντιστοιχεί σε υλοποιήσεις αλγόριθμων με λίστες.
Στο μοντέλο RAM ο χρόνος και ο χώρος ενός αλγόριθμου επί συγκεκριμένης εισόδου είναι
ανάλογος των στοιχειωδών πράξεων ή βημάτων που εκτελούνται και του πλήθους των θέσεων
μνήμης που απαιτούνται αντίστοιχα. Μπορούμε εύκολα να εκτιμήσουμε την πολυπλοκότητα
ενός αλγόριθμου αφού:
1. Κάθε εντολή ανάθεσης τιμής, δήλωσης απλής μεταβλητής, η λογική/αριθμητική πράξη
κοστίζει σταθερό χρόνο.
2. Σε κάθε βρόγχο επιλογών σε κάθε επανάληψη έχουμε ανεξάρτητα το κόστος όλων των
πράξεων σ’ αυτές.
3. Η δέσμευση μεταβλητής απλού τύπου κοστίζει σταθερό χώρο ενώ η δέσμευση ενός
πίνακα k θέσεων κοστίζει χρόνο και χώρο ανάλογο του k.
Το μέγεθος του προγράμματος στη μνήμη της μηχανής RAM που υλοποιεί έναν αλγόριθμο
καλείται στατική πολυπλοκότητα του αλγόριθμου.

1.4.3 Ασυμπτωτικοί συμβολισμοί


Στην ανάλυση της συμπεριφοράς ενός αλγόριθμου, στις περισσότερες περιπτώσεις δεν
είμαστε σε θέση ή δεν μας ενδιαφέρει να βρούμε ακριβείς τύπους για την υπολογιστική του
πολυπλοκότητα. Αυτό που έχει σημασία είναι να υπολογίσουμε τον ρυθμό αύξησης (rate of
growth) της πολυπλοκότητας όταν το μέγεθος εισόδου γίνεται πολύ μεγάλο. Για το σκοπό
αυτό χρησιμοποιούμε τους ασυμπτωτικούς συμβολισμούς (asymptotic notations).

Ο πιο συχνά χρησιμοποιούμενος συμβολισμός είναι ο Ο (μεγάλο όμικρον) ο οποίος


χρησιμοποιείται για να περιγράψει ένα πάνω όριο στο μέγεθος μιας συνάρτησης.
Συγκεκριμένα, για μια συνάρτηση g(n): No -> No συμβολίζουμε με Ο(g(n)) το σύνολο των
συναρτήσεων:
Ο(g(n)) = {f(n); υπάρχουν θετικές σταθερές c και no τέτοιες ώστε:
0 ≤ f(n) ≤ cg(n) για κάθε n ≥ no}
Για να δείξουμε ότι μια συνάρτηση f(n) είναι στοιχείο του συνόλου Ο(g(n)) γράφουμε f(n) =
O(g(n)). Ο παραπάνω τυπικός ορισμός πρακτικά σημαίνει ότι η f(n) αυξάνει όχι γρηγορότερα
από την g(n). Δεν έχει ιδιαίτερη σημασία η τιμή της σταθεράς c, δηλ. ο ορισμός περιγράφει
την τάξη μεγέθους μιας συνάρτησης και όχι την τιμή της.

Παράδειγμα 1
f(n) = 3n2+8n = 3n2+ O(n) = O(n2) (το ‘=’ σημαίνει περιέχεται).

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 19


Αλγόριθμοι και Δομές Δεδομένων

Παράδειγμα 2
Έστω οι συναρτήσεις:
f(n) = n
g(n) = n2
Όταν 0 < n < 1 ισχύει f(n) > g(n).
Όταν n ≥ 1 ισχύει f(n) ≤ g(n).
Άρα, μπορούμε να ισχυριστούμε ότι f = O(g).

Παράδειγμα 3
Έστω οι συναρτήσεις:
f(n) = 3n + 5√n
g(n) = 2n
Aπό τον ορισμό ισχύει f(n) = O(n), g(n) = O(n) παρά το γεγονός ότι η f είναι πάντα
μεγαλύτερη της g.

Αντίστοιχα με το συμβολισμό Ο, ο συμβολισμός Ω χρησιμοποιείται για να περιγράψει το


ασυμπτωτικό κάτω όριο στο μέγεθος μιας συνάρτησης και ορίζεται ως εξής:
Ω(g(n)) = {f(n); υπάρχουν θετικές σταθερές c και no τέτοιες ώστε:
f(n) ≥cg(n) για κάθε n ≥ no}
Πρακτικά, αν f(n) = Ω(g(n)), τότε αυτό σημαίνει ότι η f(n) αυξάνει τoυλάχιστον τόσο γρήγορα
όσο η g(n).

Τέλος, συχνά χρησιμοποιούμε και το συμβολισμό Θ που καλείται ασυμπτωτικό σφικτό


όριο και ορίζεται ως εξής:
f(n) = Θ(g(n)) αν και μόνο αν f(n) = Ο(g(n)) και f(n) = Ω(g(n))
και εκφράζει μια πιο ακριβή εκτίμηση στο μέγεθος μιας συνάρτησης, δηλ. οι f(n) και g(n)
αυξάνουν με τον ίδιο ρυθμό.
Οι παραπάνω βασικοί συμβολισμοί είναι ένα καλό εργαλείο για την περιγραφή της
ασυμπτωτικής συμπεριφοράς ενός αλγόριθμου αφού μας δίνουν μια σαφή ένδειξη για την
τάξη μεγέθους των συναρτήσεων που περιγράφουν την χρονική και χωρική πολυπλοκότητα
του αλγόριθμου σε σχέση με το μέγεθος εισόδου του.

Παράδειγμα
Θεωρούμε τους αλγόριθμους Α1, Α2, Α3, Α4, Α5 με αντίστοιχες ασυμπτωτικές
πολυπλοκότητες (για μέγεθος εισόδου n):
Θ(n) [γραμμική]

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 20


Αλγόριθμοι και Δομές Δεδομένων

Θ(nlogn)3
Θ(n2) [τετραγωνική]
Θ(n3) [κυβική]
Θ(2n) [εκθετική]
Έτσω ότι δίνουμε σε κάθε αλγόριθμο 1 sec να εργαστεί. Αν υποθέσουμε ότι η μονάδα
χρόνου είναι 1 msec (η εκτέλεση κάθε στοιχειώδους απαιτεί ένα 1 msec), τότε το πλήθος των
στοιχείων που προλαβαίνει να επεξεργαστεί κάθε αλγόριθμος φαίνεται στον επόμενο πίνακα:
Αλγόριθμος Πολυπλοκότητα* Πλήθος στοιχείων (κατά προσέγγιση)
Α1 n 1.000 = S1
A2 nlogn 140 = S2
A3 n2 31 = S3
A4 n3 10 = S4
A5 2n 9 = S5

* Δεν λαμβάνουμε υπόψη τις σταθερές.

Έστω τώρα ότι η ταχύτητα του υπολογιστή που εκτελεί τους αλγόριθμους
δεκαπλασιάζεται. Τότε το μέγιστο πλήθος στοιχείων που προλαβαίνει να επεξεργαστεί κάθε
αλγόριθμος μεταβάλλεται ως εξής:
Αλγόριθμος Πολυπλοκότητα Πλήθος στοιχείων (κατά προσέγγιση)
Α1 n 10S1
A2 nlogn 9S2
A3 n2 3,16S3
A4 n3 2,15S4
A5 2n S5 + 3,3

Στο παραπάνω παράδειγμα φαίνεται καθαρά η ασυμπτωτική συμπεριφορά κάθε


αλγόριθμου. Αυτό που μπορούμε να συμπεράνουμε άμεσα είναι ότι ο ρυθμός με τον οποίο
αυξάνεται (χειροτερεύει) η πολυπλοκότητα του αλγόριθμου σε συνάρτηση με την αύξηση του
μεγέθους της εισόδου είναι ενδεικτικός για την αποδοτικότητα του αλγόριθμου. Η αλλαγή του
υλικού / λογισμικού στον οποίο εκτελείται ο αλγόριθμος επηρεάζει την ασυμπτωτική του
πολυπλοκότητα κατά ένα σταθερό παράγοντα, αλλά δεν μεταβάλει το ρυθμό αύξησής της.
Επίσης, ο ρυθμός αύξησης δεν επηρεάζεται από σταθερούς παράγοντες και όρους κατώτερης
τάξης.
Στο σχήμα 2 δίνονται οι γραφικές παραστάσεις διαφόρων τάξεων πολυπλοκότητας που
δείχνουν το ρυθμό της αύξησης.

3
Όπου στις σημειώσεις χρησιμοποιεται ο λογάριθμος logn θεωρούμε ότι έχει βάση 2 εκτός κι αν
αναγράφεται διαφορετική βάση.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 21


Αλγόριθμοι και Δομές Δεδομένων

Αυτό που αναζητούμε γενικά είναι αλγόριθμοι με πολυωνυμική πολυπλοκότητα δεδομένου


ότι οι εκθετικοί αλγόριθμοι απαιτούν αφύσικα μεγάλο χρόνο εκτέλεσης όταν αυξάνει το
μέγεθος εισόδου. Tα αντίστοιχα προβλήματα που επιλύονται από πολυωνυμικούς
αλγόριθμους συνιστούν στην επιστήμη των υπολογιστών την κλάση των αποδοτικά
επιλύσιμων (υπολογίσιμων) προβλημάτων.
Υπάρχουν όμως και προβλήματα για τα οποία δεν υπάρχουν (και είναι μάλλον απίθανο
να υπάρξουν στο μέλλον) πολυωνυμικοί αλγόριθμοι για την επίλυσή τους. Τα προβλήματα
αυτά συνιστούν την κλάση των μη αποδοτικά επιλύσιμων ή υπολογιστικά δύσκολων
προβλημάτων τα οποία αποτελούν σημαντικό πεδίο έρευνας για τους θεωρητικούς της
υπολογιστικής πολυπλοκότητας.

f(n)
O(2n)

O(n3)

O(n2)

O(nlogn)

O(n)

O(logn)
O(1)

Σχήμα 2: Γραφικές παραστάσεις διαφορετικών τάξεων ασυμπτωτικής πολυπλοκότητας

Τέλος, όπως ήδη έχει αναφερθεί στην αρχή του κεφαλαίου, υπάρχουν και προβλήματα μη
επιλύσιμα ή ανοικτά, δηλ. δεν έχει βρεθεί μέχρι σήμερα αλγόριθμος που να τα επιλύει.
Χαρακτηριστικό παράδειγμα είναι το παρακάτω (πρόβλημα που τέθηκε από τον L. Collatz to
1937):
Υποθέστε ότι έχετε το παρακάτω πρόγραμμα σε C:
while (n != 1)
{
if (n%2 == 0) n = n/2; /* n άρτιος */
else n = 3n+1; /* n περιττός */
}
Αν το n έχει πχ. αρχική τιμή 7 τότε διαδοχικά παίρνει τις τιμές 22 -> 11 -> 34 -> 17 -> 52 -> 26
-> 13 -> 40 -> 20 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1, δηλ. το πρόγραμμα τερματίζει. Το γενικό

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 22


Αλγόριθμοι και Δομές Δεδομένων

όμως πρόβλημα αν το πρόγραμμα τερματίζει για κάθε φυσικό αριθμό n είναι ανοικτό, δηλ. δεν
ξέρουμε την απάντηση!

1.4.4 Τύποι πολυπλοκότητας


Η συμπεριφορά ενός αλγόριθμου εξαρτάται τις περισσότερες φορές από το είδος της
εισόδου του. Για παράδειγμα, ένα αλγόριθμος ταξινόμησης θα μπορούσε να επωφεληθεί από
μια είσοδο ήδη ταξινομημένη ή μερικώς ταξινομημένη. Με σκοπό να περιγράψουμε αυτή τη
συμπεριφορά, ορίζουμε ανάλογες πολυπλοκότητες οι οποίες και έχουν διαφορετικό τρόπο
ανάλυσης:
▪ Πολυπλοκότητα χειρότερης περίπτωσης (worst case complexity). Ορίζει τη χειρότερη
δυνατή συμπεριφορά ενός αλγόριθμου με οποιαδήποτε είσοδο. Η ανάλυση χειρότερης
περίπτωσης για έναν αλγόριθμο είναι σημαντική για τους κάτωθι λόγους:
- Ένας αλγόριθμος που φαίνεται καλός αν αναλυθεί στη χειρότερη περίπτωση,
συνήθως είναι καλός και στην πράξη.
- Για πολλούς αλγόριθμους η χειρότερη περίπτωση συμβαίνει αρκετά συχνά, πχ. όταν
ψάχνουμε σε μια βάση δεδομένων για κάποια πληροφορία που όμως δεν έχει
καταχωρηθεί στη βάση.
▪ Πολυπλοκότητα μέσης περίπτωσης (average case complexity). Η εκτίμησή της γίνεται
αφού κάνουμε πρώτα κάποιες υποθέσεις για την κατανομή των στοιχείων που αποτελούν
την είσοδο. Η σημασία της και η εγκυρότητά της είναι συνάρτηση του βαθμού με τον
οποίο προσεγγίζουν την πραγματικότητα οι υποθέσεις για την κατανομή της εισόδου. Τις
περισσότερες φορές δεχόμαστε ότι όλες οι είσοδοι σε έναν αλγόριθμο έχουν την ίδια
πιθανότητα να εμφανιστούν (ισοκατανομή) παρόλο που στην πράξη μια τέτοια υπόθεση
δεν είναι πάντα ρεαλιστική.
▪ Κατανεμημένη ή επιμερισμένη πολυπλοκότητα (amortized complexity).
Xρησιμοποιείται όταν ένα αλγόριθμος εφαρμόζεται πολλές φορές. Είναι χρήσιμη όταν οι
απαιτήσεις χρόνου του αλγόριθμου που εκτελείται κάθε φορά μεταβάλλονται σε ένα
μεγάλο διάστημα τιμών και μόνο κατανέμοντας τις απαιτήσεις αυτές μπορούμε να
εξάγουμε ενδεικτικά συμπεράσματα για τη συμπεριφορά του αλγόριθμου. Στην πράξη η
κατανεμημένη πολυπλοκότητα είναι συνήθως πολύ μικρότερη απ΄ την αντίστοιχη
χειρότερης περίπτωσης επειδή εκφράζει το μέσο χρόνο που προκύπτει όταν ο χρόνος
χειρότερης περίπτωσης επιμεριστεί σε μία ακολουθία πράξεων ή λειτουργιών (έχουμε δηλ.
ανάλυση μέσης περίπτωσης στο χρόνο).

1.4.5 Είδη αλγόριθμων


Οι αλγόριθμοι μπορούν να ταξινομηθούν σε πολλές κατηγορίες ανάλογα την απόδοση της
λύσης, των τεχνικών υλοποίησης που χρησιμοποιούν αλλά και της θεματικής περιοχής των
προβλημάτων που καλούνται να επιλύσουν.
Έτσι ένας αλγόριθμος είναι βέλτιστος (optimal) όταν επιτυγχάνει βέλτιστη
πολυπλοκότητα επίλυσης του προβλήματος ενώ μπορεί να προσεγγίζει τη βέλτιστη λύση του
προβλήματος για κάθε στιγμιότυπό του οπότε καλείται προσεγγιστικός ή ευριστικός
(approximation ή heuristic).

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 23


Αλγόριθμοι και Δομές Δεδομένων

Σε σχέση με τον τρόπο υλοποίησής του, ένας αλγόριθμος μπορεί να είναι αναδρομικός
(recursive). Στην περίπτωση αυτή ορίζεται μία αναδρομική σχέση και μία βάση (αρχική
συνθήκη). Πχ. το παραγοντικό ενός αριθμού n δίνεται από την αναδρομική σχέση n! = n(n-1)!
για n≥1 ενώ η βάση είναι 0! = 1.
Ο αλγόριθμος του Ευκλείδη για τον υπολογισμό του ΜΚΔ 2 θετικών ακεραίων όπως
παρουσιάστηκε παραπάνω μπορεί να μετατραπεί σε αναδρομικό ως εξής:
Αλγόριθμος ΜΚΔ_Ευκλείδη (x, y)
Δεδομένα //x, y: ακέραιοι //
Αρχή
Aν y>0 Τότε κάλεσε ΜΚΔ(y, x mod y)
Αποτελέσματα // τύπωσε x: O x είναι ο ΜΚΔ//
Τέλος ΜΚΔ_Ευκλείδη

Mπορεί να αποδειχθεί ότι η χρονική πολυπλοκότητα του αλγόριθμου του Ευκλείδη (είτε
της επαναληπτικής ή της αναδρομικής του υλοποίησης) είναι Ο(log(min(x,y))) (υποθέστε ότι τα
x, y είναι διαδοχικοί όροι της ακολουθίας Fibonacci η οποία ορίζεται ως εξής: F0 = 0, F1 = 1, Fn
= Fn-1 + Fn-2 για n >1).
Πολλές φορές οι αλγόριθμοι μπορούν να εκτελεστούν σε ένα υπολογιστή ο οποίος έχει
περισσότερες από μία ΚΜΕ αλλά και σε ένα σύνολο υπολογιστών όπου ο καθένας έχει 1
ΚΜΕ. Στις περιπτώσεις αυτές υλοποιούνται παράλληλοι αλγόριθμοι, όμως δεν μπορούν να
παραλληλοποιηθούν αποδοτικά όλοι οι αλγόριθμοι.
Επίσης, συχνά οι αλγόριθμοι κατηγοριοποιούνται ανάλογα με την περιοχή των
προβλημάτων που επιλύουν. Έτσι υπάρχουν οι αριθμητικοί αλγόριθμοι που εφαρμόζονται σε
προβλήματα αριθμητικής ανάλυσης, οι συνδυαστικοί για προβλήματα βελτιστοποίησης, οι
στοχαστικοί για προβλήματα θεωρίας ουρών κ.ά.

1.5 Δομές Δεδομένων


1.5.1 Η έννοια της δομής δεδομένων
Η διαχείριση των δεδομένων σε έναν υπολογιστή είναι μία πολύπλοκη διαδικασία. Ο
προγραμματιστής αρχικά πρέπει να οργανώσει τα δεδομένα του με τέτοιο τρόπο ώστε να
μπορεί στη συνέχεια με εύχρηστο τρόπο να εκτελέσει όλες τις απαιτούμενες πράξεις σ’ αυτά.
Έτσι τα δεδομένα δομούνται σε ομάδες που ονομάζονται δομές δεδομένων (data
structures). Κάθε δομή δεδομένων έχει ιδιαίτερα χαρακτηριστικά ανάλογα με τον τρόπο
που είναι αποθηκευμένα τα δεδομένα ή τον τρόπο προγραμματισμού των λειτουργιών πάνω
σ’ αυτά.
Οι πιο συνηθισμένες λειτουργίες (πράξεις) πάνω στις δομές δεδομένων είναι οι
παρακάτω:
• Προσπέλαση (access), για την ανάγνωση ενός ή περισσοτέρων δεδομένων της δομής.
• Εισαγωγή (insertion), για την εισαγωγή ενός ή περισσοτέρων δεδομένων στην δομή.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 24


Αλγόριθμοι και Δομές Δεδομένων

• Διαγραφή (deletion), για την διαγραφή ενός ή περισσοτέρων δεδομένων από την
δομή.
• Αντιγραφή (copying), για την αντιγραφή ενός ή περισσοτέρων δεδομένων από την
δομή σε μία άλλη δομή.
• Αναζήτηση (searching), για την εύρεση ενός ή περισσοτέρων δεδομένων της δομής.
• Ταξινόμηση (sorting), για την ταξινόμηση των δεδομένων της δομής με βάση κάποιο
κριτήριο από τον χρήστη
• Συγχώνευση (merging), για την συγχώνευση των δεδομένων δύο ή περισσότερων
δομών.
Οι δομές δεδομένων χωρίζονται σε δύο μεγάλες κατηγορίες ανάλογα με τον τρόπο
δέσμευσης της μνήμης που απαιτείται για την αποθήκευση των δεδομένων τους, τις στατικές
και τις δυναμικές. Οι στατικές δομές δεδομένων γνωρίζουν το ακριβές μέγεθος των δεδομένων
τους και δεσμεύουν εξ αρχής τον απαιτούμενο χώρο μνήμης γι’ αυτά. Τα δεδομένα
αποθηκεύονται με τον τρόπο αυτό σε συνεχόμενες θέσεις μνήμης.
Αντίθετα, οι δυναμικές δομές δεδομένων δεν έχουν σταθερό μέγεθος. Έτσι, κάθε φορά
που υπάρχει νέα εγγραφή – δεδομένο δεσμεύεται στην μνήμη ο απαραίτητος χώρος. Η
δέσμευση της μνήμης γίνεται με την τεχνικής της δυναμικής παραχώρησης μνήμης και
προφανώς τα δεδομένα δεν είναι αποθηκευμένα σε συνεχόμενες θέσεις μνήμης.

1.5.2 Παραδείγματα βασικών δομών δεδομένων


Στη συνέχεια της ενότητας αυτής περιγράφονται συνοπτικά οι βασικές δομές δεδομένων
που χρησιμοποιούνται κατά κόρον από τους προγραμματιστές και τις οποίες θα δούμε
αναλυτικά παρακάτω στο πλαίσιο των σημειώσεων αυτών. Μια γενική κατηγοριοποίησή τους
φαίνεται παρακάτω:

Απλές Σύνθετες
Γραμμικές Μη γραμμικές
Πίνακας Στοίβα Ουρά προτεραιότητας
Εγγραφή Ουρά Δένδρο
Γραμμική λίστα Γράφος
Δομή UNION-FIND

Πίνακας (Array)
Ο πίνακας είναι η πιο απλή και κοινά χρησιμοποιούμενη δομή δεδομένων. Είναι συνήθως
στατική δομή αφού κατά τον ορισμό του σε ένα πρόγραμμα ορίζεται και το μέγεθος του. Ένας
πίνακας μπορεί να είναι μιας διάστασης ή περισσοτέρων και περιέχει δεδομένα του ίδιου
τύπου.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 25


Αλγόριθμοι και Δομές Δεδομένων

Εγγραφή (Structure)
Απαρτίζεται από ένα σταθερό πλήθος στοιχείων πληροφορίας, όχι απαραίτητα του ίδιου
τύπου, που ονομάζονται πεδία. Αποτελεί τη μονάδα δόμησης των αρχείων.

Στοίβα (Stack)
Η στοίβα είναι μία δομή δεδομένων στην οποία τα δεδομένα εισάγονται το ένα μετά το
άλλο ενώ ο χρήστης έχει τη δυνατότητα να προσπελάσει μόνο το τελευταίο που έχει εισαχθεί
ενώ το πρώτο στοιχείο που εισήχθη θα προσπελαστεί τελευταίο (δομή LIFO - Last In First
Out).
Η υλοποίηση μίας στοίβας μπορεί να γίνει με απλό τρόπο χρησιμοποιώντας έναν
μονοδιάστατο πίνακα χρησιμοποιώντας πάντοτε έναν δείκτη που να δείχνει το πρώτο στοιχείο
της στοίβας που είναι και το τελευταίο που εισήχθη σ’ αυτήν.
Οι βασικές πράξεις σε μία στοίβα είναι:
• Empty() η οποία επιστρέφει 1 αν η στοίβα είναι άδεια και 0 στην αντίθετη περίπτωση.
• Push(x) η οποία τοποθετεί το στοιχείο x στην κορυφή της στοίβας
• Top() η οποία προσπελαύνει το στοιχείο που βρίσκεται στην κορυφή της στοίβας.
• Pop() η οποία προσπελαύνει και παράλληλα διαγράφει το στοιχείο που βρίσκεται
στην κορυφή της στοίβας.

Ουρά (Queue)
Η ουρά είναι μία δομή δεδομένων στην οποία τα δεδομένα εισάγονται το ένα μετά το άλλο
ενώ ο χρήστης έχει τη δυνατότητα να προσπελάσει τα δεδομένα με την αντίστοιχη φορά που
τα έχει εισάγει (δομή FIFO – First In First Out). Αποτελεί τυπικό παράδειγμα μιας ουράς
ανθρώπων σε μια τράπεζα.
Η υλοποίηση μίας ουράς μπορεί να γίνει με απλό τρόπο χρησιμοποιώντας έναν
μονοδιάστατο πίνακα και 2 δείκτες: έναν δείκτη που να δείχνει στο πρώτο στοιχείο της ουράς
και έναν στο τελευταίο.
Οι βασικές πράξεις σε μία ουρά είναι:
• Enqueue(x) η οποία τοποθετεί το στοιχείο x στο τέλος της ουράς
• Dequeue() η οποία προσπελαύνει και παράλληλα διαγράφει το στοιχείο που
βρίσκεται στην κορυφή της ουράς.

Γραμμική Λίστα (Linear List)


Στη γραμμική λίστα τα δεδομένα ως εγγραφές βρίσκονται σε οποιαδήποτε θέση ορίζοντας
τους κόμβους της λίστας και η σύνδεση μεταξύ τους πραγματοποιείται με δείκτες. Από κάθε
κόμβο ένας δείκτης δείχνει στον επόμενο κ.ο.κ.
Οι βασικές λειτουργίες σε μία λίστα είναι οι παρακάτω:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 26


Αλγόριθμοι και Δομές Δεδομένων

• First() η οποία επιστρέφει τον δείκτη προς τον πρώτο κόμβο


• Last() η οποία επιστρέφει τον δείκτη προς τον τελευταίο κόμβο
• Insert(x,y) η οποία ενθέτει τον κόμβο x προ του κόμβου y
• Delete(x) η οποία διαγράφει τον κόμβο x.

Ουρά Προτεραιότητας (Priority Queue)


Στην δομή αυτή κυρίαρχο ρόλο έχει η προτεραιότητα που επισυνάπτεται σε κάθε
δεδομένο της. Η προτεραιότητα αυτή ουσιαστικά είναι ένας αριθμός ο οποίος κατατάσσει με
κάποιο τρόπο τα δεδομένα.

Δένδρο (Tree)
Τα δένδρα χρησιμοποιούνται για την αναπαράσταση ιεραρχικών σχέσεων μεταξύ των
στοιχείων. Αντίθετα με τις λίστες, σε ένα δένδρο από κάθε κόμβο δεν ξεκινά ένας μόνο δείκτης
που να δείχνει μόνο σε ένα κόμβο αλλά περισσότεροι οι οποίοι δείχνουν σε κόμβους που
ονομάζονται απόγονοι του αρχικού. Ο πρώτος κόμβος στο δένδρο ονομάζεται ρίζα ενώ οι
κόμβοι που δεν δείχνουν σε απoγόνους ονομάζονται φύλλα.
Οι βασικές πράξεις που υποστηρίζει ένα δένδρο, γνωστές στη βιβλιογραφία ως πράξεις
του λεξικού (dictionary), είναι οι ακόλουθες:
• Search(x) η οποία αναζητά και επιστρέφει το στοιχείο x
• Insert(x) η οποία εισάγει το στοιχείο x στο δένδρο
• Delete(x) η οποία διαγράφει το στoιχείο x από το δένδρο.

Γράφος (Graph)
Η δομή αυτή αποτελείται από κόμβους δεδομένων (κορυφές) στους οποίους όμως δεν
υπάρχει κάποια συγκεκριμένη ιεραρχική οργάνωση. Οι κόμβοι ενώνονται μεταξύ τους μέσω
ενός συνόλου ακμών.

Δομή UNION-FIND
Η δομή αυτή χρησιμοποιείται για τη διαχείριση ξένων μεταξύ τους υποσυνόλων από
στοιχεία τα οποία ανήκουν σε ένα σταθερό και γνωστό εκ των προτέρων σύνολο που καλείται
σύμπαν. Υποστηρίζει τις πράξεις:
• Μakeset(x) η οποία δημιουργεί ένα καινούργιο μονοσύνολο με το στοιχείο x.
• Union(x, y) η οποία ενώνει τα δυναμικά σύνολα που περιέχουν τα x και y σε ένα νέο
σύνολο καταστρέφοντας έτσι τα προηγούμενα.

• Find(x) η οποία επιστρέφει το (ένα δείκτη στο) όνομα του συνόλου που περιέχει το x.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 27


Αλγόριθμοι και Δομές Δεδομένων

Ερωτήσεις Κεφαλαίου
1. Τι είναι ο αλγόριθμος; Ποια κριτήρια πρέπει να ικανοποιεί;
2. Με ποιους τρόπους αναπαρίστανται οι αλγόριθμοι;
3. Ποιοι είναι οι βασικοί τύποι εντολών ενός αλγόριθμου;
4. Περιγράψτε τον αλγόριθμο του Ευκλείδη.
5. Περιγράψτε τα χαρακτηριστικά της μηχανής RAM.
6. Δώστε τα 2 μέτρα της απόδοσης ενός αλγόριθμου.
7. Δώστε τους βασικούς ασυμπτωτικούς συμβολισμούς που χρησιμοποιούνται στην
εκτίμηση της πολυπλοκότητας ενός αλγόριθμου.
8. Περιγράψτε τους βασικούς τύπους πολυπλοκότητας ενός αλγόριθμου.
9. Τι είναι η δομή δεδομένων; Ποια η διαφορά της στατικής με τη δυναμική δομή δεδομένων.
10. Δώστε τις βασικές πράξεις πάνω στα στοιχεία μιας δομής δεδομένων.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 28


Αλγόριθμοι και Δομές Δεδομένων

ΚΕΦΑΛΑΙΟ 2: ΓΡΑΜΜΙΚΕΣ ΔΟΜΕΣ ΔΕΔΟΜΕΝΩΝ


Σκοπός του κεφαλαίου αυτού είναι να παρουσιαστούν αναλυτικά οι πιο βασικές δομές
δεδομένων που χρησιμοποιούνται τόσο στην κύρια όσο και στην δευτερεύουσα μνήμη.
Αρχικά περιγράφονται οι γραμμικές δομές που χρησιμοποιούνται στην κύρια μνήμη δηλαδή ο
πίνακας, η στοίβα, η ουρά και η λίστα. Στις δομές αυτές ορίζεται μία γραμμική σχέση διάταξης
για δύο οποιαδήποτε διαδοχικά στοιχεία τους, δηλ. πρόκειται ουσιαστικά για μονοδιάστατες
δομές. Για κάθε δομή αναλύεται η λειτουργία της καθώς και οι βασικές πράξεις που
υποστηρίζονται και δίνονται οι σχετικοί αλγόριθμοι υλοποίησής τους.

2.1 Πίνακες (Αrrays)


Ο πίνακας (array) είναι η πιο απλή και ευρέως χρησιμοποιούμενη δομή δεδομένων.
Υποστηρίζεται σε όλες τις γλώσσες προγραμματισμού και είναι συνήθως στατική δομή αφού
κατά τον ορισμό του σε ένα πρόγραμμα ορίζεται και το μέγεθός του.
Ένας πίνακας μπορεί να είναι μιας ή περισσοτέρων διαστάσεων. Παρακάτω δίνεται η
εποπτική παράσταση ενός πίνακα μιας διάστασης και ενός δύο διαστάσεων:

Σχήμα 3: Παραδείγματα πινάκων

Σε ένα πίνακα περιέχονται δεδομένα του ίδιου τύπου. Είναι σημαντικό ο προγραμματιστής
να κατανοεί κάθε φορά το χώρο μνήμης που καταλαμβάνουν οι δομές που είναι
αποθηκευμένες σε κάθε θέση του πίνακα. Τα στοιχεία ενός πίνακα αποθηκεύονται σε
γειτονικές θέσεις μνήμης και κάθε στοιχείο καταλαμβάνει το ίδιο χώρο μνήμης. Αν έχουμε ένα
μονοδιάστατο πίνακα Ν στοιχείων και κάθε στοιχείο καταλαμβάνει μία λέξη μνήμης τότε, όταν
δηλώνεται ο πίνακας σε μία γλώσσα προγραμματισμού όπως η C, δεσμεύονται εξαρχής Ν
διαδοχικές θέσεις μνήμης για τον πίνακα.
Στη γενική περίπτωση κάθε στοιχείο ενός πίνακα καταλαμβάνει τόσες θέσεις μνήμης
ανάλογα με τον τύπο του και με την αρχιτεκτονική της κεντρικής μνήμης του υπολογιστή. Έτσι
σε υπολογιστές όπου η λέξη μνήμης είναι ισοδύναμη με ένα byte:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 29


Αλγόριθμοι και Δομές Δεδομένων

• Aν ο πίνακας περιέχει προσημασμένους ή μη ακεραίους αριθμούς, τότε κάθε στοιχείο του


καταλαμβάνει 1 byte και επομένως 1 λέξη μνήμης.
• Αν ο πίνακας περιέχει ακέραιους αριθμούς, τότε κάθε στοιχείο του καταλαμβάνει 2 bytes
και επομένως 2 λέξεις μνήμης.
• Αν ο πίνακας περιέχει μεγάλους ακέραιους ή αριθμούς κινητής υποδιαστολής απλής
ακρίβειας τότε κάθε στοιχείο του καταλαμβάνει 4 bytes και επομένως 4 λέξεις μνήμης.
• Αν ο πίνακας περιέχει αριθμούς κινητής υποδιαστολής διπλής ακρίβειας τότε κάθε
στοιχείο του καταλαμβάνει 8 bytes και επομένως 8 λέξεις μνήμης.
Παρακάτω δίνονται μερικά παραδείγματα πινάκων στη γλώσσα C:
- int num[100]. Στην περίπτωση αυτή ορίζεται ένας μονοδιάστατος πίνακας ο οποίος
περιέχει 100 ακέραιους αριθμούς.
- char symbols[20]. Στην περίπτωση αυτή ορίζεται ένας μονοδιάστατος πίνακας ο οποίος
περιέχει μία σειρά 20 χαρακτήρων.
- float num[100][100]. Στην περίπτωση αυτή ορίζεται ένας διδιάστατος πίνακας 100 x 100 ο
οποίος περιέχει συνολικά 10.000 πραγματικούς αριθμούς.

Επειδή ακριβώς πρέπει να γνωρίζουμε εξαρχής το μέγεθος του πίνακα ώστε να είναι
δυνατή η δέσμευση του απαιτούμενου χώρου μνήμης, ο πίνακας είναι μία αυστηρά στατική
δομή δεδομένων. Το προκαθορισμένο όμως μέγεθος μνήμης επιτρέπει να προσπελάσουμε
οποιαδήποτε θέση του πίνακα σε Ο(1) χρόνο κι αυτή ακριβώς η ιδιότητα κάνει τους πίνακες
σημαντικούς απ΄ τη σκοπιά της υπολογιστικής πολυπλοκότητας (βλ. μοντέλο υπολογισμού
RAM).

2.1.1 Μερικοί τύποι πινάκων


Στην παράγραφο αυτή θα παρουσιάσουμε μερικούς τύπους πινάκων που
χρησιμοποιούνται συχνά στην επίλυση προβλημάτων.
1. Συμμετρικοί πίνακες. Ένα πίνακας είναι συμμετρικός όταν είναι δύο διαστάσεων και
τα στοιχεία του Αij ικανοποιούν την σχέση Αij = Aji για κάθε i και j.
Παράδειγμα τέτοιου πίνακα είναι ο:
7 9 2 5 6
9 7 2 8 1
2 2 6 3 2
5 8 3 5 3
6 1 2 3 4

Για ένα συμμετρικό πίνακα αρκεί να αποθηκεύσουμε, πχ. σε ένα νέο μονοδιάστατο
πίνακα, μόνο τα στοιχεία της διαγωνίου του κι αυτά που βρίσκονται πάνω (ή κάτω)
απ΄ αυτήν. Με τον τρόπο αυτό μειώνουμε σχεδόν στο μισό το χώρο που χρειαζόμαστε
αφού, αν ο συμμετρικός πίνακας περιέχει Ν2 στοιχεία, τότε ο νέος πίνακας θα περιέχει
Ν(Ν+1)/2 στοιχεία.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 30


Αλγόριθμοι και Δομές Δεδομένων

2. Τριγωνικοί πίνακες. Ένα πίνακας είναι τριγωνικός όταν είναι δύο διαστάσεων και τα
στοιχεία του Αij ικανοποιούν την σχέση Αij = 0 για i > j, οπότε έχουμε τον κάτω
τριγωνικό πίνακα, ή Αij = 0 για i < j, οπότε ο πίνακας είναι πάνω τριγωνικός.
Πχ. ο παρακάτω πίνακας είναι κάτω τριγωνικός:
7 0 0 0 0
9 7 0 0 0
2 2 6 0 0
5 8 3 5 0
6 1 2 3 4

Αν υποθέσουμε ότι έχουμε έναν κάτω τριγωνικό πίνακα, πάλι μπορούμε να


αποθηκεύσουμε μόνο τα μη μηδενικά στοιχεία του σε ένα νέο μονοδιάστατο πίνακα
τοποθετώντας τα πχ. από πάνω προς τα κάτω και σε κάθε γραμμή από αριστερά
προς τα δεξιά. Έτσι το στοιχείο Αij αποθηκεύεται στη θέση i(i-1)/2 + j του νέου πίνακα
και απαιτούνται κι εδώ Ν(Ν+1)/2 θέσεις.
3. Τριδιαγώνιοι πίνακες. Ένας πίνακας Αij είναι τριδιαγώνιος όταν είναι δύο
διαστάσεων και τα μόνα μη μηδενικά στοιχεία του είναι αυτά που ικανοποιούν την
σχέσεις i = j ή i-j =1 ή j–i = 1 δηλ. είναι τα στοιχεία της διαγωνίου του καθώς και τα
γειτονικά της.
Πχ. ο παρακάτω πίνακας είναι τριδιαγώνιος:
7 9 0 0 0
9 7 2 0 0
0 2 6 3 0
0 0 3 5 3
0 0 0 3 4

Και εδώ, αντί για ολόκληρο του πίνακα, μπορούμε να αποθηκεύσουμε μόνο τα μη
μηδενικά του στοιχεία χρησιμοποιώντας πχ. τρεις μονοδιάστατους πίνακες, έναν για
τα στοιχεία της διαγωνίου, έναν για αυτά που είναι πάνω απ΄ αυτήν και έναν για τα
στοιχεία που είναι κάτω από τη διαγώνιο. Με τον τρόπο αυτό προκύπτουν συνολικά
3Ν-2 θέσεις.
4. Αραιοί πίνακες. Ένας πίνακας είναι αραιός όταν πολλά στοιχεία του (συνήθως πάνω
από το 70%) είναι 0.
Παράδειγμα αραιού πίνακα:
0 0 5 0 0 0
0 7 0 9 0 0
2 0 0 0 0 6
4 0 3 0 0 0

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 31


Αλγόριθμοι και Δομές Δεδομένων

Ένας τρόπος για να αποθηκεύσουμε έναν αραιό πίνακα είναι για κάθε μη μηδενικό
στοιχείο του να κρατάμε σε ένα νέο μονοδιάστατο πίνακα μία τριάδα στοιχείων που
δείχνουν τη γραμμή, τη στήλη και την τιμή του στοιχείου αντίστοιχα. Με τον τρόπο
αυτό, για k μη μηδενικά στοιχεία, χρειαζόμαστε μόνο 3k θέσεις.
Ένας άλλος τρόπος είναι να μετατρέψουμε τον αραιό πίνακα σε δυαδικό όπου κάθε
στοιχείο του έχει τιμή 1 αν στην αντίστοιχη θέση του αραιού πίνακα υπάρχει μη
μηδενική τιμή και 0 διαφορετικά και να αποθηκεύσουμε τα μη μηδενικά στοιχεία σε
έναν μονοδιάστατο πίνακα με τη σειρά από πάνω προς τα κάτω και σε κάθε γραμμή
του αραιού πίνακα από αριστερά προς τα δεξιά. Για τον αραιό πίνακα του
παραδείγματος οι πίνακες που προκύπτουν είναι:
0 0 1 0 0 0
0 1 0 1 0 0
1 0 0 0 0 1
1 0 1 0 0 0
και
5 7 9 2 6 4 3

Στον τρόπο αυτό οι οικονομία χώρου οφείλεται στο γεγονός ότι ο δυαδικός πίνακας
αποθηκεύει σε κάθε θέση του την τιμή ενός bit μόνο.

2.1.2 Συμβολοσειρές
Μία σημαντική κατηγορία μονοδιάστατων πινάκων είναι οι πίνακες χαρακτήρων όπου
κάθε στοιχείο τους είναι ένας χαρακτήρας. Οι πίνακες αυτοί ονομάζονται συμβολοσειρές
(strings) και είναι αλφαριθμητικοί. Λόγω της ευρείας τους χρήσης σε πλήθος εφαρμογών,
κάθε γλώσσα προγραμματισμού προσφέρει ένα σύνολο συναρτήσεων οι οποίοι έχουν σκοπό
να εκτελέσουν συγκεκριμένες λειτουργίες πάνω σε συμβολοσειρές όπως συνένωση
συμβολοσειρών, αποκοπή στοιχείων ή και τμημάτων της συμβολοσειράς κ.α.
Είναι σημαντικό να σημειώσουμε ότι κάθε στοιχείο του πίνακα μπορεί να είναι ένας
χαρακτήρας αλλά και από μόνο του μία συμβολοσειρά. Στην περίπτωση αυτή μπορεί η
συμβολοσειρά να είναι σταθερού ή μεταβλητού μεγέθους. Στην πρώτη περίπτωση κατά τον
ορισμό του πίνακα δεσμεύεται ο συνολικός απαραίτητος χώρος μνήμης για την αποθήκευση
των στοιχείων του. Δηλαδή αν ο πίνακας είναι ΝxΝ τότε στην κάθε γραμμή δεσμεύεται χώρος
για μια συμβολοσειρά Ν χαρακτήρων ανεξάρτητα από το πραγματικό μέγεθός της. Αυτό έχει
σαν αποτέλεσμα να σπαταλάται αρκετός χώρος.
Στην δεύτερη περίπτωση συμβολοσειρών μεταβλητού μεγέθους σε κάθε γραμμή δεν
δεσμεύεται χώρος Ν χαρακτήρων αλλά μόνο 2 στοιχείων. Το πρώτο περιέχει το πλήθος των
χαρακτήρων της συμβολοσειράς της συγκεκριμένης γραμμής (δηλ. το μήκος της) ενώ το
δεύτερο στοιχείο είναι ένας δείκτης που δείχνει στην διεύθυνση μνήμης που περιέχει τον
πρώτο χαρακτήρα της συμβολοσειράς. Οι χαρακτήρες της συμβολοσειράς αποθηκεύονται σε
συνεχόμενες θέσεις μνήμης. Έτσι το πρόγραμμα κατά την εκτέλεσή του, όταν θέλει να
επεξεργαστεί, έστω την πρώτη συμβολοσειρά του πίνακα, προσπελαύνει τη διεύθυνση που
μνήμης όπου είναι καταχωρημένος ο πρώτος χαρακτήρας και ανακτά στη συνέχεια το

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 32


Αλγόριθμοι και Δομές Δεδομένων

περιεχόμενο των συνεχόμενων θέσεων μνήμης τις οποίες δείχνει ο αριθμός που δίνει το μήκος
της συμβολοσειράς.

2.1.3 Σάρωση στοιχείων πίνακα


Η πράξη της σάρωσης είναι έχει εφαρμογή σε πλήθος προβλημάτων όπως εύρεση του
μεγαλύτερου ή του μικρότερου στοιχείου ενός πίνακα, αναζήτηση ενός στοιχείου μέσα στον
πίνακα, υπολογισμός του αθροίσματος ή του μέσου όρου των στοιχείων του πίνακα κλπ.
Μπορεί να υλοποιηθεί τόσο σε μονοδιάστατους πίνακας όσο και σε πίνακες περισσότερων
διαστάσεων. Στη συνέχεια δίνονται δύο παραδείγματα εφαρμογής της πράξης της σάρωσης
σε έναν μονοδιάστατο πίνακα Ν ακεραίων Α[1..N] στον οποίο οι αριθμοί έχουν αποθηκευτεί
με τυχαία σειρά.

Παράδειγμα 1: Εύρεση του μεγαλύτερου στοιχείου


Ο προφανής τρόπος για να βρούμε το μεγαλύτερο στοιχείο είναι να διατρέξουμε τον
πίνακα πχ. από αριστερά προς τα δεξιά και κάθε φορά να ενημερώνουμε την τιμή μιας
βοηθητικής μεταβλητής με την τιμή που μέχρι εκείνη τη στιγμή είναι η μεγαλύτερη. Όταν
ολοκληρωθεί η σάρωση του πίνακα, η μεταβλητή αυτή θα περιέχει το μικρότερο στοιχείο του.
Παρακάτω δίνεται ο ψευδοκώδικας για τον συγκεκριμένο αλγόριθμο ο οποίος επιστρέφει την
τιμή του μεγαλύτερου στοιχείου στη μεταβλητή Max:

Αλγόριθμος Eύρεσης Max σε μονοδιάστατο πίνακα


Δεδομένα // A[N], Μax: ακέραιοι //
Αρχή
Max = A[1]
Για i = 2 Μέχρι i = N με Βήμα 1 Εκτέλεσε
Aν Α[i] > Μax Tότε Μax = A[i]
Τέλος Αν
Τέλος Επανάληψης
Αποτελέσματα // Μax //
Tέλος Eύρεσης Μax

Mπορούμε να τροποποιήσουμε τον παραπάνω αλγόριθμο ώστε στην έξοδο να δίνει και τη
θέση του μεγαλύτερου στοιχείου. Χρησιμοποιούμε μια μεταβλητή επιπλέον η οποία
αρχικοποιείται στην τιμή 1 πριν την είσοδο στο loop και σε κάθε επανάληψη ενημερώνεται με
τη θέση του μεγαλύτερου στοιχείου μέχρι εκείνη τη στιγμή.
Είναι εύκολο να δείτε ότι ο προηγούμενος αλγόριθμος εκτελεί ακριβώς N-1 συγκρίσεις και
είναι βέλτιστος: κάθε αλγόριθμος για την εύρεση της μεγαλύτερης από Ν τιμές σε τυχαία
σειρά θα πρέπει να εκτελεί τουλάχιστον N-1 συγκρίσεις αφού για να διασφαλίσει ότι η τιμή
που δίνει στην έξοδο είναι όντως η μεγαλύτερη θα πρέπει όλα τα υπόλοιπα Ν-1 στοιχεία να
έχουν συγκριθεί τουλάχιστον μία φορά με αυτήν και να έχουν χάσει.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 33


Αλγόριθμοι και Δομές Δεδομένων

Παράδειγμα 2: Εύρεση του μεγαλύτερου και μικρότερου στοιχείου


Ο προηγούμενος αλγόριθμος μπορεί να τροποποιηθεί εύκολα ώστε να επιστρέφει το
μικρότερο στοιχείο του πίνακα αντί του μεγαλύτερου. Για να βρούμε τώρα το μεγαλύτερο και
το μικρότερο στοιχείο αρκεί να καλέσουμε ανεξάρτητα κάθε φορά τον αντίστοιχο αλγόριθμο.
Με τον τρόπο αυτό θα εκτελεστούν ακριβώς 2Ν-2 συγκρίσεις. Μπορούμε να κάνουμε κάτι
καλύτερο απ΄ αυτό; Η απάντηση είναι ναι. Η ιδέα είναι να συγκρίνουμε ανά ζεύγη τα στοιχεία
του πίνακα και να βρίσκουμε το μεγαλύτερο και το μικρότερο κάθε φορά. Καθώς σαρώνουμε
τα στοιχεία του πίνακα συγκρίνουμε κάθε φορά το μεγαλύτερο και μικρότερο κάθε ζεύγους με
το τρέχον μεγαλύτερο και μικρότερο. Υποθέτοντας για λόγους ευκολίας ότι το Ν είναι άρτιος, ο
αλγόριθμος θα είναι ο εξής:

Αλγόριθμος Eύρεσης Max και Μin σε μονοδιάστατο πίνακα


Δεδομένα // A[N], Μax, Min: ακέραιοι //
Αρχή
Αν A[1] > Α[2] Τότε Max = A[1]; Min = A[2]
Aλλιώς Max = A[2]; Min = A[1]
Τέλος Αν
Για i = 3 Μέχρι i = N με Βήμα 2 Εκτέλεσε
Aν Α[i] > Α[i+1] Tότε
Αν A[i] > Max Tότε Μax = A[i] Τέλος Αν
Αν A[i+1] < Min Tότε Μin = A[i+1] Τέλος Αν
Aλλιώς
Αν A[i+1] > Max Tότε Μax = A[i+1] Τέλος Αν
Αν A[i] < Min Tότε Μin = A[i] Τέλος Αν
Tέλος Αν
Τέλος Επανάληψης
Αποτελέσματα // Μax, Min //
Tέλος Eύρεσης Μax και Μin

Μπορείτε να δείτε ότι ο παραπάνω αλγόριθμος εκτελεί 3(Ν-2)/2+1 = 3N/2-2 συγκρίσεις


που αποτελεί σημαντική βελτίωση σε σχέση με τις 2Ν-2 συγκρίσεις της κλασικής
προσέγγισης. Αν το Ν είναι περιττός, τότε το τελευταίο στοιχείο μένει δίχως ζευγάρι και
απαιτούνται άλλες 2 το πολύ συγκρίσεις ακόμη, δηλ. έχουμε το πολύ 3(Ν-1)/2 συγκρίσεις. Οι
αλλαγές στον αλγόριθμο ώστε να λειτουργεί σε κάθε περίπτωση είναι απλές και αφήνονται ως
άσκηση.

2.2 Στοίβες (Stacks)


Μία άλλη γραμμική δομή δεδομένων είναι η στοίβα (stack) στην οποία τα δεδομένα της
αποθηκεύονται διαδοχικά, το ένα μετά το άλλο. Όταν ο χρήστης εισάγει ένα στοιχείο αυτό

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 34


Αλγόριθμοι και Δομές Δεδομένων

αποθηκεύεται στο τέλος της στοίβας. Όταν όμως θέλει να εξάγει ένα στοιχείο από τη στοίβα
τότε έχει τη δυνατότητα να προσπελάσει μόνο το στοιχείο που έχει εισαχθεί τελευταίο. Αυτό
σημαίνει ότι το πρώτο στοιχείο που εισήχθη στη στοίβα θα προσπελαστεί τελευταίο. Η
λειτουργία αυτή κατατάσσει τη στοίβα στις δομές LIFO (Last In First Out).
Συχνά στην καθημερινή μας ζωή συναντάμε περιπτώσεις ή προβλήματα που απαιτούν
την εφαρμογή της δομής της στοίβας για την επίλυσή τους. Μία στοίβα πιάτων ή ένας
κερματοδέκτης είναι χαρακτηριστικά παραδείγματα. Πρέπει να τονιστεί παράλληλα ότι η
στοίβα αποτελεί βασική δομή δεδομένων και για την επιστήμη των υπολογιστών. Έτσι, όταν
στον προγραμματισμό καλείται μία συνάρτηση και στην συνέχεια μία άλλη κ.ο.κ. τότε αυτή η
διαδρομή των κλήσεων «κρατείται» σε μία στοίβα κατά την εκτέλεση του προγράμματος.
Ποιες λειτουργίες όμως εφαρμόζονται σε μία στοίβα;
Έστω έχουμε μία στοίβα στην οποία έχουμε αποθηκευμένα τα στοιχεία X, Y, Z και V
όπως φαίνεται στο παρακάτω σχήμα.

Εισαγωγή Εξαγωγή
στοιχείων στοιχείων

Σχήμα 4: Η δομή της στοίβας

Στη στοίβα αυτή υπάρχουν δύο βασικές επιλογές λειτουργιών.


Ένας χρήστης μπορεί να ενθέσει ένα στοιχείο στη στοίβα, έστω P με τη βοήθεια της
πράξης push(P). Τότε η στοίβα αποκτά την ακόλουθη μορφή:

Σχήμα 5: Η στοίβα μετά την ένθεση του στοιχείου P

Επίσης ένας χρήστης μπορεί να απωθήσει (εξάγει) ένα στοιχείο από τη στοίβα με την
εκτέλεση της πράξης pop(). Ουσιαστικά η πράξη pop προσπελαύνει το τελευταίο στοιχείο της

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 35


Αλγόριθμοι και Δομές Δεδομένων

στοίβας (το στοιχείο της κορυφής) και στη συνέχεια το σβήνει από τη στοίβα. Τότε η στοίβα
αποκτά την ακόλουθη μορφή:

Σχήμα 6: Η στοίβα μετά την απώθηση του στοιχείου της κορυφής

Υπάρχουν βέβαια και άλλες πράξεις που εφαρμόζονται σε μία στοίβα, λιγότερο
σημαντικές από τις προηγούμενες δύο. Ειδικότερα, οι βασικές πράξεις που ορίζονται σε μία
στοίβα είναι:
• Push (x) η οποία τοποθετεί το στοιχείο x στην κορυφή της στοίβας
• Pop(), η οποία προσπελαύνει και παράλληλα διαγράφει το στοιχείο που βρίσκεται
στην κορυφή της στοίβας.
• Top() η οποία προσπελαύνει το στοιχείο που βρίσκεται στην κορυφή της στοίβας.
• Empty() η οποία επιστρέφει 1 (ή ΤRUE) αν η στοίβα είναι άδεια και 0 (ή FALSΕ) στην
αντίθετη περίπτωση.

2.2.1 Υλοποίηση στοίβας


Μία στοίβα μπορεί να υλοποιηθεί ως ένας πίνακας καθορισμένου ή μη μεγέθους ή μιας
απλής λίστας. Αυτό σημαίνει ότι η στοίβα, ανάλογα τον τρόπο υλοποίησής της, μπορεί να
είναι είτε μία στατική δομή ή μία δυναμική.
Η πιο συνηθισμένη μορφή μιας στοίβας είναι ένας πίνακας περιορισμένου μεγέθους. Η
στοίβα επομένως μπορεί να υλοποιηθεί με απλό τρόπο χρησιμοποιώντας έναν μονοδιάστατο
πίνακα μεγέθους Ν, έστω χρησιμοποιώντας πάντοτε έναν δείκτη που να δείχνει το πρώτο
στοιχείο της στοίβας που είναι και το τελευταίο που εισήχθη σ’ αυτήν και ονομάζεται κεφαλή
της στοίβας (μεταβλητή Head). Κατά την υλοποίηση των πράξεων της στοίβας
χρησιμοποιείται πάντοτε μία δυαδική (τύπου boolean) μεταβλητή η οποία ενημερώνει μετά την
αίτηση εκτέλεσης μιας πράξης αν αυτή εκτελέστηκε επιτυχώς ή όχι (έστω μεταβλητή Check).

Η πράξη Push()
Αν θεωρήσουμε ότι στοίβα περιέχει ακεραίους αριθμούς (ο πίνακας Stack[N] δηλώνεται
ως ένας πίνακας ακεραίων) τότε η πράξη ένθεσης καλείται όταν θέλουμε να εισάγουμε ένα
στοιχείο στη στοίβα έστω Num (η μεταβλητή Num είναι επίσης τύπου ακεραίου). Η push είναι
μία ανεξάρτητη διαδικασία η οποία καλείται από το κύριο πρόγραμμα όταν θέλουμε να
εισάγουμε ένα στοιχείο στη στοίβα. Συντάσσεται επομένως ως μία συνάρτηση στην οποία

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 36


Αλγόριθμοι και Δομές Δεδομένων

μεταφέρεται ως αρχικό δεδομένο και η τιμή του στοιχείου Num (η συνάρτηση καλείται με
παράμετρο τη μεταβλητή Num). Όταν καλείται η push επομένως έχει την μορφή Push(Num).
Όταν γίνει αίτηση για ένθεση (Push(Num)) πρέπει να εξεταστεί αρχικά το γεγονός αν
υπάρχει διαθέσιμος χώρος στη στοίβα για να εισαχθεί το στοιχείο Νum. Τονίσαμε ότι η
μεταβλητή Head δείχνει στο τελευταίο στοιχείο της στοίβας (στην κεφαλή της). Αν επομένως
στο παράδειγμά μας, η κεφαλή δείχνει στο στοιχείο της θέσης N τότε δεν υπάρχει χώρος στη
στοίβα. Άρα δεν εκτελείται η πράξη και αυτό δηλώνεται θέτοντας την μεταβλητή Check ως
ψευδή (FALSE). Στην αντίθετη περίπτωση υπάρχει χώρος για το στοιχείο Νum και τότε η
μεταβλητή Ηead αυξάνεται κατά ένα ώστε να δείχνει στην επόμενη θέση της στοίβας που είναι
κενή. Στη συνέχεια το στοιχείο Num εισάγεται στη θέση που δείχνει η Ηead. Η ένθεση
εκτελείται επιτυχώς και η μεταβλητή Check τίθεται αληθής (TRUE).
Παρακάτω δίνεται ο αλγόριθμος υλοποίησης της πράξης push σε ψευδοκώδικα:

Αλγόριθμος Push(Num)
Δεδομένα // Stack[N], Head, Num: ακέραιοι; Check: δυαδική //
Αρχή
Aν Head < N
Tότε
Ηead = Head+1
Stack[Head] = Num
Check = TRUE
Αλλιώς Check = FALSE
Τέλος Αν
Αποτελέσματα // Check //
Τέλος Push

Η πράξη Pop()
H πράξη απώθησης καλείται όταν θέλουμε να διαβάσουμε και να σβήσουμε παράλληλα
το στοιχείο-κεφαλή της στοίβας έστω Num. Η Pop είναι μία ανεξάρτητη διαδικασία και
συντάσσεται ως μία συνάρτηση μια τιμή τύπου ακεραίου. Όταν καλείται η pop επομένως έχει
την μορφή Pop() και επιστρέφει την κεφαλή της στοίβας εφόσον εκτελεστεί επιτυχώς.
Όταν γίνει αίτηση για απώθηση (Pop()) πρέπει να εξεταστεί αρχικά αν υπάρχουν στοιχεία
στη στοίβα. Eίπαμε ότι η μεταβλητή Head δείχνει στο τελευταίο στοιχείο της στοίβας. Αν
επομένως η Ηead έχει την τιμή 0 σημαίνει ότι η στοίβα είναι κενή και δεν εκτελείται η πράξη.
Τότε στη μεταβλητή Check τίθεται η τιμή FALSE. Στην αντίθετη περίπτωση υπάρχουν στοιχεία
στη στοίβα και η pop επιστρέφει την κεφαλή της που είναι το στοιχείο Stack[Head]. Η
μεταβλητή Ηead μειώνεται κατά ένα ώστε να δείχνει στο προηγούμενο στοιχείο της στοίβας
που θα αποτελεί στη συνέχεια και τη νέα κεφαλή. Η απώθηση εκτελείται επιτυχώς και η
μεταβλητή Check παίρνει την τιμή TRUE.
Παρακάτω δίνεται ο αλγόριθμος υλοποίησης της πράξης pop σε ψευδοκώδικα:
Αλγόριθμος Pop()

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 37


Αλγόριθμοι και Δομές Δεδομένων

Δεδομένα // Stack[N], Head, Num: ακέραιοι; Check: δυαδική //


Αρχή
Aν Head > 0
Tότε
Num = Stack[Head]
Head = Head-1
Check = TRUE
Αλλιώς Check = FALSE
Τέλος Αν
Αποτελέσματα // Check, Num//
Τέλος Pop

Είναι προφανές ότι και οι δύο πράξεις Push και Pop εκτελούνται σε σταθερό Ο(1) χρόνο.

Οι αλγόριθμοι των πράξεων Top() και Εmpty() είναι πιο απλοί. Η Τop() είναι ειδική
περίπτωση της Pop() όπου δεν ενημερώνουμε το δείκτη Head ενώ η πράξη Empty() μπορεί να
υλοποιηθεί με τη χρήση μιας μεταβλητής Check η οποία τίθεται ΤRUE αν Ηead = 0 και FALSE
διαφορετικά. Και αυτές οι πράξεις εκτελούνται σε χρόνο Ο(1).

2.2.2 Εφαρμογές στοίβας


Yλοποίηση αναδρομής
Η αναδρομή είναι μια ισχυρή τεχνική επίλυσης υπολογιστικών προβλημάτων και, όπως
θα δούμε σε λίγο, έχει στενή σχέση με τη δομή της στοίβας.
Ένα πρόγραμμα λέμε ότι είναι αναδρομικό όταν καλεί έμμεσα ή έμμεσα τον εαυτό του.
Επειδή πολλά προβλήματα είναι από τη φύση τους αναδρομικά, η αναδρομή βοηθάει
σημαντικά στον εύκολο σχεδιασμό και υλοποίηση της λύσης τους στον υπολογιστή (αν
φυσικά η γλώσσα προγραμματισμού υποστηρίζει την αναδρομή).
Έστω ότι θέλουμε να υπολογίσουμε το n! για n>0. Παρότι το πρόβλημα λύνεται εύκολα και
φυσικά με ένα επαναληπτικό πρόγραμμα, για τις ανάγκες της παρουσίασης θα
χρησιμοποιήσουμε την παρακάτω συνάρτηση fact():

int fact(int n)
{
if (n == 0) /* συνθήκη τερματισμού της αναδρομής */
return(1);
else
return(n*fact(n-1)); /* αναδρομική κλήση */

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 38


Αλγόριθμοι και Δομές Δεδομένων

Μια σύντομη ανάλυση του προγράμματoς δείχνει ότι εκτελούνται συνολικά n κλήσεις
(δημιουργούνται n αντίγραφα) της fact(). H εξυπηρέτηση (επιστροφή της τιμής) των κλήσεων
ακολουθεί τη σειρά LIFO όπως φαίνεται στον πίνακα που ακολουθεί για n = 4:
Kλήση (αντίγραφο) Τιμή που επιστρέφεται
fact(0) 1
fact(1) 1*fact(0) = 1*1
fact(2) 2*fact(1) = 2*1*1
fact(3) 3*fact(2) = 3*2*1*1
fact(4) 4*fact(3) = 4*3*2*1*1

H σωστή υλοποίηση ενός αναδρομικού προγράμματος απαιτεί μία συνθήκη (ή συνθήκες)


ελέγχου της αναδρομής: σε κάθε νέα αναδρομική κλήση η συνθήκη αποφασίζει αν η
αναδρομή θα συνεχιστεί με την επόμενη κλήση ή θα τερματιστεί. Συχνά αναφερόμαστε στον
όρο βάθος αναδρομής που δείχνει πόσες φορές έγινε κλήση του προγράμματος μείον τις
φορές που έχει επιστρέψει ο έλεγχος στο σημείο κλήσης.
Η αναδρομή, παρότι αποτελεί ένα χρήσιμο και ισχυρό εργαλείο για τον
προγραμματιστή, έχει ένα σοβαρό μειονέκτημα: απαιτεί κάποιον έξτρα χώρο πέρα απ’ αυτόν
που χρειάζεται το πρόγραμμα επειδή κάθε νέα αναδρομική κλήση απαιτεί να φυλάγονται
κάποιες πληροφορίες που αφορούν την προηγούμενη κλήση κάπου στη μνήμη για να
μπορέσει να τις επεξεργαστεί ο υπολογιστής μόλις ο έλεγχος επιστρέψει στην εκτέλεση αυτής
της κλήσης. Επειδή η εκτέλεση των αναδρομικών κλήσεων ακολουθεί τη σειρά LIFO, η
καταλληλότερη δομή για τη φύλαξη των πληροφοριών αυτών είναι η στοίβα. Συγκεκριμένα, μια
στοίβα του συστήματος χρησιμοποιείται για ν’ αποθηκεύσει μια σειρά από ενεργές εγγραφές
(activation records) οι οποίες κρατούν πληροφορίες που σχετίζονται με κάθε αναδρομική
κλήση. Οι πληροφορίες αυτές περιγράφουν το περιβάλλον (environment) μιας κλήσης και
στην πιο απλή τους μορφή περιλαμβάνουν τις τιμές των τοπικών μεταβλητών της κλήσης, τις
τιμές των πραγματικών της παραμέτρων αν πρόκειται για αναδρομική συνάρτηση και τη
διεύθυνση επιστροφής (η διεύθυνση της εντολής με την οποία θα συνεχιστεί η εκτέλεση της
κλήσης αφού τερματιστεί η εκτέλεση της προηγούμενης). Με κάθε αναδρομική κλήση μια νέα
εγγραφή με το παλιό περιβάλλον εισάγεται στη στοίβα και η εκτέλεση συνεχίζεται στο νέο
περιβάλλον. Όταν η κλήση τερματιστεί, η ενεργή εγγραφή που βρίσκεται στην κορυφή της
στοίβας ανακαλείται και η εκτέλεση συνεχίζεται με την εντολή επιστροφής στο περιβάλλον
που ανακλήθηκε. Η λειτουργία αυτή μπορεί να περιγραφεί συνοπτικά ως εξής:
Αναδρομική κλήση Τερματισμός της κλήσης
push(παλιό περιβάλλον) pop(περιβάλλον κορυφής)
η εκτέλεση συνεχίζεται η εκτέλεση συνεχίζεται με
με το νέο περιβάλλον την εντολή επιστροφής

Θα πρέπει βεβαίως να επισημανθεί ότι τα περισσότερα αναδρομικά προγράμματα


μπορούν να υλοποιηθούν και επαναληπτικά, συνήθως όμως μια επαναληπτική υλοποίηση
είναι πιο πολύπλοκη και οδηγεί σε λιγότερο κομψά προγράμματα. Τι συμβαίνει όμως με τις
γλώσσες που δεν υποστηρίζουν αναδρομή; Τότε πρέπει ο χρήστης να προσομοιώσει την

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 39


Αλγόριθμοι και Δομές Δεδομένων

αναδρομή εξωτερικά με τη χρήση μιας στοίβας (εφόσον φυσικά είναι επιθυμητή μια
αναδρομική υλοποίηση). Πολλές φορές η προσέγγιση αυτή ακολουθείται και σε γλώσσες που
είναι αναδρομικές για λόγους απόδοσης επειδή η υλοποίηση των αναδρομικών κλήσεων από
το σύστημα είναι γενικά μια αρκετά χρονοβόρα διαδικασία λόγω των πολλών αλλαγών
περιβάλλοντος. Όταν δε οι κλήσεις είναι πολλές, η στοίβα του συστήματος μπορεί να
υπερχειλίσει.

Πολωνικός συμβολισμός
Σε πολλές περιπτώσεις ιδιαίτερα στην επιστήμη των υπολογιστών απαιτείται η χρήση
ενός συμβολισμού για τις αριθμητικές και λογικές παραστάσεις στον οποίο δεν
χρησιμοποιούνται παρενθέσεις. Για το λόγο αυτό ο πολωνός μαθηματικός, το 1951, Jan
Lukasiewicz παρουσίασε έναν συμβολισμό ο οποίος προς τιμή του ονομάστηκε πολωνικός.
Οι παραστάσεις τις οποίες χρησιμοποιεί ο άνθρωπος αποτελούνται από τους τελεστέους,
τους τελεστές καθώς και από παρενθέσεις. Η συνηθισμένη τους μορφή όπως την γνωρίζουμε
είναι οι τελεστές να παρεμβάλλονται ανάμεσα στους τελεστέους. Η μορφή αυτή των
παραστάσεων ονομάζεται ένθετη (infix). Σύμφωνα με τον πολωνικό συμβολισμό κάθε ένθετη
μορφή μπορεί να μετατραπεί στην προθεματική (prefix) στην οποία οι τελεστές προηγούνται
από τους τελεστέους αλλά και στην μεταθετική (postfix) στην οποία οι τελεστές ακολουθούν
τους τελεστέους.
Έτσι για παράδειγμα έχουμε:
Ένθετη Προθεματική Μεταθετική
1 a+b +ab ab+
2 a+b*c +a*bc abc*+
3 a*b*c **abc ab*c*
4 a*b+c*d +*ab*cd ab*cd*+

Ένα πρόβλημα που παρουσιάστηκε στον πολωνικό συμβολισμό αφορά στην πράξη της
αφαίρεσης. Το σύμβολο – μπορεί να θεωρηθεί σε μία παράσταση είτε ως πρόσημο είτε ως το
σύμβολο της πράξης αφαίρεσης. Στην περίπτωση του πολωνικού συμβολισμού δεν έχουμε
πράξη αφαίρεσης. Αυτή μετατρέπεται σε πράξη πρόσθεσης του αντίθετου του αριθμού.
Συγκεκριμένα, όταν ζητείται η εκτέλεση της πράξης a-b τότε αυτή μετατρέπεται στην πράξη a+(-
b). Στην συνέχεια στον πολωνικό συμβολισμό αφαιρούνται και οι παρενθέσεις όπως θα δούμε
παρακάτω.
Επίσης πρέπει να τονιστεί ότι βασικό στοιχείο αποτελεί ο ορισμός του πλήθους των
τελεστέων που αντιστοιχούν σε ένα τελεστή.
Παράλληλα είναι σημαντικό να γνωρίζουμε ότι τόσο στην ένθετη μορφή των
παραστάσεων όσο και στη μεταθετική και προθεματική ισχύει μία ιεραρχία ή προτεραιότητα
στις αριθμητικές πράξεις. Αυτή η ιεραρχία στις βασικές πράξεις είναι:
1. Ύψωση σε δύναμη.
2. Πολλαπλασιασμός και διαίρεση.
3. Πρόσθεση και αφαίρεση.
Οι πράξεις που ανήκουν στο ίδιο επίπεδο ιεραρχίας (έχουν την ίδια προτεραιότητα)
εκτελούνται από αριστερά προς τα δεξιά. Αυτή ακριβώς η σειρά προτεραιότητας τηρείται και

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 40


Αλγόριθμοι και Δομές Δεδομένων

κατά τον υπολογισμό των τιμών των αριθμητικών παραστάσεων σε μια γλώσσα
προγραμματισμού.
Οι εφαρμογές που έχει ο πολωνικός συμβολισμός είναι αρκετές. Στις περισσότερες
γλώσσες προγραμματισμού μπορεί ο προγραμματιστής να εισάγει εντολές για την εκτέλεση
παραστάσεων στην ένθετη μορφή, όμως ο μεταγλωττιστής της γλώσσας αρχικά μετατρέπει
την παράσταση στην προθεματική ή την μεταθετική μορφή και στη συνέχεια εκτελεί τις
πράξεις. Επίσης, ανάλογη διαδικασία εκτελείται και σε διάφορους τύπους
προγραμματιζόμενων αριθμομηχανών κ.α.
Ο πιο συνηθισμένος συμβολισμός παραστάσεων στις γλώσσες προγραμματισμού είναι
της μεταθετικής μορφής. Παρακάτω θα εξεταστεί ο τρόπος με τον οποίο μετατρέπεται η ένθετη
μορφή μιας παράστασης είτε με παρενθέσεις είτε όχι στην αντίστοιχη μεταθετική. Επίσης θα
εξεταστεί ο τρόπος εκτέλεσης πράξεων με δοσμένη την παράσταση στην μεταθετική μορφή. Σε
όλες τις παραπάνω περιπτώσεις χρησιμοποιούνται στοίβες.

Μετατροπή ένθετης παράστασης χωρίς παρενθέσεις σε μεταθετική


Για την μετατροπή μιας παραστάσεις από την ένθετη μορφή στην μεταθετική ακολουθούμε
τα παρακάτω βήματα.
1. Αρχικά κατατάσσουμε τα σύμβολα της ένθετης παράστασης το ένα μετά το άλλο ώστε
να μπορούν να αριθμηθούν.
2. Στη συνέχεια εξετάζονται ένα – ένα διαδοχικά όλα τα σύμβολα της παράστασης
ξεκινώντας από αυτό αριστερά της.
3. Όλοι οι τελεστέοι τοποθετούνται άμεσα ο ένας στα δεξιά του άλλου σε μία παράσταση
σε μεταθετική μορφή.
4. Όλοι οι τελεστές ωθούνται σε μία στοίβα ενδιάμεσα και στη συνέχεια απωθούνται και
ενσωματώνονται στα δεξιά της παράστασης μεταθετικής μορφής που σχηματίζεται
σταδιακά. Ειδικότερα, για τον παραπάνω λόγο ισχύουν οι παρακάτω κανόνες:
Α. Αν ο τελεστής που εξετάζουμε έχει μεγαλύτερη ιεραρχία από αυτόν που βρίσκεται
στην κεφαλή της στοίβας τότε αυτός εισάγεται στην στοίβα (push).
B. Αν ο τελεστής που εξετάζουμε έχει ίση ή μικρότερη ιεραρχία από αυτόν που
βρίσκεται στην κεφαλή της στοίβας τότε αρχικά απωθούνται (pop) όλοι οι τελεστές
της στοίβας οι οποίοι έχουν μεγαλύτερη ή ίση ιεραρχία από αυτόν. Οι τελεστές
αυτοί εισάγονται διαδοχικά στα δεξιά της παράστασης της μεταθετικής μορφής.
Στη συνέχεια ο τελεστής που εξετάζουμε εισάγεται στην στοίβα (push).
Για την καλύτερη κατανόηση των παραπάνω κανόνων δίνεται το παρακάτω παράδειγμα
μετατροπής της αριθμητικής παράστασης ένθετης μορφής a+b*ce+d/f στην αντίστοιχη
μεταθετική.
Αρχικά, διατάσσουμε τα σύμβολα της παράστασης και παίρνουμε a+b*c^e+d/f.
Εξετάζουμε τα σύμβολα της παράστασης διαδοχικά ξεκινώντας από τα αριστερά και έχουμε:
Σύμβολα στην ένθετη μορφή Στοίβα Μεταθετική μορφή
a - a
+ + a
b + ab
* *+ ab

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 41


Αλγόριθμοι και Δομές Δεδομένων

c *+ abc
^ ^*+ abc
e ^*+ abce
+ + abce^*+
d + abce^*+d
/ /+ abce^*+d
f /+ abce^*+df
- abce^*+df/+

Μετατροπή ένθετης παράστασης με παρενθέσεις σε μεταθετική


Στην περίπτωση που η παράστασή μας στην ένθετη μορφή έχει παρενθέσεις τότε:
▪ Ισχύουν επιπλέον οι παρακάτω δύο κανόνες:
1. Όταν κατά την διαδοχική εξέταση των συμβόλων της παράστασης συναντάται αριστερή
παρένθεση τότε αυτήν εισάγεται ως έχει στην στοίβα.
2. Όταν κατά την διαδοχική εξέταση των συμβόλων της παράστασης συναντάται δεξιά
παρένθεση τότε αυτή προκαλεί την απώθηση από τη στοίβα όλων των τελεστών μέχρι
να συναντήσουμε την αριστερή παρένθεση. Οι παρενθέσεις εξαλείφονται.
▪ Στους προηγούμενους κανόνες έχουμε την εξής διαφοροποίηση:
Όταν ο τελεστής που εξετάζουμε έχει μικρότερη ή ίση προτεραιότητα από αυτόν στην
κεφαλή της στοίβας τότε βγάζουμε από τη στοίβα (pop) όλους τους τελεστές με
προτεραιότητα μεγαλύτερη ή ίση από αυτόν μέχρι να συναντήσουμε αριστερή παρένθεση.

Υποθέστε ότι έχουμε την παράσταση (a+b)*(c-d)^e*f στην ένθετη μορφή. Αρχικά
μετατρέπουμε την πράξη αφαίρεσης σε πρόσθεση και παίρνουμε (a+b)*(c+-d)^e*f. Εξετάζουμε
τα σύμβολα της παράστασης διαδοχικά ξεκινώντας από τα αριστερά και έχουμε:
Σύμβολα στην ένθετη μορφή Στοίβα Μεταθετική μορφή
( (
a ( a
+ +( a
b +( ab
) - ab+
* * ab+
( (* ab+
c (* ab+c
+ +(* ab+c
-d +(* ab+c-d
) * ab+c-d+
^ ^* ab+c-d+
e ^* ab+c-d+e
* * ab+c-d+e^*

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 42


Αλγόριθμοι και Δομές Δεδομένων

f * ab+c-d+e^*f
- - ab+c-d+e^*f*

Υπολογισμός της τιμής μιας παράστασης μεταθετικής μορφής


Αφού η παράσταση ένθετης μορφής μετατραπεί στην αντίστοιχη μεταθετική στη συνέχεια
ο υπολογισμός της τιμής της είναι εύκολος. Εξετάζουμε τα στοιχεία της παράστασης ένα ένα
διαδοχικά από τα αριστερά. Όταν συναντάται τελεστέος τότε η αντίστοιχη τιμή του εισάγεται
στην στοίβα. Όταν συναντάται τελεστής τότε γίνεται η πράξη που δηλώνει ανάμεσα στην
κεφαλή της στοίβας και το αμέσως επόμενο της στοίβας και το αποτέλεσμα αποτελεί την νέα
κεφαλή της. Δηλαδή εκτελούνται δύο συνεχόμενες pop στην στοίβα, εκτελείται η αντίστοιχη
πράξη και το αποτέλεσμά της γίνεται push στη στοίβα.
Έστω ότι έχουμε την παράσταση (a+b)*c που η μεταθετική της μορφή είναι ab+c*. Αν a=1,
b=2 και c=3 τότε: (1+2)*3=9.
Αν γίνει ο υπολογισμός της τιμής μέσω της μεταθετικής μορφής έχουμε:
Παράσταση στην μεταθετική Στοίβα
μορφή
a 1
b 2 1
+ 3
c 3 3
* 9

2.3 Ουρές (Queues)


Μία άλλη πολύ συνηθισμένη δομή δεδομένων είναι η ουρά (queue) στην οποία τα
δεδομένα της αποθηκεύονται γραμμικά (διαδοχικά) το ένα μετά το άλλο. Όταν ο χρήστης
εισάγει ένα στοιχείο αυτό αποθηκεύεται στο τέλος της ουράς. Όταν όμως θέλει να εξάγει ένα
στοιχείο της στοίβας τότε έχει τη δυνατότητα να προσπελάσει μόνο το πρώτο στοιχείο της
αρχής της ουράς. Το στοιχείο αυτό ουσιαστικά είναι το πιο «παλιό» στοιχείο που έχει
εισαχθεί στην ουρά δηλαδή το πρώτο στοιχείο που εισήχθη στην ουρά θα προσπελαστεί
πρώτο. Η λειτουργία της αυτή επομένως την κατατάσσει στις δομές FIFO (First In First Out).
Συχνά στην καθημερινή μας ζωή συναντάμε περιπτώσεις ή προβλήματα που απαιτούν
την εφαρμογή δομής ουράς για τον ορισμό και την επίλυση τους. Παντού συναντούμε ουρές
αναμονής ανθρώπων για την εξυπηρέτηση των εργασιών τους όπως στις τράπεζες,
καταστήματα κ.α.
Πρέπει να τονιστεί παράλληλα ότι η ουρά αποτελεί μία βασική δομή δεδομένων η οποία
χρησιμοποιείται συχνά στην επιστήμη των υπολογιστών. Ο προγραμματισμός στις εμπορικές
επιχειρήσεις απαιτεί την υλοποίηση ουρών για να προσομοιώσουν είτε την εξυπηρέτηση
πελατών είτε την διαχείριση της αποθήκης τους κ.τ.λ.
Ποιες λειτουργίες όμως εφαρμόζονται σε μία ουρά; Ουσιαστικά η ουρά είναι μία
παραπλήσια δομή με την στοίβα που εξετάσαμε στην προηγούμενη παράγραφο. Η μόνη τους
διαφορά είναι ότι στη στοίβα είχαμε μόνο ένα σημείο τόσο για την εισαγωγή όσο και για την
απώθηση στοιχείων. Αυτό το σημείο ήταν η κεφαλή της στοίβας και επομένως απαιτούνταν
μόνο ένας δείκτης ο οποίος να δηλώνει τη θέση της κεφαλής στη στοίβα (πίνακα ή λίστα).

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 43


Αλγόριθμοι και Δομές Δεδομένων

Στην ουρά όμως δεν έχουμε μόνο ένα σημείο εισόδου και εξόδου. Στοιχεία εισέρχονται
από το ένα άκρο της ουράς, το πίσω έστω το τέλος της ουράς ενώ εξέρχονται (απωθούνται)
από το εμπρός μέρος της ουράς έστω την αρχή της. Μπορούμε επομένως να φανταστούμε την
ουρά σαν ένα σωλήνα στον οποίο εισέρχονται στοιχεία από το ένα άκρο του (πίσω-τέλος) και
εξέρχονται από το άλλο άκρο (εμπρός-αρχή).
Έστω επομένως έχουμε μία ουρά στην οποία έχουμε αποθηκευμένα τα στοιχεία X, Y, Z
και V όπως φαίνεται στο παρακάτω σχήμα.

Εμπρός - Αρχή Ουράς Πίσω - Τέλος Ουράς


Εξαγωγή στοιχείων Εισαγωγή στοιχείων

X Y Z V

Σχήμα 7: Η δομή της ουράς

Στην ουρά αυτή υπάρχουν δύο βασικές επιλογές λειτουργιών. Ένας χρήστης μπορεί να
ενθέσει ένα στοιχείο στην ουρά, έστω P με την εκτέλεση της πράξης Enqueue(P). Τότε η ουρά
αποκτά την ακόλουθη μορφή:

Εμπρός - Αρχή Ουράς Πίσω - Τέλος Ουράς


Εξαγωγή στοιχείων Εισαγωγή στοιχείων

X Y Z V P

Σχήμα 8: Η ουρά μετά την ένθεση του στοιχείου P

Επίσης, ένας χρήστης μπορεί να απωθήσει ένα στοιχείο από την ουρά με την πράξη
Dequeue(). Συγκεκριμένα, η πράξη dequeue προσπελαύνει το πρώτο στοιχείο της ουράς και
στη συνέχεια το σβήνει από αυτή. Τότε η ουρά αποκτά την ακόλουθη μορφή:

Σχήμα 9: Η ουρά μετά την απώθηση του πρώτου της στοιχείου (Χ)

Bεβαίως υπάρχουν και άλλες πράξεις που εφαρμόζονται σε μία ουρά, όχι βέβαια τόσο
σημαντικές όσο οι προηγούμενες δύο που αναφέρθηκαν και για το λόγο αυτό δεν θα
ασχοληθούμε στη συνέχεια μ’ αυτές.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 44


Αλγόριθμοι και Δομές Δεδομένων

2.3.1 Μέθοδοι υλοποίησης ουράς


Και η ουρά μπορεί να υλοποιηθεί ως ένας πίνακας ορισμένου ή μη μεγέθους ή μιας
απλής λίστας και επομένως μπορεί να είναι στατική ή δυναμική δομή αντίστοιχα. Υπάρχουν
αρκετές παραλλαγές ουρών, ανάλογα τον τρόπο που διαχειρίζονται τα δεδομένα ανεξάρτητα
από το αν υλοποιούνται με πίνακες ή με λίστες. Έτσι υπάρχει η απλή φυσική υλοποίηση, τη
λειτουργία της οποίας περιγράψαμε παραπάνω. Π΄ροκειται για ένα σωλήνα (pipeline)
στοιχείων τα οποία εισέρχονται από το ένα άκρο και εξέρχονται από το άλλο. Επίσης, υπάρχει
η παραλλαγή δακτυλίου (ring) στην οποία η ουρά σχηματίζει ένα δακτύλιο συγκεκριμένου
μεγέθους. Στην περίπτωση αυτή δεν υπάρχει εμπρός και πίσω μέρος ουράς αλλά αυτά
ορίζονται αποκλειστικά από 2 δείκτες αρχής και τέλους όπως θα δούμε παρακάτω.
Στη συνέχεια παρουσιάζουμε τους αλγόριθμους υλοποίησης και των παραλλαγών με
χρήση πινάκων συγκεκριμένου μεγέθους.

Υλοποίηση ουράς pipeline με χρήση πινάκων


Η πιο συνηθισμένη μορφή μιας ουράς είναι ένας πίνακας περιορισμένου μεγέθους. Η
ουρά επομένως μπορεί να υλοποιηθεί με απλό τρόπο χρησιμοποιώντας έναν μονοδιάστατο
πίνακα μεγέθους έστω Queue[N] χρησιμοποιώντας πάντοτε δύο δείκτες. Ένα δείκτη που να
δείχνει το πρώτο στοιχείο της ουράς που είναι και το πρώτο από όλα της τα στοιχεία που
εισήχθη σ’ αυτήν και ονομάζεται κεφαλή της ουράς (μεταβλητή Head). Κάθε φορά επομένως
απωθείται το στοιχείο της αρχής της ουράς που δείχνει η μεταβλητή head. Επίσης
χρησιμοποιείται ένας δείκτης που δείχνει στο τελευταίο στοιχείο της ουράς που είναι και το
τελευταίο από όλα της τα στοιχεία που εισήχθη σ’ αυτήν (μεταβλητή Back). Κάθε φορά
επομένως εισέρχεται ένα στοιχείο στο τέλος της ουράς μετά το τελευταίο της στοιχείο που
δείχνει η μεταβλητή Back. Κατά την υλοποίηση των πράξεων της ουράς χρησιμοποιείτε
πάντοτε μία μεταβλητή τύπου boolean η οποία ενημερώνει πάντοτε μετά την αίτηση εκτέλεσης
πράξης αν αυτήν εκτελέστηκε επιτυχώς ή όχι (έστω μεταβλητή Check).
Έχουμε επομένως τον πίνακα Queue[N] με τους δείκτες Head και Back ως εξής:
N

X Back

V Head

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 45


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 10: Υλοποίηση ουράς pipeline με χρήση πίνακα

Αρχικά η ουρά είναι άδεια και ισχύει Head=0 και Back=0 (η ίδια συνθήκη ισχύει και κάθε
φορά που η ουρά αδειάζει).
Όταν εισάγεται για πρώτη φορά σε μια άδεια ουρά ένα στοιχείο τότε αυξάνεται κατά ένα η
Back (Back=1) και το στοιχείο καταλαμβάνει τη θέση Queue[Back]=Queue[1]. Παράλληλα η
head (που είναι μηδέν εφόσον ήταν άδεια η ουρά) αυξάνεται κατά ένα και δείχνει στη θέση
Queue[1]. Το επόμενο στοιχείο πηγαίνει στη θέση Queue[2] και η Back=2 (η head τώρα δεν
αλλάζει).
Αν θέλουμε να εξάγουμε ένα στοιχείο τότε παίρνουμε το Queue[Head] = Queue[1] και
αυξάνουμε κατά ένα την Head και γίνεται 2 δείχνοντας στο επόμενο στοιχείο της κεφαλής της
ουράς.
Από το παραπάνω σχήμα καταλαβαίνουμε ότι εισήχθησαν στην ουρά 8 στοιχεία και
απωθήθηκαν 4. Το 5o στοιχείο που εισήχθη είναι το V το οποίο είναι και στην κεφαλή της
ουράς για να απωθηθεί.

Πράξη Enqueue()
Αν θεωρήσουμε ότι ουρά θα περιέχει ακεραίους αριθμούς (ο πίνακας Queue[N] δηλώνεται
ως ένας πίνακας ακεραίων) τότε η πράξη ένθεσης καλείται όταν θέλουμε να εισάγουμε ένα
στοιχείο στην ουρά έστω Num (η μεταβλητή Νum είναι τύπου ακεραίου). Η Enqueue είναι μία
ανεξάρτητη διαδικασία η οποία καλείται από το κύριο πρόγραμμα όταν θέλουμε να εισάγουμε
ένα στοιχείο στην ουρά. Συντάσσεται επομένως ως μία συνάρτηση στην περίπτωση μας
τύπου ακεραίου στην οποία μεταφέρεται μαζί ως αρχικά δεδομένα και η τιμή του στοιχείου
num. Όταν καλείται η Enqueue επομένως έχει την μορφή Enqueue(Νum).
Όταν γίνει αίτηση για εισαγωγή πρέπει να εξεταστεί αρχικά το γεγονός αν υπάρχει
διαθέσιμος χώρος στην ουρά για να εισαχθεί το στοιχείο num. Τονίσαμε ότι η μεταβλητή Back
δείχνει στο τελευταίο στοιχείο της ουράς (που εισήχθη και τελευταίο) και έχει στο παράδειγμα
μας την τιμή Ν. Αν επομένως στο παράδειγμα μας, η Back δείχνει στο στοιχείο N τότε δεν
υπάρχει χώρος στην ουρά. Άρα δεν εκτελείται η πράξη και αυτό δηλώνεται ορίζοντας την
μεταβλητή Check ως FALSE. Στην αντίθετη περίπτωση υπάρχει χώρος για το στοιχείο num
και τότε η μεταβλητή Back αυξάνεται κατά ένα ώστε να δείχνει στην επόμενη θέση της ουράς
που είναι κενή. Στη συνέχεια το στοιχείο Num εισάγεται στη θέση που δείχνει η Back. Η
ένθεση εκτελείται επιτυχώς και τίθεται η μεταβλητή Check ως TRUE.
Παράλληλα εξετάζουμε την περίπτωση που ένα στοιχείο εισάγεται για πρώτη φορά σε μια
άδεια ουρά. Τότε, όπως αναφέραμε ήδη, αυξάνεται κατά ένα η Back (Back=1) και το στοιχείο
καταλαμβάνει τη θέση Queue[Back]=Queue[1]. Παράλληλα η Head (που έχει τιμή 0 εφόσον η
ουρά ήταν άδεια) αυξάνεται κατά ένα και δείχνει στο στοιχείο Queue[1].
Παρακάτω δίνεται ο αλγόριθμος υλοποίησης της πράξης Enqueue σε ψευδοκώδικα:

Αλγόριθμος Enqueue Pipeline(Num)


Δεδομένα // Queue[N], Head, Back, Νum: ακέραιοι; Check: δυαδική //
Αρχή

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 46


Αλγόριθμοι και Δομές Δεδομένων

Aν Head = 0 Τότε Head = 1 Τέλος Αν


Aν Back < N Τότε
Back = Back +1
Queue[Back] = Νum
Check = ΤRUE
Αλλιώς Check = FALSE
Τέλος Αν
Αποτελέσματα // Check //
Τέλος Enqueue Pipeline

Πράξη Dequeue()
Η πράξη απώθησης καλείται όταν θέλουμε να διαβάσουμε και να σβήσουμε παράλληλα
το στοιχείο στην κεφαλή της ουράς, έστω Νum. Η Dequeue είναι μία ανεξάρτητη διαδικασία
και υλοποιείται ως μία συνάρτηση που στην περίπτωσή μας επιστρέφει μία ακέραια τιμή. Η
κλήση της επομένως έχει την μορφή Dequeue() και επιστρέφει το στοιχείο-κεφαλή της ουράς
(αν εκτελεστεί επιτυχώς).
Όταν γίνει αίτηση για απώθηση (κλήση Dequeue()), πρέπει να εξεταστεί αρχικά αν
υπάρχουν στοιχεία στην ουρά.. Είπαμε ότι η μεταβλητή Head δείχνει στο πρώτο στοιχείο της
ουράς (κεφαλή). Αν επομένως η Head έχει την τιμή 0 σημαίνει ότι η ουρά είναι κενή και δεν
εκτελείται η πράξη. Επομένως επιστρέφεται η μεταβλητή Check ως FALSE. Στην αντίθετη
περίπτωση υπάρχουν στοιχεία στην ουρά και η Dequeue επιστρέφει το στοιχείο της κεφαλής
που είναι το Queue[Head]. Η μεταβλητή Ηead αυξάνεται κατά ένα ώστε να δείχνει στο
επόμενο στοιχείο της ουράς που θα αποτελεί στη συνέχεια και την κεφαλή της. Η απώθηση
εκτελείται επιτυχώς και η μεταβλητή Check τίθεται TRUE.
Τέλος, πρέπει να εξεταστεί η περίπτωση αν μετά από κάθε απώθηση άδειασε η ουρά.
Αυτό συμβαίνει αν η Head, αυξανόμενη κατά 1, πάρει τιμή μεγαλύτερη της Back. Τότε έχει
απωθηθεί το τελευταίο στοιχείο της ουράς. Στην περίπτωση αυτή η ουρά είναι άδεια και
επομένως μπορούμε να την αρχικοποιήσουμε θέτοντας στις μεταβλητές Head και Back την
τιμή 0.
Παρακάτω δίνεται ο αλγόριθμος υλοποίησης της πράξης Dequeue σε μορφή
ψευδοκώδικα:

Αλγόριθμος Dequeue Pipeline()


Δεδομένα // Queue[N], Head, Num: ακέραιοι; Check: δυαδική //
Αρχή
Aν Head > 0 Τότε
Num = Queue[Head]
Head = Head+1
Check = ΤRUE
Αν Head > Back Τότε

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 47


Αλγόριθμοι και Δομές Δεδομένων

Head = 0
Back = 0
Τέλος Αν
Αλλιώς Check=FALSE
Τέλος Αν
Αποτελέσματα // Check, Νum //
Τέλος Dequeue Pipeline

Από την υλοποίηση των πράξεων Enqueue και Dequeue καταλαβαίνουμε ότι μπορεί η
μεταβλητή Back να δείχνει στην τιμή N χωρίς αυτό να σημαίνει ότι η ουρά είναι γεμάτη. Είναι
γεμάτη μόνο αν Back = N και Head = 1. Αν επομένως κάποια στιγμή η Back πάρει την τιμή N
τότε ελέγχουμε την τιμή της Head. Αν είναι 1 τότε η ουρά είναι γεμάτη, διαφορετικά υπάρχουν
κενές θέσεις στην ουρά που προέκυψαν στην αρχή της από τις συνεχόμενες απωθήσεις
στοιχείων. Στην περίπτωση αυτή πρέπει να εκτελεστεί μία ρουτίνα η οποία να μετακινεί όλα τα
στοιχεία της ουράς της προς την αρχή της ώστε η Head να γίνει 1 και η Back να πάρει μία τιμή
ανάλογα με το πλήθος των στοιχείων της ουράς και μικρότερη από την τιμή N. Aυτή τη ρουτίνα
μετακίνησης στοιχείων ονομάζεται Fragm() και δίνεται στη συνέχεια σε μορφή ψευδοκώδικα:

Αλγόριθμος Fragm()
Δεδομένα // Queue[N], Head, Back, i: ακέραιοι //
Αρχή
Για i = Head Μέχρι i = Back με Βήμα 1 Εκτέλεσε
Queue[i-Head+1] = Queue[i]
Tέλος Eπανάληψης
Back = Back-Head+1
Head = 1
Αποτελέσματα // Head, Back //
Τέλος Fragm

Στη συνέχεια δίνεται ο ψευδοκώδικας για την πράξη Enqueue η οποία κάνει χρήση της
ρουτίνας Fragm() . Τώρα στη χειρότερη περίπτωση η Enqueue έχει πολυπλοκότητα O(N).

Αλγόριθμος Enqueue Pipeline with Fragm


Δεδομένα // Queue[N], head, back, num: ακέραιοι; check: δυαδική //
Αρχή
Αν Head = 0 Τότε Head = 1 Tέλος Αν
Αν Back < N Τότε
Back = Back+1

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 48


Αλγόριθμοι και Δομές Δεδομένων

Queue[Back] = num
Check = TRUE
Αλλιώς
Αν Head = 1 Τότε Check = FALSE
Αλλιώς κάλεσε τη Fragm() και κάνε μετά την ένθεση
Tέλος Αν
Τέλος Αν
Αποτελέσματα // Check //
Τέλος Εnqueue Pipeline with Fragm

Υλοποίηση ουράς δακτυλίου


Όπως περιγράφθηκε παραπάνω, μία παραλλαγή ουράς είναι αυτή του δακτυλίου (ring)
στην οποία η ουρά σχηματίζει ένα δακτύλιο συγκεκριμένου μεγέθους. Ουσιαστικά στην
περίπτωση αυτή δεν υπάρχει εμπρός και πίσω μέρος ουράς αλλά αυτά ορίζονται
αποκλειστικά από 2 δείκτες αρχής και τέλους. Το γεγονός αυτό βελτιώνει τον αλγόριθμο
υλοποίησης της ουράς αφού θέτει μη αναγκαία την χρήση της ρουτίνας μετακίνησης των
στοιχείων Fragm ενώ όλες οι πράξεις κοστίζουν Ο(1) χρόνο.
Παράδειγμα μιας ουράς δακτυλίου φαίνεται στο επόμενο σχήμα:

N W

Y Head

Z Back
1 V

Σχήμα 11: Υλοποίηση ουράς δακτυλίου με χρήση πίνακα

Για να υλοποιήσουμε τις πράξεις Enqueue και Dequeue σε μια ουρά δακτυλίου θα
χρησιμοποιήσουμε μία μεταβλητή counter η οποία θα κρατά το πλήθος των στοιχείων της
ουράς. Όταν counter = 0, τότε η ουρά είναι άδεια ενώ όταν Counter = N η ουρά είναι γεμάτη.
Οι αλγόριθμοι για τις πράξεις Enqueue και Dequeue ελέγχουν κάθε φορά την τιμή της

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 49


Αλγόριθμοι και Δομές Δεδομένων

μεταβλητής Counter και εκτελούν την πράξη αναπροσαρμόζοντας τις τιμές Back και Ηead
κατάλληλα εφόσον χρειάζεται. Στη συνέχεια δίνονται οι αλγόριθμοι σε μορφή ψευδοκώδικα:

Αλγόριθμος Enqueue Ring(Num)


Δεδομένα // Queue[N], Head, Back, Num, Counter: ακέραιοι; Check: δυαδική //
Αρχή
Αν Head = 0 Τότε Head = 1 τέλος Αν
Αν Counter < N Τότε
Αν Back < N Τότε Back = Back+1
Αλλιώς Back = 1
Tέλος Αν
Queue[Back] = Num
Counter = Counter+1
Check = TRUE
Αλλιώς Check = FALSE
Τέλος Αν
Αποτελέσματα // Check //
Τέλος Εnqueue Ring

Αλγόριθμος Dequeue Ring()


Δεδομένα // Queue[N], Head, Back, Num, Counter: ακέραιοι; Check: δυαδική //
Αρχή
Αν Counter > 0 Τότε
Num = Queue[head]
Αν head < Ν Τότε Head = Head+1
Αλλιώς Head = 1
Tέλος Αν
Counter = Counter-1
Check = TRUE
Αλλιώς Check = FALSE
Tέλος Αν
Αποτελέσματα // Check, Num //
Τέλος Dequeue Ring

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 50


Αλγόριθμοι και Δομές Δεδομένων

2.4 Λίστες (Lists)


Οι δομές δεδομένων που εξετάστηκαν προηγουμένως (στοίβες και ουρές) έχουν κάποια
κοινά χαρακτηριστικά:
• Ακολουθούν αυστηρούς κανόνες αναφορικά με τον τρόπο αποθήκευσης των δεδομένων.
• Υποστηρίζουν τις ίδιες λειτουργίες εκ των οποίων αυτή της απώθησης (Pop και Dequeue)
έχουν σαν αποτέλεσμα την φυσική αλλαγή της δομής. Δηλαδή η απώθηση ενός στοιχείου
από την στοίβα ή την ουρά έχει σαν αποτέλεσμα και τη διαγραφή του.
• Και στις δύο δομές η λογική σειρά με την οποία αποθηκεύονται τα στοιχεία τους είναι
ίδια με την φυσική (χρονική) τους σειρά.
Τόσο οι στοίβες όσο και οι ουρές χρησιμοποιούν μία συμπαγή περιοχή μνήμης –
τουλάχιστον θεωρητικά. Το γεγονός αυτό δημιουργεί σε αρκετές εφαρμογές προβλήματα που
έχουν να κάνουν με την διαχείριση του αποθηκευτικού χώρου (μνήμης). Έτσι βρίσκει πεδίο
εφαρμογής μία άλλη δομή δεδομένων, η λίστα (list).
Αντίθετα με την στοίβα ή την ουρά, σε μία λίστα τα στοιχεία αποθηκεύονται με θέσεις που
δεν προκαθορίζονται και στις οποίες είναι δυνατή η πρόσβαση με οποιονδήποτε τρόπο. Αυτό
είναι δυνατό γιατί κάθε στοιχείο πληροφορίας συνοδεύεται από ένα δείκτη (pointer) που
λειτουργεί ως σύνδεσμος (link) με το επόμενο στοιχείο της λίστας. Με τον τρόπο αυτό η
ανάκτηση στοιχείων από τη λίστα δεν απομακρύνει ή διαγράφει ουσιαστικά στοιχεία από τη
λίστα.

2.4.1 Εφαρμογές
Οι λίστες χρησιμοποιούνται ευρύτατα στον προγραμματισμό εφαρμογών λόγω του
αποδοτικού τρόπου διαχείρισης της μνήμης. Ενδεικτικές περιπτώσεις είναι οι κάτωθι:
1. Δημιουργία πινάκων με μη προκαθορισμένο μέγεθος μνήμης. Δηλαδή, όταν γνωρίζουμε
το μέγεθος μνήμης που θα χρησιμοποιήσουμε μπορούμε να χρησιμοποιήσουμε ένα
πίνακα συγκεκριμένου μεγέθους ενώ σε αντίθετη περίπτωση μία λίστα.
2. Αποθήκευση πληροφοριών σε βάσεις δεδομένων. Στην περίπτωση αυτή τα δεδομένα
αποθηκεύονται σε αρχεία του δίσκου (δευτερεύουσα μνήμη). Οι λίστες στην περίπτωση
αυτή μας βοηθούν στην εγγραφή και διαγραφή στοιχείων από τα αρχεία του δίσκου
γρήγορα και εύκολα, χωρίς να απαιτείται η αναδιάταξη ολόκληρου του αρχείου. Τα αρχεία
συνήθως αποτελούνται από πολλά δεδομένα ενώ στην πλειοψηφία τους οι εφαρμογές
διαχείρισης βάσεων δεδομένων είναι δυναμικές εφαρμογές με συνεχείς λειτουργίες
ανάκτησης, εισαγωγής και διαγραφής.

2.4.2 Είδη λιστών


Υπάρχουν αρκετά είδη λιστών ανάλογα τον τρόπο μεν τον οποίο υλοποιούνται αλλά και
τις δυνατότητες που δίνονται στους χρήστες για την διαχείριση των στοιχείων σ’ αυτές.
Η πιο απλή μορφή λίστας είναι η απλά συνδεδεμένη λίστα (simply linked list).
Αποτελείται από θέσεις στις οποίες αποθηκεύονται τα στοιχεία και ονομάζονται κόμβοι
(nodes). Σε έναν κόμβο αποθηκεύονται τα ίδια τα στοιχεία της πληροφορίας που έχουμε και
παράλληλα ένας δείκτης που λειτουργεί ως σύνδεσμος με το επόμενο στοιχείο της λίστας. Η
θέση του πρώτου κόμβου είναι γνωστή και με τον τρόπο αυτό μέσω των συνδέσμων

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 51


Αλγόριθμοι και Δομές Δεδομένων

μπορούμε να “διατρέξουμε” όλους τους κόμβους μέχρι τον τελευταίο. Αυτός αναγνωρίζεται από
το γεγονός ότι ο δείκτης στο επόμενο στοιχείο του έχει την τιμή ΝULL (κενός δείκτης).
Παρακάτω δίνεται ένα παράδειγμα απλά συνδεδεμένης λίστας όπου η τιμή NULL
απεικονίζεται με το 0.

Α Β C N
0

Σχήμα 12: Παράδειγμα απλά συνδεδεμένης λίστας

Επίσης, ένα άλλο είδος λίστας είναι η διπλά συνδεδεμένη λίστα ή συμμετρική
(symmetric list). Στις λίστες αυτές περιέχονται σε κάθε κόμβο σύνδεσμοι όχι μόνο για το
επόμενο στοιχείο – κόμβο αλλά και για το προηγούμενο. Παρακάτω δίνεται ένα παράδειγμα
διπλής συνδεδεμένης λίστας.

Α Β C N
0 0

Σχήμα 13: Παράδειγμα διπλά συνδεδεμένης λίστας

Ένα ακόμα είδος λίστας είναι η κυκλική (circular list) που φαίνεται στο επόμενο σχήμα.
Ουσιαστικά είναι μία απλά συνδεδεμένη λίστα με την διαφορά ότι ο δείκτης επόμενου
στοιχείου του τελευταίου της κόμβου δεν περιέχει την τιμή 0 (NULL) αλλά δείχνει στο πρώτο
στοιχείο της λίστας. Η συγκεκριμένη δομή εισάγει αρκετές διαφοροποιήσεις στον τρόπο
υλοποίησης των βασικών πράξεων. Παρακάτω δίνεται ένα παράδειγμα κυκλικής λίστας.

Α Β C N

Σχήμα 14: Κυκλική λίστα

2.4.3 Βασικές πράξεις σε λίστες


Πριν αναφερθούν οι βασικές πράξεις σε μία λίστα πρέπει να τονιστούν δύο βασικές
παράμετροι στην υλοποίησή τους.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 52


Αλγόριθμοι και Δομές Δεδομένων

• Οι λίστες είναι είτε στατικές είτε δυναμικές δομές δεδομένων. Όταν το μέγεθος της λίστας
δηλαδή το σύνολο των κόμβων είναι προκαθορισμένο τότε η λίστα είναι στατική. Όταν
όμως δεν το γνωρίζουμε το μέγεθός της και κάθε φορά που δημιουργείται ένας κόμβος
(εισαγωγή ενός στοιχείου) δεσμεύουμε τον χώρο αποθήκευσης στην μνήμη για αυτόν, τότε
η λίστα είναι δυναμική.
• Για την υλοποίηση μίας λίστας μπορούμε να χρησιμοποιήσουμε είτε έναν αριθμητικό
πίνακα στον οποίο θα αποθηκεύονται όλοι οι σύνδεσμοι των κόμβων δηλαδή οι θέσεις
των επόμενων στοιχείων είτε, αν και η γλώσσα προγραμματισμού το επιτρέπει, δείκτες. Οι
δείκτες είναι παρέχουν εύκολη δημιουργία, διαχείριση, και διαγραφή στοιχείων από μία
λίστα. Μερικές γλώσσες προγραμματισμού διευκολύνουν το χειρισμό λιστών όπως είναι η
LISP (List Processing Language) και η Prolog. Οι περισσότερες όμως παρέχουν τη
δυνατότητα χρήσης δεικτών και έτσι η επεξεργασία μία λίστας γίνεται εύκολα και
αποδοτικά.
Οι βασικές πράξεις που υποστηρίζει μια λίστα είναι:
1. Εισαγωγή (insertion) στοιχείου. Για να γίνει η εισαγωγή ενός στοιχείου πρέπει να
δημιουργηθεί ένας νέος κόμβος στην λίστα στον οποίο να αποθηκευτεί. Η θέση του
κόμβου ορίζεται με βάση τις απαιτήσεις του προγράμματος. Σε κάθε περίπτωση
ανάλογα με το είδος της λίστας (απλή, διπλή ή κυκλική) γίνεται μόνο τροποποίηση
των περιεχομένων των συνδέσμων επόμενου στοιχείου των κατάλληλων κόμβων.
2. Διαγραφή (deletion) ενός στοιχείου. Για να γίνει η διαγραφή ενός στοιχείου από τη
λίστα αρκεί να τροποποιήσουμε το περιεχόμενο του συνδέσμου του επόμενου
στοιχείου του προηγούμενου κόμβου.
3. Σάρωση (traversal) ή διαπέραση λίστας. Με την πράξη αυτή προσπελαύνονται όλα
τα στοιχεία της λίστας ξεκινώντας από το πρώτο και ακολουθώντας τους κόμβους
μέσω των συνδέσμων τους μέχρι το τελευταίο στοιχείο που ο σύνδεσμος του
επόμενου στοιχείου του έχει την τιμή NULL (0).
4. Αναζήτηση (search) στοιχείου. Με την πράξη αυτή μπορεί να γίνει η αναζήτηση ενός
στοιχείου που επιθυμούμε. Για την υλοποίηση της πράξης αυτής χρησιμοποιείται η
πράξη σάρωσης της λίστας. Η λίστα είναι μία γραμμική δομή άρα και η διαπέραση
της μέσω των συνδέσμων είναι γραμμική.
5. Συνένωση (concatenation) λιστών. Με την πράξη αυτή είναι δυνατή η συνένωση
δύο λιστών σε μία λίστα διατηρώντας αρχικές προϋποθέσεις που δίνονται. Δηλαδή η
συνένωση μπορεί να γίνει απλά τοποθετώντας τη μία λίστα μετά την άλλη είτε με
συγκεκριμένο αλγόριθμο στην περίπτωση π.χ. που οι αρχικές λίστες είναι
διατεταγμένες και επιθυμούμε η νέα λίστα να είναι και αυτή διατεταγμένη.
6. Αντιστροφή (reversal) λίστας. Με την πράξη αυτή αλλάζει η φορά διάταξης της
λίστας ορίζοντας τον τελευταίο κόμβο ως πρώτο και αυτόν τελευταίο τροποποιώντας
κατάλληλα τους συνδέσμους όλων των κόμβων της λίστας.

2.4.4 Αλγόριθμοι βασικών πράξεων


Στην παράγραφο αυτή θα μελετηθούν οι αλγόριθμοι υλοποίησης των βασικών πράξεων
μίας λίστας τονίζοντας τα βασικά βήματα επεξεργασίας τα οποία πρέπει να γίνουν στα
στοιχεία της. Στην συγκεκριμένη περίπτωση η δομή με την οποία ασχολούμαστε είναι μία
απλά συνδεδεμένη λίστα.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 53


Αλγόριθμοι και Δομές Δεδομένων

Ο σχεδιασμός της λίστας θα βασιστεί στους παρακάτω κανόνες:


1. Κάθε ένας κόμβος της λίστας ουσιαστικά αποτελεί μία συγκεκριμένη διακριτή
οντότητα στην λίστα. Είναι σαφές ότι ένας ευέλικτος σχεδιασμός της λίστας είναι να
οριστεί κάθε κόμβος σαν μία μεταβλητή δομής η οποία περιέχει τόσο το περιεχόμενο –
στοιχείο όσο και τον δείκτη που δείχνει στο επόμενο στοιχείο. Το περιεχόμενο μπορεί
να είναι μία απλή μεταβλητή πχ. ακεραίων όταν έχουμε μία λίστα ακεραίων αριθμών.
Τότε στη γλώσσα C θα έχει την μορφή εγγραφής (structure) ως εξής:
struct listnode {
int num;
struct listnode *next;
} node;
Η μεταβλητή node είναι τύπου εγγραφής και αποθηκεύει το περιεχόμενο ενός
κόμβου. Έχει δύο επιμέρους πεδία, τα num και next και για την προσπέλασή τους
χρησιμοποιείται ο τελεστής της τελείας ., δηλ. node.num είναι ο ακέραιος που
αποθηκεύει η μεταβλητή του τύπου listnode.
Επίσης, το περιεχόμενο-πληροφορία ενός κόμβου μπορεί να είναι ένα σύνολο
μεταβλητών όπως πχ. στην περίπτωση μίας ταχυδρομικής λίστας που στη C θα
μπορούσε να έχει την παρακάτω μορφή:
struct addressnode {
char name[40];
char street[40];
char city[20];
char state[3];
char zip[10];
struct addressnode *next;
} node;
2. Παράλληλα, όπως θα προσέξατε στα προηγούμενα παραδείγματα, ο δείκτης που
λειτουργεί σαν σύνδεσμος με τον επόμενο κόμβο έχει την μορφή:
struct listnode *next;
ή
struct addressnode *next;
Ο τελεστής * σημαίνει ότι χρησιμοποιείται ένας δείκτης διευθύνσεων (pointer) ως
σύνδεσμος με το επόμενο στοιχείο. Αυτός ο δείκτης σε κάθε κόμβο περιέχει τη
διεύθυνση μνήμης που περιέχει τον επόμενο κόμβο (δηλαδή όλα τα στοιχεία της
δομής κόμβου).

Για τις πράξεις που θα δούμε στη συνέχεια θεωρούμε ότι έχουμε μια απλά συνδεδεμένη
λίστα ακεραίων και χρησιμοποιούμε τη μεταβλητή δείκτη node που δείχνει σε έναν κόμβο της
λίστας. Στη C o συμβολισμός node->num αναφέρεται στον ίδιο τον ακέραιο αριθμό του
κόμβου που δείχνει ο node ενώ ο συμβολισμός node->next αναφέρεται στο δείκτη του

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 54


Αλγόριθμοι και Δομές Δεδομένων

κόμβου προς τον επόμενό του. Τέλος, για την επεξεργασία μίας λίστας ορίζουμε και δύο
δείκτες, έστω head και last, που δείχνουν στον πρώτο και στον τελευταίο κόμβο της
λίστας.
Οι τυπικές δηλώσεις στη C για τα παραπάνω έχουν ως εξής:
Struct listnode {
int num;
Struct listnode *next;
};
Struct listnode *head, *last, *node;

Εισαγωγή στοιχείου
Για να γίνει η εισαγωγή ενός στοιχείου πρέπει να δημιουργηθεί ένας νέος κόμβος στην
λίστα στον οποίο να αποθηκευτεί. Η θέση του κόμβου ορίζεται με βάση τις απαιτήσεις του
προγράμματος. Σε κάθε περίπτωση, ανάλογα με το είδος της λίστας (απλή, διπλή ή κυκλική),
γίνεται μόνο τροποποίηση των περιεχομένων των συνδέσμων επόμενου στοιχείου των
κατάλληλων κόμβων.
Πιο συγκεκριμένα, μπορεί ο κόμβος να εισαχθεί στο μέσο της λίστας όπου ο σύνδεσμος
επόμενου στοιχείου του προηγούμενου κόμβου θα δείχνει στον νέο κόμβο. ενώ ο σύνδεσμος
του νέου κόμβου θα πάρει την τιμή του είχε ο σύνδεσμος του προηγούμενου. Στην περίπτωση
που ο κόμβος – στοιχείο εισάγεται στο τέλος της λίστας τότε ο σύνδεσμος επόμενου στοιχείου
του τελευταίου κόμβου που είναι NULL πλέον δείχνει στο νέο στοιχείο ο σύνδεσμος του
οποίου παίρνει την τιμή NULL και είναι ο τελευταίος κόμβος. Οι πιο συνηθισμένες
περιπτώσεις εισαγωγής στοιχείων σε λίστες είναι είτε εισαγωγή στοιχείου σε ταξινομημένη
λίστα είτε στον τέλος μιας απλά συνδεδεμένης λίστας.
Στο παρακάτω σχήμα, φαίνεται η εισαγωγή ενός στοιχείου σε μία απλά συνδεδεμένη
λίστα.

Α Β C N
0

Α Β C N
0

Σχήμα 15: Εισαγωγή του στοιχείου Χ σε μια απλά συνδεδεμένη λίστα

O αλγόριθμος που υλοποιεί την παραπάνω εισαγωγή παρουσιάζεται παρακάτω:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 55


Αλγόριθμοι και Δομές Δεδομένων

Αλγόριθμος Εισαγωγής
Δεδομένα // Ο δείκτης pro_node που δείχνει τον κόμβο μετά τον οποίο θα εισαχθεί το
στοιχείο, ο δείκτης node που δείχνει τη θέση (διεύθυνση) του νέου κόμβου και
το στοιχείο x που είναι ο ακέραιος που θα εισαχθεί //
Αρχή
node->num = x
node->next = pro_node->next
pro_node->next = node
Τέλος Εισαγωγής

Δεδομένου ότι έχουμε τη θέση στην οποία εισάγεται το στοιχείο x (δείκτης pro_node), ο
παραπάνω αλγόριθμος εκτελείται σε χρόνο Ο(1).
Πρέπει να σημειώσουμε ότι ο παραπάνω αλγόριθμος εισαγωγής προϋποθέτει ότι έχει
γίνει μία αίτηση δέσμευσης χώρου από τη μνήμη του συστήματος για τον κόμβο node. Μία
τέτοια αίτηση στις γλώσσες προγραμματισμού είναι μία ρουτίνα ή συνάρτηση. Τέτοια
συνάρτηση στη γλώσσα C είναι η malloc() η οποία συντάσσεται στην περίπτωσή μας ως
εξής:
node = (struct listnode *)malloc(sizeof(struct listnode));
Η malloc() είτε επιστρέφει μία διεύθυνση μνήμης στην οποία μπορεί να αποθηκευτεί ο
κόμβος είτε επιστρέφει την τιμή NULL που σημαίνει ότι δεν μπορεί να ικανοποιηθεί το αίτημα.
Επίσης, τα bytes του χώρου της μνήμης δεν παίρνουν αρχική τιμή. Η λειτουργία της malloc()
απεικονίζεται σχηματικά παρακάτω:

Σχήμα 16: Δυναμική δέσμευση μνήμης για τη δομή του δείκτη node με τη χρήση της
malloc()

Διαγραφή στοιχείου
Για να γίνει η διαγραφή ενός στοιχείου από τη λίστα αρκεί να τροποποιήσουμε το
περιεχόμενο του συνδέσμου επόμενου στοιχείου του προηγούμενου κόμβου. Αν αυτός δείχνει

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 56


Αλγόριθμοι και Δομές Δεδομένων

στον κόμβο που δείχνει ο σύνδεσμος του προς διαγραφή κόμβου τότε ουσιαστικά δεν υπάρχει
κόμβος που να δείχνει σ’ αυτόν ενώ η αλυσίδα στοιχείων της λίστας δεν “κόβεται”.
Στο παρακάτω σχήμα, φαίνεται η διαγραφή ενός στοιχείου από μία απλά συνδεδεμένη
λίστα.

Α Β C N
0

Α Β C N
0

Σχήμα 17: Διαγραφή του στοιχείου Β από μια απλά συνδεδεμένη λίστα

Στην περίπτωση της διαγραφής ενός κόμβου θα πρέπει να επιστρέψουμε στο σύστημα το
χώρο που καταλαμβάνει ο κόμβος αυτός ώστε να μπορεί να χρησιμοποιηθεί από άλλη
εφαρμογή. Αν node είναι ο δείκτης στον κόμβο, τότε στη C η αποδέσμευση χώρου γίνεται με
την εκτέλεση της τύπου void συνάρτησης:
free(node);
Η λειτουργία της free() μπορεί να παρασταθεί με το ακόλουθο σχήμα:

Σχήμα 18: Αποδέσμευση μνήμης που καταλάμβανε η δομή του δείκτη node μετά την
εκτέλεση της free(node)

Θα πρέπει να θυμόμαστε πάντα ότι ο κανόνας δέσμευση – αποδέσμευση είναι καθοριστικός


παράγοντας για τη σωστή και αποδοτική διαχείριση της μνήμης του συστήματος.
Ακολουθεί ο αλγόριθμος για την πράξη της διαγραφής:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 57


Αλγόριθμοι και Δομές Δεδομένων

Αλγόριθμος Διαγραφής
Δεδομένα // Ο δείκτης pro_node που δείχνει τον προηγούμενο κόμβο από αυτόν που
θέλουμε να διαγράψουμε //
Αρχή
node = pro_node->next
pro_node->next = node->next
Aπελευθέρωσε το χώρο που καταλαμβάνει ο node
Τέλος Διαγραφής

Και η πράξη αυτή εκτελείται σε Ο(1) χρόνο.


Αν και εκ πρώτης άποψης ο αλγόριθμος της διαγραφής είναι ιδιαίτερα απλός, παρόλα
αυτά δημιουργείται ένα από τα δυσκολότερα προβλήματα στην διαχείριση λιστών. Το
πρόβλημα σχετίζεται με το γεγονός ότι με τη συνεχή διαγραφή κόμβων της λίστας
δημιουργούνται κενές θέσεις ιδιαίτερα στην περίπτωση στατικών λιστών που υλοποιούνται με
πίνακες. Έτσι, μετά από ένα σύνολο ακυρωμένων θέσεων είναι δυνατό η λίστα να φαίνεται
γεμάτη ενώ δεν είναι. Το γεγονός αυτό το συναντήσαμε και στην περίπτωση της ουράς και
λύθηκε με την μετακίνηση των στοιχείων αφού οι κενές θέσεις ήταν συνεχόμενες. Στις λίστες
όμως οι κενές θέσεις δεν είναι συνεχόμενες αλλά διάσπαρτες. Αυτό δυσκολεύει αρκετά την
υλοποίηση ενός αλγόριθμου για την αντιμετώπιση του προβλήματος και αυτό γιατί απαιτείται
η απομνημόνευση από το πρόγραμμα όλων των ακυρωμένων θέσεων ώστε σε κάθε εισαγωγή
να μπορεί να παραχωρήσει μία από αυτές τις κενές θέσεις.
Για την επίλυση του συγκεκριμένου προβλήματος χρησιμοποιούνται γενικά δύο τρόποι:
1. Χρήση στοίβας: Στην περίπτωση αυτή κάθε φορά που διαγράφεται ένας κόμβος τότε η
θέση του εισάγεται σε μία στοίβα. Για το σκοπό αυτό θα μπορούσε να χρησιμοποιηθεί και
η δομή της ουράς αλλά η υλοποίηση με στοίβα είναι πιο εύκολη. Όταν ζητείται μία
εισαγωγή στοιχείου τότε το πρόγραμμα εξετάζει στη στοίβα αν υπάρχει κενή θέση που
προέκυψε από διαγραφή. Με τον τρόπο αυτό δεν σπαταλάται χώρος και η λίστα είναι
γεμάτη μόνο όταν δεν υπάρχει επιπλέον αποθηκευτικός χώρος και ταυτόχρονα η στοίβα
είναι άδεια.
2. Χρήση λίστας: Αντί της στοίβας μπορούμε να χρησιμοποιήσουμε μία λίστα στην οποία
αποθηκεύονται όλες οι κενές θέσεις που δημιουργούνται μετά από διαγραφή στοιχείων
από την αρχική λίστα. Ο τρόπος με τον οποίο διατίθενται οι κενές θέσεις έχει σχέση με το
είδος της λίστας που χρησιμοποιείται ανάλογα με τις απαιτήσεις του χρήστη.

Σάρωση (διαπέραση) λίστας


Με την πράξη αυτή προσπελαύνονται όλα τα στοιχεία της λίστας ξεκινώντας από το
πρώτο και ακολουθώντας τους κόμβους μέσω των συνδέσμων τους μέχρι το τελευταίο
στοιχείο όπου ο σύνδεσμος προς το επόμενο έχει την τιμή ΝULL. Προφανώς, γνωρίζουμε
ποιο είναι τόσο το πρώτο όσο και το τελευταίο στοιχείο της λίστας μέσω των δεικτών head και
last που χρησιμοποιήσαμε παραπάνω στη δήλωση της δομής της λίστας.
Ο αλγόριθμος σάρωσης της λίστα παρουσιάζονται παρακάτω:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 58


Αλγόριθμοι και Δομές Δεδομένων

Αλγόριθμος Σάρωσης Λίστας


Δεδομένα // Ο δείκτης head που δείχνει στο πρώτο στοιχείο της λίστας //
Αρχή
node = head;
Όσο (node <> NULL) Εκτέλεσε
Εμφάνισε στοιχείο node->num
node = node->next
Τέλος Όσο
Τέλος Σάρωσης Λίστας

Η πράξη της σάρωσης εκτελείται σε χρόνο Θ(μέγεθος λίστας) δεδομένου ότι ο αλγόριθμος
επισκέπτεται όλους τους κόμβους της λίστας και σε κάθε κόμβο ξοδεύει Ο(1) χρόνο.

Αναζήτηση στοιχείου
Με την πράξη αυτή μπορεί να γίνει η αναζήτηση ενός στοιχείου που επιθυμούμε. Για την
υλοποίηση της πράξης αυτής χρησιμοποιείται η πράξη της σάρωσης ελαφρά παραλλαγμένη.
Ο αλγόριθμος αναζήτησης του στοιχείου x στη λίστα παρουσιάζεται παρακάτω:

Αλγόριθμος Αναζήτησης σε Λίστα


Δεδομένα // Ο δείκτης head που δείχνει στο πρώτο στοιχείο της λίστας //
Αρχή
node = head
Όσο (node <> NULL) Εκτέλεσε
Αν (node->num = x)
Επέστρεψε TRUE
node = node->next
Τέλος Όσο
Eπέστρεψε FALSE
Τέλος Αναζήτησης σε Λίστα

Η εκτέλεση του αλγόριθμου απαιτεί χρόνο Ο(μέγεθος λίστας). Η χειρότερη περίπτωση


συμβαίνει όταν το x δεν υπάρχει στη λίστα ή είναι το τελευταίο στοιχείο της, οπότε ο
αλγόριθμος επισκέπτεται όλους τους κόμβους της λίστας.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 59


Αλγόριθμοι και Δομές Δεδομένων

Συνένωση λιστών
Με την πράξη αυτή είναι δυνατή η συνένωση δύο λιστών σε μία διατηρώντας τις αρχικές
προϋποθέσεις που δίνονται. Δηλαδή η συνένωση μπορεί να γίνει απλά τοποθετώντας τη μία
λίστα μετά την άλλη είτε με συγκεκριμένο αλγόριθμο στην περίπτωση πχ. που οι αρχικές
λίστες είναι ταξινομημένες και επιθυμούμε η νέα λίστα να είναι και αυτή ταξινομημένη.
Παρακάτω δίνουμε τον αλγόριθμο συνένωσης για την απλή περίπτωση:

Αλγόριθμος Συνένωσης Λιστών


Δεδομένα // Ο δείκτης head που δείχνει στο πρώτο στοιχείο της δεύτερης λίστας, ο
δείκτης last που δείχνει στο τελευταίο στοιχείο της πρώτης λίστας //
Αρχή
last->next = head
Τέλος Συνένωσης Λιστών

Στην περίπτωση των ταξινομημένων λιστών θα πρέπει να εφαρμόσουμε έναν αλγόριθμο


συγχώνευσης τον οποίο θα δούμε στο επόμενο κεφάλαιο όταν και θα αναφερθούμε στον
αλγόριθμο ταξινόμησης mergesort.

Αντιστροφή λίστας
Με την πράξη αυτή αλλάζει η φορά διάταξης της λίστας ορίζοντας τον τελευταίο κόμβο ως
πρώτο και τον πρώτο τελευταίο τροποποιώντας κατάλληλα τους συνδέσμους όλων των
κόμβων της λίστας. Η διαδικασία αυτή φαίνεται στο επόμενο σχήμα:

Σχήμα 19: Αντιστροφή των στοιχείων μιας λίστας

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 60


Αλγόριθμοι και Δομές Δεδομένων

Ο αλγόριθμος της αντιστροφής χρησιμοποιεί δύο βοηθητικές μεταβλητές που ελέγχουν την
προσπέλαση στους κόμβους της λίστας:

Αλγόριθμος Αντιστροφής Λίστας


Δεδομένα // Ο δείκτης head που δείχνει στο πρώτο στοιχείο της λίστας,
ο δείκτης last του τελευταίου στοιχείου της λίστας,
βοηθητικοί δείκτες t, y //
Αρχή
node = head
y = node->next
Όσο (node<> last) Εκτέλεσε
t = y->next
y->next = node
node = y
y=t
Τέλος Όσο
last = head
head = node
last->next = NULL
Τέλος Αντιστροφής Λίστας

Εδώ ο χρόνος εκτέλεσης του αλγόριθμου είναι Ο(μέγεθος λίστας) αφού επισκέπτεται
όλους τους κόμβους για να ενημερώσει κατάλληλα το δείκτη next του κόμβου.

2.4.4 Δυναμική υλοποίηση στοίβας και ουράς


Η δυναμική υλοποίηση της δομής της στοίβας ή της ουράς με τη χρήση δεικτών
συνεπάγεται αποδοτικότερη διαχείριση του χώρου της δομής. Μια στοίβα θα μπορούσε να
υλοποιηθεί για παράδειγμα ως μια απλά συνδεδεμένη λίστα για την οποία χρησιμοποιούμε
τις ακόλουθες δηλώσεις στη C:
Struct stacknode {
int num;
Struct stacknode *next;
};

Struct stacknode *head;

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 61


Αλγόριθμοι και Δομές Δεδομένων

Oι αλγόριθμοι για τις πράξεις push(x) και pop() θα είχαν τώρα την εξής μορφή:

Αλγόριθμος Push(x)
Δεδομένα // Δείκτης head που δείχνει στο πρώτο στοιχείο (κορυφή) της στοίβας; x:
ακέραια; p βοηθητικός δείκτης σε κόμβο της στοίβας //
Αρχή
Δέσμευσε χώρο για το δείκτη p
Αν δεν είναι δυνατή η δέσμευση χώρου Τότε Επέστρεψε FALSE
Αλλιώς
p->num = x
p->next = head
head = p
Επέστρεψε ΤRUE
Tέλος Αν
Τέλος Push

Αλγόριθμος Pop()
Δεδομένα // Δείκτης head που δείχνει στο πρώτο στοιχείο (κορυφή) της στοίβας; x:
ακέραια; p βοηθητικός δείκτης σε κόμβο της στοίβας //
Αρχή
Αν (head = NULL) Τότε Επέστρεψε FALSE
Αλλιώς
p = head
x = p->num /* Στη σφαιρική μεταβλητή x επιστρέφεται το αποτέλεσμα της Pop */
head = head->next
Απελευθέρωσε το χώρο του p
Επέστρεψε ΤRUE
Tέλος Αν
Τέλος Pop

Για τη δυναμική υλοποίηση μιας ουράς αρκεί επίσης μια απλά συνδεδεμένη λίστα με τους
δείκτες head και last να δείχνουν στο πρώτο και στο τελευταίο στοιχείο της λίστας αντίστοιχα.
H πράξη enque(x) εισάγει το στοιχείο x πάντα μετά τον κόμβο που δείχνει ο δείκτης last και
παράλληλα ενημερώνει τον last, ενώ η πράξη Dequeue() επιστρέφει στη σφαιρική μεταβλητή x
την τιμή του κόμβου head ενημερώνοντας επίσης τον head.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 62


Αλγόριθμοι και Δομές Δεδομένων

Για να δούμε τη διαχείριση και μιας διπλά συνδεδεμένης λίστας, στη συνέχεια υλοποιούμε
την ουρά με μία τέτοια λίστα μέσω των ακόλουθων C δηλώσεων:
Struct queuenode {
int num;
Struct queuenode *next, *previous;
};

Struct queuenode *head, *last;

Oι αλγόριθμοι των πράξεων enqueue(x) και dequeue() θα είναι τώρα oι εξής:

Αλγόριθμος Εnqueue(x)
Δεδομένα // Δείκτες head, last που δείχνoυν στο πρώτο και τελευταίο στοιχείο της ουράς
αντίστοιχα; x: ακέραια; p βοηθητικός δείκτης σε κόμβο της oυράς //
Αρχή
Δέσμευσε χώρο για το δείκτη p
Αν δεν είναι δυνατή η δέσμευση χώρου Τότε Επέστρεψε FALSE
Αλλιώς
p->num = x
p->next = NULL
p->previous = last
Αν (last = ΝULL) Τότε head = p // Η ουρά είναι άδεια //
Αλλιώς last->next = p
Τέλος Αν
last = p
Επέστρεψε ΤRUE
Tέλος Αν
Τέλος Εnqueue

Αλγόριθμος Dequeue()
Δεδομένα // Δείκτες head, last που δείχνoυν στο πρώτο και τελευταίο στοιχείο της ουράς
αντίστοιχα; x: ακέραια; p βοηθητικός δείκτης σε κόμβο της oυράς //
Αρχή
Αν (head = NULL) Τότε Επέστρεψε FALSE
Αλλιώς
p = head

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 63


Αλγόριθμοι και Δομές Δεδομένων

x = p->num
head = head->next
Αν (head = ΝULL) Τότε last = NULL // Η ουρά είχε ένα μόνο στοιχείο //
Αλλιώς head->previous = NULL
Τέλος Αν
Απελευθέρωσε το χώρο του p
Επέστρεψε ΤRUE
Tέλος Αν
Τέλος Dequeue

Ερωτήσεις Κεφαλαίου
1. Τι είναι οι γραμμικές δομές δεδομένων; Αναφέρατε τις σημαντικότερες από αυτές.
2. Αναφέρατε μερικά παραδείγματα μορφών πινάκων.
3. Τι είναι οι συμβολοσειρές; Ποια είναι η κύρια διαφορά όταν σε πίνακα κάθε στοιχείο του
που είναι συμβολοσειρά είναι σταθερού ή μεταβλητού μεγέθους;
4. Εξηγείστε γιατί ο αλγόριθμος εύρεσης του μεγαλύτερου στοιχείο ενός πίνακα που είδαμε
είναι βέλτιστος σε σχέση με το πλήθος συγκρίσεων.
5. Περιγράψτε τη δομή της στοίβας και τις πράξεις που υποστηρίζει.
6. Δώστε τους αλγόριθμους των πράξεων push και pop σε μία στοίβα που υλοποιείται με
πίνακα.
7. Eξηγείστε τον τρόπο υλοποίησης της αναδρομής σε μια γλώσσα προγραμματισμού.
8. Τι είναι ο πολωνικός συμβολισμός. Πως υλοποιείται η μετατροπή μιας παράστασης από
την ένθετη στη μεταθετική μορφή;
9. Τι είναι η ουρά. Ποιες οι βασικές της πράξεις;
10. Δώστε τους αλγόριθμους των πράξεων enqueue και dequeue σε μία ουρά pipeline και σε
μια ουρά δακτυλίου.
11. Ποια είναι τα δύο κοινά χαρακτηριστικά της ουράς και της στοίβας;
12. Τι είναι λίστα. Ποιες οι βασικές της εφαρμογές; Ποιες οι βασικές πράξεις που υποστηρίζει;
13. Δώστε τους αλγόριθμους εισαγωγής και διαγραφής σε μία απλά συνδεδεμένη λίστα.
14. Δώστε τον αλγόριθμο αντιστροφής μιας απλά συνδεδεμένης λίστας.
15. Δώστε τους αλγόριθμους των πράξεων push και pop σε μία στοίβα που υλοποιείται
δυναμικά ως μία απλά συνδεδεμένη λίστα.
16. Δώστε τους αλγόριθμους των πράξεων enqueue και dequeue σε μία ουρά που
υλοποιείται δυναμικά: (α) ως μία απλά συνδεδεμένη λίστα και (β) ως μία διπλά
συνδεδεμένη λίστα.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 64


Αλγόριθμοι και Δομές Δεδομένων

ΚΕΦΑΛΑΙΟ 3: ΤΑΞΙΝΟΜΗΣΗ, ΑΝΑΖΗΤΗΣΗ ΚΑΙ ΕΠΙΛΟΓΗ


Στα προηγούμενα κεφάλαια περιγράψαμε τον τρόπο με τον οποίο τα δεδομένα
οργανώνονται μεταξύ τους και δημιουργούν τις γραμμικές δομές δεδομένων ώστε στη
συνέχεια να μπορεί να γίνει η διαχείρισή τους με τη βοήθεια συγκεκριμένων πράξεων. Στην
περιγραφή κάθε δομής δεδομένων αναλύσαμε τις πράξεις ένθεσης και απώθησης χωρίς να
κάνουμε ιδιαίτερη αναφορά στις λειτουργίες ταξινόμησης, αναζήτησης και επιλογής που
αποτελούν και το αντικείμενο αυτού του κεφαλαίου. Στο κεφάλαιο αυτό αρχικά θα
περιγράψουμε την πράξη της ταξινόμησης και τους αλγόριθμους που χρησιμοποιούνται για
την υλοποίησή της οι οποίοι και χωρίζονται στους βασιζόμενους σε συγκρίσεις (comparison
based) ή συγκριτικούς και σε εκείνους που χρησιμοποιούν την αναπαράσταση των
στοιχείων εισόδου (representation based). Θα δούμε επίσης μεθόδους ταξινόμησης που
εφαρμόζονται στην βοηθητική μνήμη. Στο δεύτερο μέρος θα περιγράψουμε τις πιο βασικές
μεθόδους αναζήτησης που εφαρμόζονται σε έναν ταξινομημένο πίνακα και στο τρίτο μέρος θα
παρουσιάσουμε δύο αλγόριθμους για την επιλογή του i-oστού μεγαλύτερου στοιχείου σε έναν
τυχαίο πίνακα στοιχείων.

3.1 Το πρόβλημα της Ταξινόμησης


Η πράξη της αναζήτησης σε μια γραμμική δομή δεδομένων όπως ένας πίνακας είναι πιο
εύκολη όταν τα στοιχεία της δομής έχουν κάποιου είδους διάταξη. Ας αναλογιστούμε την
αναζήτηση σε πραγματικά πληροφοριακά συστήματα και σε αρχεία μεγάλων βάσεων
δεδομένων οργανισμών. Δεν είναι τυχαίο ότι, σύμφωνα με εκτιμήσεις της IBM, το 25% του
χρόνου υπολογισμού αφιερώνεται σε ταξινομήσεις στοιχείων.
Η λειτουργία της ταξινόμησης στοιχείων σε δομές δεδομένων αφορά σε δύο περιπτώσεις:
1. Εσωτερική ταξινόμηση (Internal Sorting). Στην περίπτωση αυτή τα στοιχεία της δομής
δεδομένων τα οποία καλείται ο αλγόριθμος να διατάξει είναι αποθηκευμένα στην κεντρική
μνήμη.
2. Εξωτερική ταξινόμηση (External Sorting). Στην περίπτωση αυτή τα στοιχεία της δομής
δεδομένων τα οποία καλείται ο αλγόριθμος να διατάξει είναι αποθηκευμένα στην
δευτερεύουσα μνήμη.
Ο λόγος που οδηγεί την αποθήκευση των δομών δεδομένων στην δευτερεύουσα μνήμη
είναι το μικρό μέγεθος της κεντρικής μνήμης σε σχέση με το σύνολο των στοιχείων που
απαιτείται να διαταχθούν. Στη συνέχεια του κεφαλαίου θα εξετάσουμε και τις δύο κατηγορίες
αλγόριθμων ταξινόμησης
Όλοι οι αλγόριθμοι στην περίπτωση της ταξινόμησης των στοιχείων ενός πίνακα έχουν:
Είσοδο: Έναν πίνακα S[1..n]
Έξοδο : Έναν πίνακα S[1..n] με S[i] ≤ S[i+1] όπου 1 ≤ i ≤ n-1.
Οι αλγόριθμοι ταξινόμησης μπορούν να ταξινομηθούν περαιτέρω σε συγκριτικούς και
βασιζόμενους στην αναπαράσταση των τιμών των στοιχείων εισόδου. Οι συγκριτικοί
αλγόριθμοι χρησιμοποιούν ως βασική πράξη τη σύγκριση για να αποφασίσουν για τη σειρά
δύο στοιχείων και τέτοιοι είναι οι αλγόριθμοι Ταξινόμησης Φυσαλίδας (Bubblesort),
Ταξινόμησης με Εισαγωγή (Insertionsort), Ταξινόμησης με Επιλογή (Selectionsort),
Ταξινόμησης Σωρού (Heapsort), Ταξινόμησης με Συγχώνευση (Mergesort) και Γρήγορης
Ταξινόμησης (Quicksort). Αναπαράσταση των τιμών των στοιχείων εισόδου χρησιμοποιούν
οι αλγόριθμοι Ταξινόμησης με Μέτρηση (Countingsort) και Radixsort.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 65


Αλγόριθμοι και Δομές Δεδομένων

Στη συνέχεια εξετάζουμε κάθε έναν από τους παραπάνω αλγόριθμους και αναλύουμε την
πολυπλοκότητά του στη μέση και χειρότερη περίπτωση. Στο τέλος του κεφαλαίου
αποδεικνύουμε ένα κάτω φράγμα στη χρονική πολυπλοκότητα των συγκριτικών αλγόριθμων
ταξινόμησης.

3.1.1 Ταξινόμηση Φυσαλίδας (Bubblesort)


Η κεντρική ιδέα του αλγόριθμου είναι η εξής:
Εξετάζονται ανά ζεύγη όλα τα στοιχεία του πίνακα ξεκινώντας από το τελευταίο (το δεξιότερο
του πίνακα). Εφόσον S[i] > S[i+1] γίνεται αντιμετάθεσή τους (αμοιβαία αλλαγή της θέσης
τους). Η διαδικασία αυτή εκτελείται για όλα τα στοιχεία του πίνακα και τόσες διαφορετικές
διαπεράσεις (περάσματα) μέχρι να ταξινομηθεί ολόκληρος ο πίνακας. Η i-οστή διαπέραση
(1≤i≤n-1) ξεκινά από το στοιχείο στη θέση n του πίνακα και φτάνει μέχρι το στοιχείο στη θέση
i εξασφαλίζοντας έτσι ότι όλα τα στοιχεία από την πρώτη μέχρι και τη θέση αυτή έχουν μπει
στη σωστή τους θέση.
Πιο συγκεκριμένα ο αλγόριθμος εκτελείται ως εξής:
1η διαπέραση
Ο αλγόριθμος συγκρίνει το τελευταίο στοιχείο του πίνακα με το προηγούμενό του. Αν το
προηγούμενο είναι μεγαλύτερο αντιμεταθέτει τα δύο στοιχεία και συνεχίζει την ίδια διαδικασία
μέχρι το πρώτο στοιχείο του πίνακα. Στο τέλος το μικρότερο στοιχείο του πίνακα αποθηκεύεται
στην πρώτη θέση (S[1]).
2η διαπέραση
Η 2η διαπέραση είναι επανάληψη της πρώτης όχι όμως για το σύνολο των στοιχείων του
πίνακα αλλά για τα n-1 στοιχεία S[2..n], δηλ. εκτός του S[1] αφού αυτό έχει μπει ήδη στη
σωστή του θέση.
3η διαπέραση
Η 3η διαπέραση είναι μία επανάληψη της δεύτερης για τα υπόλοιπα n-2 στοιχεία του
πίνακα S[3..n] (τα S[1] και S[2] είναι ήδη στη σωστή τους θέση).
Γενικά στην i-οστή διαπέραση έχουν τοποθετηθεί στη σωστή τους θέση τα i πρώτα στοιχεία
του πίνακα (S[1] έως S[i]), επομένως η επεξεργασία γίνεται στα υπόλοιπα n-i στοιχεία. Η
μετακίνηση προς τα αριστερά του μικρότερου στοιχείου μέχρι να μπει στη σωστή του θέση
κάθε φορά θυμίζει την κίνηση μιας φυσαλίδας αέρα μέσα σε ένα υγρό μέχρι να φτάσει στην
επιφάνειά του, γι’ αυτό και η αλγόριθμος πήρε το όνομα ταξινόμηση φυσαλίδας.
Το επόμενο παράδειγμα βοηθάει για να καταλάβουμε τη λειτουργία του αλγόριθμου:
Παράδειγμα
Είσοδος: 5, 4, 2, 8, 9, 1, 6, 7
Æ 1, 5, 4, 2, 8, 9, 6, 7
Æ 1, 2, 5, 4, 6, 8, 9, 7
Æ 1, 2, 4, 5, 6, 7, 8, 9
Τα στοιχεία του πίνακα έχουν ταξινομηθεί και οι επόμενες διαπεράσεις δεν επιφέρουν
αλλαγές.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 66


Αλγόριθμοι και Δομές Δεδομένων

Παρακάτω δίνεται ο βασικός αλγόριθμος bubblesort σε μορφή ψευδοκώδικα:

Αλγόριθμος Bubblesort
Είσοδος (S, n)
Δεδομένα // i, j: ακέραιοι //
Αρχή
Για i = 2 Μέχρι n με Βήμα 1
Για j = n Μέχρι i με Βήμα -1
Αν S[j-1] > S[j] Τότε
Αντιμετάθεσε S[j-1] , S[j]
Τέλος Αν
Τέλος Επανάληψης
Τέλος Επανάληψης
Αποτελέσματα // Ταξινομημένος πίνακας S[1..n] //
Τέλος Bubblesort

Από τον τρόπο λειτουργίας του βασικού αλγόριθμου προκύπτει ότι για το πλήθος των
συγκρίσεων T(n) που εκτελούνται ισχύει:
1
Τ(n) =
∑i = (n-1) + (n-2) +…+ 1= n (n-1) / 2,
i=n−1
και το πλήθος αυτό είναι ανεξάρτητο από τη μορφή της εισόδου, αν δηλ. ο πίνακας είναι ήδη
ταξινομημένος ή όχι. Γι’ αυτόν ακριβώς το λόγο για τον bubblesort έχουν αναπτυχθεί αρκετές
παραλλαγές που βελτιώνουν την απόδοση της βασικής έκδοσης σε πολλές πρακτικές
περιπτώσεις.
Μια πρώτη βελτίωση μπορούμε να έχουμε όταν σε κάθε διαπέραση του πίνακα
ελέγχουμε αν έχει ήδη ταξινομηθεί. Αυτό είναι εύκολο: ο πίνακας έχει ταξινομηθεί πλήρως
μετά τα πρώτα i περάσματα αν στο επόμενο πέρασμα i+1 δεν συμβεί καμία αντιμετάθεση.
Χρησιμοποιώντας λοιπόν μια έξτρα δυαδική μεταβλητή sorted που θα μας λέει αν έγινε ή όχι
τουλάχιστον μία αντιμετάθεση στην τρέχουσα διαπέραση, παίρνουμε την παρακάτω
παραλλαγή του βασικού αλγόριθμου:

Αλγόριθμος Bubblesort_παραλλαγή 1
Είσοδος (S, n)
Δεδομένα // i, j: ακέραιοι; sorted: δυαδική //
Aρχή
sorted = FALSE
i=2

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 67


Αλγόριθμοι και Δομές Δεδομένων

Όσο (i <= n AND sorted = FALSE) Εκτέλεσε


sorted = TRUE
Για j = n Μέχρι i με Βήμα -1
Αν S[j-1] > S[j] Τότε
Αντιμετάθεσε S[j-1] , S[j]
sorted = FALSE
Τέλος Αν
Τέλος Επανάληψης
i = i+1
Τέλος Όσο
Αποτελέσματα // Ταξινομημένος πίνακας S[1..n] //
Τέλος Bubblesort_παραλλαγή 1

Με την παραλλαγή αυτή, όταν ο αρχικός πίνακας είναι ήδη ταξινομημένος, τότε εκτελείται
1 μόνο διαπέραση και n-1 συγκρίσεις και ο αλγόριθμος τερματίζει χωρίς να γίνει καμία
αντιμετάθεση. Φυσικά, στην περίπτωση που ο αλγόριθμος είναι ταξινομημένος με την
αντίστροφη (φθίνουσα) σειρά εκτελούνται n-1 διαπεράσεις με το μέγιστο πλήθος συγκρίσεων
όπως και στο βασικό αλγόριθμο που είναι n(n-1)/2. Στην περίπτωση αυτή γίνονται ακριβώς
και n(n-1)/2 αντιμεταθέσεις.
Η συγκεκριμένη παραλλαγή αποτελεί μια βελτίωση αλλά δεν είναι η καλύτερη. Στην
πράξη, η απόδοση του αλγόριθμου μπορεί να βελτιωθεί ακόμα περισσότερο αν σε κάθε
διαπέραση προχωρούμε μέχρι τη θέση όπου έγινε η τελευταία αντιμετάθεση. Αν ξέρουμε ότι σε
κάποιο πέρασμα i η τελευταία αντιμετάθεση έγινε στην θέση k (δηλ. μεταξύ των S[k] και S[k-
1]), τότε σίγουρα όλα τα στοιχεία του υποπίνακα S[1..k-1] είναι ταξινομημένα. Επομένως, στο
επόμενο πέρασμα k+1 αρκεί να επισκεφτούμε μόνο τα στοιχεία του S[k..n].
H υλοποίηση αυτής της δεύτερης παραλλαγής είναι επίσης εύκολη. Χρησιμοποιούμε μία
μεταβλητή που κρατά σε κάθε πέρασμα του αλγόριθμου τη θέση της τελευταίας
(αριστερότερης) αντιμετάθεσης. Όταν η τιμή της μεταβλητής αυτής στο τέλος του περάσματος
είναι 0, αυτό σημαίνει ότι ο πίνακας έχει ταξινομηθεί και ο αλγόριθμος τερματίζει. Ο
αλγόριθμος δίνεται παρακάτω:

Αλγόριθμος Bubblesort_παραλλαγή 2
Είσοδος (S, n)
Δεδομένα // i, k, l: ακέραιοι //
Aρχή
l = 1 /* θέση αριστερότερης αντιμετάθεσης */
Όσο (l <> 0) Εκτέλεσε
k=0
Για i = n Μέχρι l+1 με Βήμα -1
Αν S[i-1] > S[i] Τότε

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 68


Αλγόριθμοι και Δομές Δεδομένων

Αντιμετάθεσε S[i-1], S[i]


k=i
Τέλος Αν
Τέλος Επανάληψης
l=k
Τέλος Όσο
Αποτελέσματα // Ταξινομημένος πίνακας S[1..n] //
Τέλος Bubblesort_παραλλαγή 2

Η δεύτερη παραλλαγή είναι καλή σε περιπτώσεις που αρκετά από τα πρώτα


(αριστερότερα) στοιχεία του πίνακα είναι ήδη ταξινομημένα ή περίπου ταξινομημένα. Και πάλι
όμως στη χειρότερη περίπτωση που ο πίνακας είναι ταξινομημένος με την αντίστροφη σειρά
εκτελούνται n(n-1)/2 συγκρίσεις και αντιμεταθέσεις ενώ στη μέση περίπτωση εκτελούνται οι
μισές περίπου πράξεις, δηλ. και στη μέση και στη χειρότερη περίπτωση ο αλγόριθμος
bubblesort για μέγεθος εισόδου n έχει χρονική πολυπλοκότητα Ο(n2).
Μια τελευταία παραλλαγή του αλγόριθμου φυσαλίδας επεκτείνει την προηγούμενη ιδέα ως
εξής: σε κάθε διαπέραση του πίνακα κινούμαστε πρώτα αριστερά και μετά δεξιά μέχρι τη θέση
της τελευταίας αντιμετάθεσης κάθε φορά. Η παραλλαγή αυτή είναι γνωστή με το όνομα
“Shakersort” λόγω των αντίθετων κινήσεων σε κάθε πέρασμα.
Παρακάτω δίνεται ο αλγόριθμος Shaker sort:

Αλγόριθμος Shakersort
Είσοδος (S, n)
Δεδομένα // i, k, l, r: ακέραιοι //
Αρχή
l = 1; r = n; k = 0
Όσο (l < r) Εκτέλεσε
Για i = l Μέχρι r-1 με Βήμα 1 /* κίνηση → */
Αν S[i] > S[i+1] Τότε
Αντιμετάθεσε S[i], S[i+1]
k=i
Τέλος Αν
Τέλος Επανάληψης
r=k
Για i = r Μέχρι l+1 με Βήμα -1 /* κίνηση ← */
Αν S[i-1] > S[i] Τότε
Αντιμετάθεσε S[i-1], S[i]
k=i

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 69


Αλγόριθμοι και Δομές Δεδομένων

Τέλος Αν
Τέλος Επανάληψης
l=k
Τέλος Όσο
Αποτελέσματα // Ταξινομημένος πίνακας S[1..n] //
Τέλος Shakersort

Στην πράξη ο αλγόριθμος Shakersort χρησιμοποιείται για να διορθώσει ασυμμετρίες κατά


την είσοδο και λειτουργεί καλά όταν υπάρχουν ήδη ταξινομημένα ή σχεδόν ταξινομημένα
τμήματα στην αρχή και στο τέλος του πίνακα. Ωστόσο, ανάλογα με τη μορφή της εισόδου, σε
κάποιες περιπτώσεις εκτελεί περισσότερες συγκρίσεις από την προηγούμενη παραλλαγή του
bubblesort. Φυσικά οι ασυμπτωτικές πολυπλοκότητες μέσης και χειρότερης περίπτωσης
παραμένουν Ο(n2).
Κλείνοντας την παρουσίαση του αλγόριθμου, επισημαίνουμε ότι όλες οι παραλλαγές του
βασικού αλγόριθμου bubblesort υπερτερούν στην πράξη μόνο στο πλήθος συγκρίσεων και όχι
στις αντιμεταθέσεις στοιχείων που είναι και πιο χρονοβόρες. Υπό την έννοια αυτή, οι όποιες
βελτιώσεις δεν είναι και πολύ σημαντικές αφού στη χειρότερη περίπτωση εκτελούνται πάντα
n*n(-1)/2 αντιμεταθέσεις. Αυτό οφείλεται ακριβώς στο γεγονός ότι αντιμεταθέτουν σε κάθε
βήμα τους μόνο γειτονικά στοιχεία, δηλ. μετακινούν ένα στοιχείο κατά μία μόνο θέση.
Οποιαδήποτε άλλη βελτίωση πρέπει να στηρίζεται στην αρχή της μετακίνησης στοιχείων σε
μεγαλύτερες αποστάσεις και τέτοιους αλγόριθμους θα δούμε παρακάτω.

3.1.2 Ταξινόμηση με Εισαγωγή (Insertionsort)


Η κεντρική ιδέα του αλγόριθμου αυτού είναι η εξής:
Σε κάθε διαπέραση i ταξινομείται o υποπίνακας S[1..i+1]. Εκτελούνται επομένως n-1
διαπεράσεις για την ταξινόμηση όλων των στοιχείων του πίνακα (το πρώτο στοιχείο του
πίνακα είναι ταξινομημένο σε σχέση με τον εαυτό του). Έστω τα πρώτα i στοιχεία του πίνακα
έχουν ταξινομηθεί και εξετάζουμε το S[i+1]. Ο αλγόριθμος συγκρίνει το στοιχείο S[i+1] με όλα
τα προηγούμενα και το τοποθετεί στη σωστή του θέση μετακινώντας όλα τα υπόλοιπα μία
θέση δεξιά. Έτσι τα στοιχεία S[1..i+1] είναι ταξινομημένα και εξετάζουμε το επόμενο στοιχείο
S[i+2]. Η διαδικασία επαναλαμβάνεται για όλα τα στοιχεία του πίνακα.
Παράδειγμα
Έστω έχουμε τον πίνακα S = [3, 5, 4, 6].
Ο αλγόριθμος εξετάζει τα στοιχεία και στο 2ο πέρασμα βρίσκει το πρώτο αταξινόμητο που
είναι το S[3] = 4. Στη συνέχεια ταξινομεί τον υποπίνακα S = [3, 5, 4]. Συγκρίνει το 4 με το S[2]
= 5 και μετακινεί το S[2] στην θέση του S[3]. Στη συνέχεια συγκρίνει το 4 με το S[1] = 3 κι
επειδή αυτό είναι μεγαλύτερο τίθεται S[2] = 4 και ο πίνακας διαμορφώνεται σε S = [3, 4, 5, 6].
Στην 3η και τελευταία διαπέραση ο αλγόριθμος συγκρίνει το S[4] = 6 με το τελευταίο στοιχείο
του προηγούμενου ταξινομημένου υποπίνακα, δηλ. το S[3] = 5. Αφού S[4] > S[3] ο αλγόριθμος
τερματίζει.
Στη συνέχεια περιγράφεται ο αλγόριθμος ταξινόμησης με εισαγωγή σε μορφή
ψευδοκώδικα:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 70


Αλγόριθμοι και Δομές Δεδομένων

Αλγόριθμος Insertionsort
Είσοδος (S, n)
Δεδομένα // i, j, t: ακέραιοι //
Αρχή
Για i = 1 Μέχρι n-1 με Βήμα 1
t = S[i+1]; j = i
Eπανέλαβε
Αν S[j] <= t Τότε έξοδος
S[j+1] = S[j]; j = j-1
Eνόσω j > 0
S[j+1] = t
Τέλος Επανάληψης
Αποτελέσματα // Ταξινομημένος πίνακας S[1..n] //
Τέλος Insertionsort

Το μέγιστο πλήθος συγκρίσεων του αλγόριθμου δίνεται από την παρακάτω σχέση:
n−1
T(n) =
∑i = n(n-1)/2 = Ο(n )
2

i =1
Στη χειρότερη περίπτωση οι μετακινήσεις στοιχείων σε κάθε διαπέραση είναι κατά 1
περισσότερες από το πλήθος των συγκρίσεων, επομένως το μέγιστο πλήθος μετακινήσεων
n−1
είναι
∑ (i + 1) = (n-1)(n+2)/2 = O(n ). 2

i=1
Η ίδια ασυμπτωτική πολυπλοκότητα ισχύει και στη μέση περίπτωση.

3.1.3 Ταξινόμηση με Επιλογή (Selectionsort)


Η κεντρική ιδέα του αλγόριθμου είναι η εξής:
Σε κάθε διαπέραση i εξετάζονται όλα τα στοιχεία του πίνακα και επιλέγεται το i-στό μικρότερο
στοιχείο. Στην 1η διαπέραση επιλέγεται το μικρότερο στοιχείο από τα n στοιχεία του πίνακα,
στη 2η διαπέραση επιλέγεται το αμέσως μικρότερο στοιχείο κ.ο.κ. Εκτελούνται επομένως
ακριβώς n-1 διαπεράσεις αφού μετά την (n-1)-οστή το μεγαλύτερο στοιχείο του πίνακα θα
έχει μπει στην τελευταία (δεξιότερη) θέση που είναι και η σωστή. Έστω τα πρώτα i στοιχεία
του πίνακα έχουν τοποθετηθεί στη σωστή τους θέση και εξετάζουμε τα υπόλοιπα S[i+1..n]. Θα
συγκρίνουμε το στοιχείο S[i+1] με όλα τα επόμενα στοιχεία, αν υπάρχει κάποιο μικρότερο
κρατούμε εκείνο και συνεχίζουμε τη σύγκριση μέχρι να εξετάσουμε όλα τα στοιχεία του πίνακα
και να βρούμε το μικρότερο από τα S[i+1..n]. Έτσι, τα στοιχεία S[1..i+1] έχουν μπει στη

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 71


Αλγόριθμοι και Δομές Δεδομένων

σωστή τους θέση ενώ στην επόμενη διαπέραση ψάχνουμε να βρούμε το (i+2)-οστό μικρότερο
στοιχείο. Η διαδικασία επαναλαμβάνεται για n-1 ακριβώς διαπεράσεις.
Παράδειγμα
Έστω έχουμε τον πίνακα S=[6, 5, 4, 7].
Ο αλγόριθμος εξετάζει για τη θέση 1 όλα τα στοιχεία. Στην πρώτη διαπέραση συγκρίνει το
S[1]=6 με το S[2]=5. To S[2] < S[1], κρατά ως μικρότερο το S[2] = 5 και συνεχίζει συγκρίνοντάς
το με το επόμενο στοιχείο του πίνακα που είναι το S[3] = 4. To S[3] < S[2], ο αλγόριθμος κρατά
ως μικρότερο το S[3] = 4 και συνεχίζει συγκρίνοντάς το με το επόμενο στοιχείο του πίνακα
που είναι το S[4] = 7. To S[4] > S[3], άρα το S[3] είναι το μικρότερο στοιχείο του πίνακα και
γίνεται αντιμετάθεση του S[1] με το S[3]. Η διαδικασία συνεχίζεται με τα υπόλοιπα n-1
στοιχεία [5, 6, 7] κ.ο.κ.
Ακολουθεί ο αλγόριθμος Selectionsort σε μορφή ψευδοκώδικα:

Αλγόριθμος Selectionsort
Είσοδος (S, n)
Δεδομένα // i, j, k: ακέραιοι //
Αρχή
Για i=1 Μέχρι n-1 με Βήμα 1
k = i /* θέση min στοιχείου στον S[i..n] */
Για j=i+1 Μέχρι n με Βήμα 1
Αν S[j] < S[k] Τότε k=j Tέλος Αν
Τέλος Επανάληψης
Αντιμετάθεσε τα S[i], S[k]
Τέλος Επανάληψης
Αποτελέσματα // Ταξινομημένος πίνακας S[1..n] //
Τέλος Selectionsort

Για τη εύρεση του μικρότερου στοιχείου στον υποπίνακα S[i..n] στην i-οστή διαπέραση
εκτελούνται n-i συγκρίσεις. Άρα, ο αλγόριθμος με επιλογή κάνει πάντα
n −1 n −1

∑ i =1
(n − i ) =

j =1
j = n(n-1)/2 συγκρίσεις ανεξάρτητα από το στιγμιότυπο εισόδου. Οι

αντιμεταθέσεις είναι μόνο n-1 αφού σε κάθε πέρασμα γίνεται μία μόνο αντιμετάθεση του
μικρότερου στοιχείου κι αυτό κάνει τον αλγόριθμο κατάλληλο σε εφαρμογές όπου οι
αντιμεταθέσεις κοστίζουν ακριβά, όταν πχ. επεξεργαζόμαστε σύνθετες εγγραφές.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 72


Αλγόριθμοι και Δομές Δεδομένων

3.1.4 Ταξινόμηση Σωρού (Heapsort)


Oι αλγόριθμοι ταξινόμησης που είδαμε ως τώρα έχουν πολυπλοκότητα Ο(n2) στη
χειρότερη περίπτωση. Στην παράγραφο αυτή θα δούμε έναν αλγόριθμο που σπάει αυτό το
φράγμα κι έχει πολυπλοκότητα Ο(nlogn). Oνομάζεται Heapsort (ταξινόμηση σωρού),
ανακαλύφθηκε από τον Williams το 1964 και χρησιμοποιεί την τεχνική της ταξινόμησης με
επιλογή αλλά με πιο έξυπνο τρόπο απ΄ ότι ο προηγούμενος αλγόριθμος.
Ο Ηeapsort κάνει χρήση μιας ειδικής δομής δεδομένων που καλείται δυαδικός σωρός
(binary heap) και ανήκει στη γενική κατηγορία των μη γραμμικών δεδομένων που καλούνται
ουρές προτεραιότητας (priority queues). Οι συγκεκριμένες δομές είναι βασικές στο
σχεδιασμό αλγορίθμων, γι΄ αυτό και στη συνέχεια κρίνουμε σκόπιμο να αναφερθούμε
συνοπτικά στις πράξεις που υποστηρίζουν.

Ουρές προτεραιότητας
Οι ουρές προτεραιότητας χρησιμοποιούνται για τη διαχείριση μη διατεταγμένων συνόλων
από στοιχεία. Υποστηρίζουν τις ακόλουθες βασικές πράξεις:
o Ιnsert(x): εισάγει το στοιχείο με την τιμή x στην ουρά,
o Find_max(): επιστρέφει το στοιχείο με τη μεγαλύτερη τιμή,
o Delete_max(): διαγράφει και επιστρέφει στοιχείο με τη μεγαλύτερη τιμή,
o Ιncrease(x, k): αλλάζει την τιμή του στοιχείου x με τη νέα τιμή k > x,
o Delete(x): διαγράφει το στοιχείο με τιμή x.

Για τις τελευταίες δύο πράξεις θεωρούμε ότι η θέση του στοιχείου με την τιμή x είναι γνωστή
πχ. μέσω ενός δείκτη που αποτελεί μέρος της εισόδου, απαίτηση που δεν δημιουργεί
πρόβλημα σε πολλές εφαρμογές.
Στη βιβλιογραφία έχουν καταγραφεί αρκετές υλοποιήσεις των ουρών προτεραιότητας.
Κάποιες εξασφαλίζουν ότι όλες οι παραπάνω πράξεις σε μια ουρά n στοιχείων εκτελούνται
στη χειρότερη περίπτωση σε χρόνο Ο(logn). Άλλες πιο έξυπνες υλοποιήσεις υποστηρίζουν τις
πράξεις Delete_max() και Delete() σε κατανεμημένο χρόνο Ο(logn) και όλες τις υπόλοιπες σε
χρόνο Ο(1).
Μια βασική εφαρμογή των ουρών προτεραιότητας είναι η δρομολόγηση εργασιών (job
scheduling) σε κάποιον εξυπηρετητή. Η ουρά αποθηκεύει τις εργασίες που πρέπει να
εκτελεστούν και τις σχετικές τους προτεραιότητες οι οποίες καθορίζονται εφαρμόζοντας
κάποιο συγκεκριμένο κανόνα (πχ. μία εργασία που πρέπει να ολοκληρωθεί πιο σύντομα από
τις υπόλοιπες έχει μεγαλύτερη προτεραιότητα). Όταν μια εργασία ολοκληρώνεται ή
διακόπτεται, επιλέγεται για ανάθεση αυτή με τη μεγαλύτερη προτεραιότητα χρησιμοποιώντας
τη Delete_max(). Μια νέα εργασία μπορεί να προστεθεί στην ουρά οποιαδήποτε στιγμή
εκτελώντας την Insert(). Οι προτεραιότητες μπορούν να αλλάζουν δυναμικά (πχ. για εργασίες
με μεγάλο χρόνο αναμονής) με την Ιncrease() ενώ η ακύρωση μιας εργασίας που δεν έχει
εξυπηρετηθεί ακόμα γίνεται με την Delete().

Ο δυαδικός σωρός
Ο δυαδικός σωρός μπορεί να παρασταθεί με ένα δυαδικό ισοζυγισμένο δένδρο από
στοιχεία όπου η τιμή κάθε στοιχείου ικανοποιεί την ιδιότητα του σωρού (heap property)
όπως στο σχήμα που ακολουθεί:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 73


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 20: Ένας δυαδικός σωρός 10 στοιχείων

Στο παραπάνω δένδρο 10 στοιχείων οι κύκλοι περιέχουν πληροφορία και ονομάζονται


κόμβοι (nodes) (είναι τα στοιχεία που αποθηκεύει το δένδρο) ενώ οι γραμμές παριστάνουν
διακλαδώσεις και ονομάζονται ακμές (edges). O κόμβος στην κορυφή καλείται ρίζα (root) του
δένδρου. Οι κόμβοι που βρίσκονται ακριβώς κάτω από έναν κόμβο («κρέμονται» από τον
κόμβο) ονομάζονται παιδιά του κόμβου ενώ αυτός είναι ο πατέρας τους. Παιδιά του ίδιου
πατέρα ονομάζονται αδέλφια. Υπόδενδρο ονομάζεται το δένδρο που σχηματίζεται, αν ως ρίζα
ληφθεί ένας οποιασδήποτε κόμβος τους. Οι κόμβοι ιεραρχούνται κατά επίπεδα από πάνω
προς τα κάτω: η ρίζα ανήκει στο επίπεδο 1 ή αλλιώς λέμε ότι έχει βάθος 1, οι κόμβοι-παιδιά
της στο επίπεδο 2 κ.ο.κ. Οι κόμβοι που δεν έχουν παιδιά ονομάζονται φύλλα του δένδρου. Η
διαδρομή από τη κόμβο προς ένα φύλλο ονομάζεται μονοπάτι. Το δένδρο είναι δυαδικό και
ισοζυγισμένο επειδή κάθε κόμβος με εξαίρεση τα φύλλα και πιθανά τον τελευταίο (κάτω δεξιά)
κόμβο έχουν δύο παιδιά. Αν το τελευταίο επίπεδο του δένδρου είναι το k, τότε, όπως φαίνεται
στο σχήμα, όλα τα φύλλα ανήκουν στο επίπεδο k-1 ή k και είναι στοιχισμένα προς τα
αριστερά. Τέλος, για ένα δυαδικό ισοζυγισμένο δέντρο n στοιχείων όπως περιγράφεται
παραπάνω, το μήκος του μακρύτερου μονοπατιού του ή αλλιώς το ύψος του δένδρου είναι ίσο
με ⎡log(n + 1)⎤ -1. Δείτε ότι το ύψος του δένδρου ισούται με το πλήθος των επιπέδων – 1.

Η ιδιότητα σωρού τώρα διατυπώνεται ως εξής:


1. Κάθε κόμβος u του δένδρου περιέχει μία τιμή από ένα διατεταγμένο σύνολο,
2. Ισχύει η συνθήκη: τιμή(πατέρα(u)) ≥ τιμή(u)
Η ιδιότητα σωρού καθορίζει μια μερική διάταξη στα στοιχεία του: η μεγαλύτερη τιμή είναι
αποθηκευμένη στη ρίζα του δένδρου και τα στοιχεία που αποθηκεύονται στους κόμβους κάθε
μονοπατιού από τη ρίζα προς ένα φύλλο είναι διατεταγμένα κατά φθίνουσα σειρά.
Είναι εύκολο να δείτε ότι σε ένα δυαδικό σωρό που υλοποιείται με τον παραπάνω τρόπο
η πράξη Find_max() επιστρέφει σε Ο(1) χρόνο το στοιχείο που βρίσκεται στη ρίζα του
δένδρου.
Για την πράξη Ιnsert(x) πρώτα προσθέτουμε στο τέλος της ουράς (μία θέση δεξιά από το
δεξιότερο φύλλο του τελευταίου επιπέδου) του δένδρου ένα καινούργιο φύλλο με την τιμή x
και στη συνέχεια ανεβαίνουμε από το φύλλο αυτό προς τη ρίζα του δένδρου ψάχνοντας να
βρούμε τη σωστή θέση όπου θα βάλουμε το x αλλάζοντας ταυτόχρονα θέση σε άλλα στοιχεία
ώστε να ικανοποιείται η ιδιότητα σωρού. H ιδέα είναι να μεταφέρουμε το x από το παιδί στον

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 74


Αλγόριθμοι και Δομές Δεδομένων

πατέρα αν αυτό είναι μεγαλύτερο από την τιμή του πατέρα αποθηκεύοντας ταυτόχρονα την
τιμή του πατέρα στο παιδί.
Για την πράξη Delete_max() πρώτα μεταφέρουμε την τιμή του τελευταίου φύλλου στη ρίζα του
δένδρου και στη συνέχεια κατεβαίνουμε το μονοπάτι από τη ρίζα μεταφέροντας στον πατέρα
τη μεγαλύτερη από τις τιμές των δύο παιδιών του μέχρι όλες οι τιμές να μπουν στη σωστή τους
θέση ή να φτάσουμε σε κάποιο φύλλο. Στη συνέχεια θα δούμε ότι την τεχνική αυτή ακριβώς
χρησιμοποιεί και ο αλγόριθμος Heapsort για να ταξινομήσει τα στοιχεία ενός πίνακα.
Και η πράξη Delete(x) εκτελείται παρόμοια με την Delete_max() μόνο που τώρα μεταφέρουμε
την τιμή του τελευταίου φύλλου στον κόμβο v με την τιμή x, σβήνουμε το τελευταίο φύλλο και
ξεκινούμε την κάθοδο στο δένδρο από τον κόμβο v. Aν η πρόσβαση στο τέλος του σωρού
καθώς και στον κόμβο v με την τιμή που επιθυμούμε να σβήσουμε μπορεί να γίνει σε Ο(1)
χρόνο, τότε ο χρόνος εκτέλεσης των προηγούμενων πράξεων είναι Ο(ύψος δένδρου) και άρα
για ένα σωρό n στοιχείων Ο(logn). Τέλος, η πράξη Ιncrease(x, k) αντιστοιχεί σε μια Delete(x)
ακολουθούμενη από μια Ιnsert(k), άρα και αυτή παίρνει χρόνο Ο(logn).

Για τις ανάγκες του Heapsort o δυαδικός ισοζυγισμένος σωρός υλοποιείται με έναν
πίνακα S. Για το σωρό του προηγούμενου σχήματος ο πίνακας αυτός είναι ο εξής:

Δηλ. το στοιχείο της πρώτης θέσης του πίνακα S[1] είναι το μεγαλύτερο και αντιστοιχεί στη
ρίζα του δένδρου. Τα παιδιά του κόμβου S[i] του δένδρου βρίσκονται στις θέσεις S[2i]
(αριστερό παιδί) και S[2i+1] (δεξί παιδί) του πίνακα, ενώ ο κόμβος S[i] έχει πατέρα τον
S[ ⎣i / 2⎦ ].

Ο αλγόριθμος Heapsort
Έστω τώρα ότι θέλουμε να ταξινομήσουμε τον πίνακα S n ακέραιων στοιχείων. Ο
αλγόριθμος Ηeapsort εκτελείται σε δύο φάσεις:
Φάση Δόμησης: Ο πίνακας S[1..n] με τα μη ταξινομημένα στοιχεία μετατρέπεται σε σωρό.
Φάση Διαλογής: To μεγαλύτερο στοιχείο του πίνακα, που βρίσκεται στη ρίζα του σωρού
(στοιχείο S[1]), αλλάζει αμοιβαία θέση με το δεξιότερο στοιχείο του πίνακα
(στοιχείο S[n]). Στη συνέχεια επαναλαμβάνεται η φάση δόμησης και πάλι η
φάση διαλογής για το δεύτερο μεγαλύτερο στοιχείο κ.ο.κ. μέχρι να
φτάσουμε στο τελευταίο στοιχείο.

Στη φάση δόμησης χτίζουμε σωρούς σε διαφορετικά υπόδενδρα κάθε φορά. Ξεκινούμε
από το στοιχείο στη θέση ⎣n / 2⎦ , δηλ. το S[ ⎣n / 2⎦ ] και προχωράμε στα στοιχεία S[ ⎣n / 2⎦ -1],
S[ ⎣n / 2⎦ – 2], … μέχρι να φτάσουμε στη ρίζα S[1] του δένδρου. Σε κάθε υποδένδρο με ρίζα
S[k] χτίζουμε το σωρό για τα στοιχεία S[k..n] του πίνακα.
Στη φάση διαλογής αρχικά αντιμεταθέτουμε το S[1] με το S[n] ώστε το μεγαλύτερο
στοιχείο του πίνακα να μπει στην τελευταία θέση που είναι και η σωστή. Στη συνέχεια
ξαναχτίζουμε το σωρό αλλά για τα n-1 στοιχεία S[1..n-1]. Προχωρούμε με αυτόν τον τρόπο

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 75


Αλγόριθμοι και Δομές Δεδομένων

αυτό μέχρι να μείνουν 2 στοιχεία, το μικρότερο και το δεύτερο μικρότερο στοιχείο του πίνακα.
Βάζουμε το δεύτερο μικρότερο στοιχείο στη σωστή του θέση και ο αλγόριθμος τερματίζει.
Μπορούμε τώρα να δώσουμε τον αλγόριθμο σε μορφή ψευδοκώδικα:

Aλγόριθμος Heapsort
Είσοδος (S, n)
Δεδομένα // i: temp: ακέραιοι //
Aρχή
/* Φάση Δόμησης */
Για i = n/2 Μέχρι 1 με Βήμα -1
1.1 <Mετέτρεψε τον υποπίνακα S[i..n] σε έναν σωρό>
Τέλος Επανάληψης

/* Φάση Διαλογής */
Για i = n Μέχρι 2 με Βήμα -1
/* Το S[1] μπαίνει στη σωστή του θέση */
temp = S[1]
S[1] = S[i]
S[i] = temp
1.2 <Μετέτρεψε τον υποπίνακα S[1..i-1] σε έναν σωρό>
/* Στο σημείο αυτό ο υποπίνακας S[1..i-1] είναι ένας σωρός και ο S[i..n]
περιέχει τα n-i+1 μεγαλύτερα στοιχεία του αρχικού πίνακα ταξινομημένα */
Τέλος Επανάληψης
Αποτελέσματα // Tαξινομημένος πίνακας S[1..n] //
Τέλος Ηeapsort

Στον προηγούμενο ψευδοκώδικα το βασικό ζητούμενο είναι να βρούμε πώς ο πίνακας


S[root..last] μετατρέπεται σε σωρό (εντολές 1.1 και 1.2). Αυτό γίνεται με τη «βύθιση» του
στοιχείου S[root] (που αντιστοιχεί στη ρίζα του υπόδενδρου με στοιχεία τα S[root], S[root+1],
…, S[last]) προς τα κάτω κατά μήκος ενός μονοπατιού μέχρι να μπει στη σωστή του θέση και
να ικανοποιείται η ιδιότητα του σωρού. Αυτό, όπως έχουμε δει ήδη, γίνεται συγκρίνοντας κάθε
φορά το στοιχείο S[root] που βυθίζεται με το μεγαλύτερο από τα παιδιά του: όταν το S[root]
είναι μεγαλύτερο τότε ο σωρός έχει δημιουργηθεί και η βύθιση σταματά, διαφορετικά το
μεγαλύτερο παιδί ανεβαίνει στη θέση του πατέρα του και το S[root] κατεβαίνει στη θέση του
παιδιού.
Aς υποθέσουμε ότι δουλεύουμε στον πίνακα του προηγούμενου σχήματος και έστω ότι
κατά τη φάση διαλογής απομακρύνθηκε το μεγαλύτερο στοιχείο (το 14 που βρίσκονταν στη
ρίζα του σωρού), μπήκε στη θέση του το 2 (που βρίσκονταν στην τελευταία θέση του πίνακα)
και ο σωρός πρέπει να επαναδομηθεί. Το δένδρο θα έχει την εξής μορφή:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 76


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 21: Το δένδρο μετά την απομάκρυνση του μεγαλύτερου στοιχείου

Η διαδικασία βύθισης απεικονίζεται παρακάτω:

Σχήμα 22: Διαδικασία βύθισης του στοιχείου 2 και επανακατασκευή του σωρού

Καλούμε τον αλγόριθμο βύθισης Shift_down. Σύμφωνα με τα όσα είπαμε παραπάνω ο


ψευδοκώδικας για τον Shift_down θα είναι ο εξής:

Aλγόριθμος Shift_down
Είσοδος (S, root, last)
Δεδομένα // j, k, v: ακέραιοι //
Aρχή
v = S[root]
k = root
Όσο (k <= last/2) Εκτέλεσε /* Δεν έχουμε φτάσει σε φύλλο */

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 77


Αλγόριθμοι και Δομές Δεδομένων

j = 2*k /* Στις θέσεις j και j+1 αποθηκεύονται το αριστερό και δεξί


παιδί του S[k] αντίστοιχα */
Αν (j < last) Τότε
Αν (S[j+1]>S[j]) Τότε j = j+1 Τέλος Αν
Τέλος Αν
Αν (v>= S[j]) Τότε τερμάτισε την επανάληψη Τέλος Αν /* Σ’ αυτό το σημείο η
βύθιση τερματίζεται */
S[k] = S[j]
k=j
Tέλος Όσο
S[k] = v /* η ρίζα S[root] του δένδρου μπαίνει στη σωστή της θέση k */
Aποτελέσματα // Σωρός S[root..last] //
Τέλος Shift_down

Τώρα μπορούμε να αντικαταστήσουμε τις εντολές 1.1 και 1.2 στο βασικό αλγόριθμο
Heapsort με τις Shift_down(i, n) και Shift_down(1, i-1) αντίστοιχα.

Μπορεί να αποδειχθεί σχετικά εύκολα ότι η φάση δόμησης απαιτεί συνολικό χρόνο O(n).
Η διαίσθηση είναι ότι η φάση δόμησης περιλαμβάνει πολλά υπόδενδρα που έχουν μικρό
μέγεθος κι έτσι προκύπτει γραμμικός χρόνος. Πράγματι, ο χρόνος αυτός είναι ανάλογος της
ποσότητας:

Ισχύει τώρα:

Η φάση διαλογής απαιτεί χρόνο O(nlogn) αφού με κάθε απομάκρυνση της ρίζας ξαναχτίζουμε
σε O(logn) χρόνο το σωρό. Επομένως, ο συνολικός χρόνος εκτέλεσης του αλγόριθμου είναι
O(nlogn) στη χειρότερη περίπτωση για την ταξινόμηση n στοιχείων.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 78


Αλγόριθμοι και Δομές Δεδομένων

3.1.5 Γρήγορη Ταξινόμηση (Quicksort)


O αλγόριθμος γρήγορης ταξινόμησης ή ταχυταξινόμησης (quicksort) αναπτύχθηκε από τον
Hoare το 1962. Eφαρμόζει την τεχνική Διαίρει και Βασίλευε (Divide & Conquer) η οποία
βασίζεται στην λογική της κατάτμησης του προβλήματος σε μικρότερα ομοειδή προβλήματα
έτσι ώστε ο συνδυασμός των μερικών λύσεων να δώσει την συνολική λύση του προβλήματος.
Πιο συγκεκριμένα η τεχνική αυτή περιλαμβάνει τα παρακάτω 3 βήματα:
Διαίρει (divide): Το αρχικό πρόβλημα διαιρείται σε συγκεκριμένο πλήθος (συνήθως
δύο) ομοειδών υποπροβλημάτων ή αλλιώς στιγμιοτύπων.
Βασίλευε (conquer): Κάθε στιγμιότυπο του προβλήματος επιλύεται ανεξάρτητα με
αναδρομικό τρόπο.
Συνδύασε (combine): Όλες οι λύσεις από κάθε στιγμιότυπο συνδυάζονται εντός
πολυωνυμικού χρόνου για την επίλυση του συνολικού προβλήματος.
Για την ακρίβεια η τεχνική Διαίρει και Βασίλευε εφαρμόζεται στον quicksort χωρίς την
εκτέλεση του βήματος συνδυασμού: πρώτα επεξεργαζόμαστε τον πίνακα και στη συνέχεια τον
διαχωρίζουμε σε δύο υποπίνακες έτσι ώστε όλα τα στοιχεία του αριστερού υποπίνακα να
είναι μικρότερα ή ίσα όλων των στοιχείων του δεξιού. Στη συνέχεια ταξινομούμε με τον ίδιο
τρόπο αναδρομικά τους δύο υποπίνακες μέχρι να φτάσουμε σε υποπίνακες με ένα μόνο
στoιχείο.
Ο αλγόριθμος quicksort έχει πολλές παραλλαγές. Στη συνέχεια περιγράφεται η πιο
συνηθισμένη.
Έστω ότι l και r είναι το αριστερό και δεξί όριο του πίνακα αντίστοιχα (για τον αρχικό
πίνακα ισχύει l =1 και r = n). Επιλέγουμε ως στοιχείο διαχωρισμού x το αριστερότερο στοιχείο
του πίνακα. Ορίζουμε δύο δείκτες i και j όπου ο i διατρέχει τον πίνακα από τα αριστερά και
σταματά όταν S[i]≥S[1]. Όμοια ο j διατρέχει τον πίνακα από τα δεξιά και σταματά όταν
S[j]≤S[1]. Τότε αντιμεταθέτουμε τα στοιχεία μεταξύ τους. Συνεχίζουμε την σάρωση μέχρι το j
να γίνει ίσο ή μεγαλύτερο του i. Τότε αρκεί να γίνει εναλλαγή του S[j] με το S[1] ώστε να
διαχωριστεί ο πίνακας σε δύο υποπίνακες αριστερά και δεξιά από το στοιχείο S[1]. Αριστερά
θα περιέχονται μόνο ίσοι ή μικρότεροί του αριθμοί ενώ δεξιά ίσοι ή μεγαλύτεροί του όπως
απεικονίζεται σχηματικά παρακάτω:

Σχήμα 23: Λειτουργία του βήματος διαχωρισμού του quicksort

Καλούμε τη διαδικασία διαχωρισμού partition. Παίρνει ως είσοδο τον υποπίνακα S[l..r] και
επιστρέφει τη θέση j του πίνακα όπου γίνεται ο διαχωρισμός. Ο αλγόριθμος της partition σε
μορφή ψευδοκώδικα είναι ο εξής:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 79


Αλγόριθμοι και Δομές Δεδομένων

Aλγόριθμος Partition
Είσοδος (S, l, r)
Δεδομένα // i, j, x, temp: ακέραιοι //
Aρχή
x = S[l]
i=l
j = r+1
Όσο (i< j) Εκτέλεσε
Εκτέλεσε i = i+1 όσο (S[i] < x)
Εκτέλεσε j = j-1 όσο (S[j] > x)
Αν (i<j) Τότε
temp = S[i]
S[i] = S[j]
S[j] = temp
Tέλος Αν
Τέλος Όσο
/* Στο σημείο αυτό οι δείκτες έχουν διασταυρωθεί και το στοιχείο διαχωρισμού πρέπει
να μπει στη σωστή του θέση */
S[l] = S[j]
S[j] = x
Aποτελέσματα // Δείκτης j //
Τέλος Partition

Tώρα μπορούμε να δώσουμε τον ψευδοκώδικα για τον αλγόριθμο quicksort o oποίος είναι
εξαιρετικά απλός λόγω της αναδρομής.

Aλγόριθμος Quicksort
Είσοδος (S, l, r)
Δεδομένα // l, r, k: ακέραιοι //
Aρχή
Αν (l < r) Τότε
/* Βήμα Διαμέρισης */
k = θέση του στοιχείου διαχωρισμού που επιστρέφει η Partition με είσοδο τον
υποπίνακα S[l ..r]
/* Aναδρομικό βήμα */
Κάλεσε αναδρομικά τον Quicksort με είσοδο τον υποπίνακα S[l..k-1]

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 80


Αλγόριθμοι και Δομές Δεδομένων

Κάλεσε αναδρομικά τον Quicksort με είσοδο τον υποπίνακα S[k+1, r]


Τέλος Αν
Αποτελέσματα // Tαξινομημένος πίνακας S[l..r] //
Τέλος Quicksort

Για την ταξινόμηση του S[1..n] η αρχική κλήση του quicksort γίνεται με παραμέτρους 1 και n
δηλ. έχει τη μορφή Quicksort(S, 1, n).

Παρατήρηση:
Αν το στοιχείο S[1] είναι το μεγαλύτερο στοιχείο του πίνακα και είναι μοναδικό, τότε κατά την
υλοποίηση του αλγόριθμου της Partition ο αριστερός δείκτης i θα πάρει διαδοχικά όλες τις
τιμές 1, 2, 3, …n, n+1 δίχως να σταματήσει και θα προκαλέσει ένα run time λάθος, αφού το
στοιχείο S[n+1] δεν υπάρχει. Το πρόβλημα μπορεί να λυθεί με έναν επιπλέον έλεγχο (μια Αν
… Τότε … εντολή) ή χρησιμοποιώντας μια επιπλέον θέση στον πίνακα που έχει το ρόλο του
«φρουρού». Στη θέση αυτή αποθηκεύουμε μια τιμή τέτοια ώστε να υπάρχει η εγγύηση ότι το i
δεν θα βγει έξω από τα όρια του πίνακα. Η μεγαλύτερη τιμή του πίνακα (τη βρίσκουμε με n-1
συγκρίσεις) ή ο μεγαλύτερος ακέραιος στο σύστημα που δουλεύουμε είναι κατάλληλες για το
σκοπό αυτό. Η λύση του φρουρού είναι και η πιο πρακτική και δεν αλλάζει τον αλγόριθμο της
Partition, αρκεί να θυμάται ο χρήστης ότι για να ταξινομήσει n στοιχεία σ΄ έναν πίνακα με τον
quicksort, ο πίνακας πρέπει να έχει μέγεθος n+1, με το τελευταίο στοιχείο να είναι ο φρουρός.
Παρατηρήστε ότι για το δεξιό δείκτη j δεν χρειαζόμαστε αντίστοιχο φρουρό αφού, μόλις ο j
φτάσει στο αριστερό όριο του πίνακα, ο έλεγχος S[j] > x για j = l αποτυγχάνει και ο δείκτης
σταματά.

Στη χειρότερη περίπτωση ο αριθμός των συγκρίσεων που εκτελεί η συγκεκριμένη


παραλλαγή του Quicksort είναι O(n2) γιατί μπορεί να έχουμε Ο(n) ενεργές αναδρομικές
κλήσεις (διαφορετικά περιβάλλοντα που είναι αποθηκευμένα την ίδια στιγμή στη στοίβα του
συστήματος). Πράγματι, αν τα στοιχεία του πίνακα είναι όλα διαφορετικά και ήδη
ταξινομημένα, το στοιχείο διαχωρισμού είναι ‘κακό’ και δημιουργεί πάντα έναν μόνο
υποπίνακα με μέγεθος κατά 1 μικρότερο από τον αρχικό για τον οποίο εκτελείται το
αναδρομικό βήμα. Έτσι, αν T(n) είναι το πλήθος των συγκρίσεων χειρότερης περίπτωσης για
είσοδο μεγέθους n, τότε η Partition κάνει το πολύ n+1 συγκρίσεις όταν ο δείκτης i προσπερνά
το δείκτη j και ισχύει:
Τ(n) = n+1 + Τ(n-1) = (n+1)(n+2)/2 – 3 = O(n2)
με αρχικές συνθήκες T(0) = T(1) = 0.
Για την ανάλυση της μέσης περίπτωσης υποθέτουμε ότι τα στοιχεία του πίνακα είναι όλα
διαφορετικά μεταξύ τους και κάθε μια από τις n! δυνατές αντιμεταθέσεις των στοιχείων είναι
ισοπίθανη ως είσοδος. Αυτό συνεπάγεται ότι οποιοδήποτε από τα n στοιχεία του πίνακα έχει
την ίδια πιθανότητα 1/n να είναι στοιχείο διαχωρισμού και οι ίδιες υποθέσεις ισχύουν για τους
υποπίνακες μεγέθους < n.
__
Επομένως, αν T (n) είναι το μέσο πλήθος συγκρίσεων για εισόδους μεγέθους n, τότε ισχύει:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 81


Αλγόριθμοι και Δομές Δεδομένων


__
T (n) = n + 1 + 1/n * [Τ(k - 1) + Τ(n − k)] = Ο(nlogn), Τ(0) = Τ(1) = 0
k=1
δηλ. η πολυπλοκότητα μέσης περίπτωσης του quicksort είναι O(nlogn). H O(nlogn)
πολυπλοκότητα ερμηνεύεται φυσικά από το γεγονός ότι για τυχαίες εισόδους το στοιχείο
διαχωρισμού κόβει τον πίνακα κάθε φορά περίπου στη μέση που είναι και το καλύτερο που
θα μπορούσαμε να περιμένουμε (στην περίπτωση αυτή Τ(n) = n+1 + 2T(n/2) που έχει λύση
Ο(nlogn)).
Εξαιτίας των Ο(n) αναδρομικών κλήσεων στη χειρότερη περίπτωση, για μεγάλα n η
στοίβα του συστήματος μπορεί να υπερχειλίσει. Μία λύση στο πρόβλημα αυτό είναι στο
αναδρομικό βήμα καλούμε τον quicksort μόνο για τον μικρότερο υποπίνακα και ταξινομούμε
τον μεγαλύτερο επαναληπτικά. Έτσι έχουμε μόνο Ο(logn) ενεργές αναδρομικές κλήσεις, αφού
κάθε νέο περιβάλλον που αποθηκεύεται στη στοίβα αντιπροσωπεύει ένα υποπίνακα με
μέγεθος ≤ μισό του υποπίνακα για το προηγούμενο περιβάλλον.
Παρακάτω δίνεται ο αλγόριθμος για την παραλλαγή αυτή:

Aλγόριθμος Quicksort_stack_limited
Είσοδος (S, l, r)
Δεδομένα // k: ακέραιoς //
Αρχή
Όσο (l < r) Εκτέλεσε
k = θέση του στοιχείου διαχωρισμού που επιστρέφει η Partition με είσοδο τον
υποπίνακα S[l ..r]
Αν (k-l < r-k) Τότε
<Κάλεσε αναδρομικά τον αλγόριθμο για τον S[l..k-1]>
l=k+1 /* ενημέρωση αριστερού ορίου του πίνακα για το */
/* επαναληπτικό βήμα */
Αλλιώς
<Κάλεσε αναδρομικά τον αλγόριθμο για τον S[k+1..r]>
r=k-1 /* ενημέρωση δεξιού ορίου του πίνακα */
Tέλος Αν
Τέλος Όσο
Αποτελέσματα // Ταξινομημένος πίνακας S[l..r] //
Tέλος Quicksort_stack_limited

Αν θέλουμε να εξαλείψουμε κάθε πρόβλημα με τη μνήμη του συστήματος, μπορούμε να


προσομοιώσουμε την αναδρομή με μια εξωτερική στοίβα, ώστε η δέσμευση χώρου γίνεται
στατικά από την αρχή. Η υλοποίηση της παραλλαγής αυτής, η οποία αφήνεται ως άσκηση,
κάνει επίσης τον quicksort κατάλληλο για εκτέλεση σε μια γλώσσα προγραμματισμού που δεν
υποστηρίζει αναδρομή.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 82


Αλγόριθμοι και Δομές Δεδομένων

Μια άλλη βελτίωση που μπορεί να γίνει στον quicksort είναι να τερματίζουμε την αναδρομή
όταν φτάσουμε σε υποπίνακες μεγέθους < Μ για κάποια σταθερά Μ < n και να ταξινομούμε
τους υποπίνακες αυτούς με έναν επαναληπτικό αλγόριθμο όπως για παράδειγμα τον
selectionsort. Η τιμή της Μ εξαρτάται από τι λεπτομέρειες της υλοποίησης, όμως δεν απαιτείται
να είναι η καλύτερη δυνατή: ο αλγόριθμος λειτουργεί εξίσου καλά για τις τιμές της Μ στην
περιοχή από 5 έως 25, η δε μείωση στο χρόνο εκτέλεσης είναι της τάξης του 20% όπως
αποδεικνύουν σχετικές μετρήσεις που έχουν γίνει.

Για να βελτιώσουμε τώρα την κακή συμπεριφορά του αλγόριθμου για ταξινομημένες ή περίπου
ταξινομημένες εισόδους, μπορούμε να επιλέγουμε τυχαία ένα στοιχείο του πίνακα ως
στοιχείο διαχωρισμού, χρησιμοποιώντας για παράδειγμα μία γεννήτρια ψευδοτυχαίων. Με
την αλλαγή αυτή η χειρότερη περίπτωση πλέον συμβαίνει με πολύ μικρή πιθανότητα και δεν
υπάρχουν πια κακές είσοδοι: ο τυχαίος quicksort συμπεριφέρεται το ίδιο σε όλες τις εισόδους.
Η παραλλαγή αυτή είναι ένα απλοϊκό παράδειγμα πιθανοτικού (probabilistic) αλγόριθμου,
όπου η τυχαιότητα χρησιμοποιείται για να έχουμε σχεδόν πάντα καλή απόδοση. Γενικά η
τυχαιότητα αποτελεί μια ισχυρή τεχνική για τον αποδοτικό σχεδιασμό αλγορίθμων , ειδικά
όταν υπάρχει από πριν κάποια γνώση για την κατανομή των εισόδων.
Μεγαλύτερη βελτίωση προκύπτει αν πάρουμε 3 στοιχεία από τον πίνακα, πχ. το αριστερότερο
S[l], το δεξιότερο S[r], και το S[(l+r)/2] που βρίσκεται στη μέση και χρησιμοποιήσουμε το
μεσαίο σε μέγεθος από τα τρία ως στοιχείο διαχωρισμού. Η μέθοδος αυτή είναι γνωστή ως
μέσος-από-τρεις μέθοδος διαχωρισμού. Είναι εύκολο να δείτε ότι με την παραλλαγή αυτή
αποφεύγουμε τη χρήση του στοιχείου φρουρού.
Κλείνοντας την παρουσίαση του quicksort επισημαίνουμε ότι είναι δυνατές κι άλλες
αλγοριθμικές βελτιώσεις, (πχ. θα μπορούσε να χρησιμοποιηθεί ο μεσαίος από 5 στοιχεία)
αλλά το κέρδος πλέον είναι οριακό. Αξίζει δεν αναφερθεί ότι σε σύγκριση δε με τον αλγόριθμο
heapsort που έχει πολυπλοκότητα χειρότερης περίπτωσης Ο(nlogn), o quicksort στην πράξη,
αν υλοποιηθεί σωστά, είναι περίπου 2 με 3 φορές γρηγορότερος επειδή οι πράξεις της
Partition() είναι απλούστερες!

3.1.6 Ταξινόμηση με Συγχώνευση (Mergesort)


Ένας αλγόριθμος με πολύ καλή απόδοση είναι η ταξινόμηση με συγχώνευση. Η κεντρική
ιδέα του αλγόριθμου συνοψίζεται στην εκτέλεση 2 βασικών βημάτων:
Βήμα 1 (Αναδρομή): Χώρισε το σύνολο των στοιχείων εισόδου σε δύο τμήματα ίσου
μεγέθους και ταξινόμησε τα στοιχεία των δύο τμημάτων ανεξάρτητα
μεταξύ τους.
Βήμα 2 (Συγχώνευση): Παρήγαγε το τελικό σύνολο ταξινομημένων στοιχείων από τα δύο
διακριτά τμήματα των οποίων τα στοιχεία έχουν ταξινομηθεί στο
βήμα 1.
Ο αλγόριθμος ταξινόμησης με συγχώνευση είναι ένας αναδρομικός αλγόριθμος και
αποτελεί το πλέον κλασικό παράδειγμα εφαρμογής της τεχνικής Διαίρει και Βασίλευε. Οι
αναδρομικές κλήσεις σταματούν όταν ο διαχωρισμός του βήματος 1 δώσει υποπίνακες
μεγέθους 1 οπότε θεωρούνται ταξινομημένοι και αρχίζει αναδρομικά η διαδικασία της
συγχώνευσης.
Η λειτουργία του αλγόριθμου για τον πίνακα S = [15, 4, 9, 12, 21, 3, 27, 6] αποτυπώνεται
σχηματικά παρακάτω. Στην αριστερή πλευρά του σχήματος φαίνεται η διαδικασία

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 83


Αλγόριθμοι και Δομές Δεδομένων

διαχωρισμού του πίνακα και στη δεξιά η διαδικασία συγχώνευσης που παράγει
ταξινομημένους υποπίνακες.

Σχήμα 24: Λειτουργία του αλγόριθμου mergesort

Στη συνέχεια παρουσιάζεται ο αλγόριθμος ταξινόμησης με συγχώνευση για τον πίνακα


S[l..r] (o αλγόριθμος καλείται αρχικά με παραμέτρους l = 1 και r = n):

Aλγόριθμος Mergesort
Είσοδος (S, l, r)
Δεδομένα // m: ακέραιος //
Aρχή
Aν (l < r) Τότε
m=(r-l+1)/2
/* Αναδρομικό βήμα */
<Kάλεσε τον Μergesort με είσοδο τον υποπίνακα S[l..m]>
<Kάλεσε τον Μergesort με είσοδο τον υποπίνακα S[m+1..r]>

/* Βήμα συγχώνευσης */
<Kάλεσε τον αλγόριθμο συγχώνευσης Merge των δύο υποπινάκων>
Tέλος Αν
Aποτελέσματα // Tαξινομημένος πίνακας S[l..r] //
Τέλος Mergesort

Αυτό που πρέπει να περιγράψουμε ακόμη είναι ο αλγόριθμος συγχώνευσης merge. O


merge χρησιμοποιεί έναν δεύτερο βοηθητικό πίνακα C για την αντιγραφή των στοιχείων των
δύο υποπινάκων με τη σωστή (ταξινομημένη) σειρά (επειδή ο αλγόριθμος mergesort για n
στοιχεία χρησιμοποιεί 2n θέσεις μνήμης, λέμε ότι δεν είναι “in-place” αλγόριθμος). Στο τέλος,
τα ταξινομημένα στοιχεία του C ξαναγράφονται πάλι στον S. Για τη συγχώνευση σε κάθε βήμα

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 84


Αλγόριθμοι και Δομές Δεδομένων

συγκρίνονται ανά δύο τα στοιχεία των υποπινάκων και ανάλογα με τη σειρά που έχουν
γράφονται στον C.
O αλγόριθμος συγχώνευσης με τη μορφή ψευδοκώδικα δίνεται παρακάτω:

Aλγόριθμος Merge
Είσοδος (S, l1, r1, l2, r2) /* Tαξινομημένοι υποπίνακες S[l1..r1] και S[l2..r2] */
Δεδομένα // C, i, j, k: ακέραιοι // /* Ο C χρησιμοποιείται ως βοηθητικός πίνακας */
Aρχή
i = l1; j = l2; k = 1
Όσο (i<= r1) AND (j<=r2) Εκτέλεσε
Aν (S[i] < S[j]) Τότε
C[k] = S[i]; k=k+1; i=i+1
Aλλιώς
C[k] = S[j]; k=k+1; j=j+1
Tέλος Αν
Τέλος Όσο
Όσο (i <= r1) Εκτέλεσε
C[k] = S[i]; k=k+1; i=i+1
Τέλος Όσο
Όσο (j <= r2) Εκτέλεσε
C[k] = S[j]; k=k+1; j=j+1
Τέλος Όσο
k = 1;
Για i=l1 Μέχρι r1 Εκτέλεσε
S[i] = C[k]; k=k+1 /* Τα ταξινομημένα στοιχεία του C αποθηκεύoνται
ξανά στους υποπίνακες του S */
Τέλος επανάληψης
Για j=l2 Μέχρι r2 Εκτέλεσε
S[j] = C[k]; k=k+1
Τέλος επανάληψης
Aποτελέσματα // Tαξινομημένοι υποπίνακες S[l1..r1] και S[l2..r2], όπου S[r1] ≤ S[l2] //
Τέλος Merge

Για την ανάλυση του αλγόριθμου Ταξινόμησης με Συγχώνευση παρατηρούμε ότι σε κάθε
βήμα, διαιρείται το μέγεθος του προβλήματος κατά δύο, γεγονός που σημαίνει ότι θα
εκτελέσουμε το πολύ k = logn + 1 βήματα. Σε κάθε βήμα εκτελούνται O(n) συγκρίσεις οπότε
αποδεικνύεται ότι ο αλγόριθμος Ταξινόμησης με Συγχώνευση ταξινομεί μία ακολουθία μήκους

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 85


Αλγόριθμοι και Δομές Δεδομένων

n με nlogn – 2logn + 1 το πολύ συγκρίσεις, δηλ. έχει πολυπλοκότητα χειρότερης περίπτωσης


Ο(nlogn).

3.1.7 Ταξινόμηση με Μέτρηση (Countingsort)


Πρόκειται για έναν αλγόριθμο που χρησιμοποιεί κάποια πληροφορία για την είσοδο ώστε
να επιτύχει καλύτερους χρόνους.
Στην συγκεκριμένη περίπτωση, ο αλγόριθμος Ταξινόμησης με Μέτρηση μετρά πόσοι
αριθμοί είναι μικρότεροι από δοσμένο ακέραιο, έστω x, ώστε να προσδιορίσει τη θέση του x
στον ταξινομημένο πίνακα. Αν υπάρχουν i ακέραιοι < x τότε ο x θα τοποθετηθεί στην (i+1)-
oστή θέση του πίνακα.

Υποθέτουμε ότι η είσοδος στον αλγόριθμο είναι ο πίνακας S[1..n] και κάθε στοιχείο του
είναι ένας ακέραιος στο διάστημα [1..k]. Θεωρούμε επίσης ότι έχουμε στη διάθεσή μας τους
βοηθητικούς πίνακες B[1..n] και C[1..k] που αποθηκεύουν την ταξινομημένη έξοδο του
αλγόριθμου και το πλήθος των τιμών που είναι μικρότερες ή ίσες από ένα δεδομένο ακέραιο
αντίστοιχα (δηλ. και ο συγκεκριμένος αλγόριθμος δεν εκτελείται in-place). Ο αλγόριθμος
countingsort έχει τις εξής τρεις φάσεις:
Φάση 1: Αρχικοποιούμε τα στοιχεία του C με την τιμή 0. Σαρώνουμε τον S με φορά → και
για κάθε θέση i αυξάνουμε την τιμή C[S[i]] κατά 1.
Φάση 2: Σαρώνουμε τον C με φορά → και για κάθε θέση i προσθέτουμε τα στοιχεία C[i] και
C[i-1]. Έτσι κάθε C[j] αποθηκεύει το πλήθος των στοιχείων του S που είναι ≤ j.
Φάση 3: Σαρώνουμε τον S με φορά ← Έστω ότι είμαστε στη θέση i και S[i] = j. Το C[j]
δείχνει πόσα στοιχεία είναι ≤ j. Toποθετούμε την τιμή j στη θέση C[j] του Β και
μειώνουμε το C[j] κατά 1.
Ο αλγόριθμος σε μορφή ψευδοκώδικα παρουσιάζεται στη συνέχεια:

Aλγόριθμος Countingsort
Είσοδος (S, n, k)
Δεδομένα // B[1..n], C[1..k], i, j: ακέραιοι //
Aρχή
/* Φάση 1 */
Για i = 1 Μέχρι k με Βήμα 1
C[i] = 0
Τέλος Επανάληψης
Για j = 1 Μέχρι n με Βήμα 1
C[S[j]] = C[S[j]]+1
Τέλος Επανάληψης
/* Στο σημείο αυτό το C[i] περιέχει το πλήθος των στοιχείων S[j] με S[j] = i */

/* Φάση 2 */

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 86


Αλγόριθμοι και Δομές Δεδομένων

Για i = 2 Μέχρι k με Βήμα 1


C[i] = C[i]+C[i-1]
Τέλος Επανάληψης
/* Στο σημείο αυτό το C[i] περιέχει το πλήθος των στοιχείων S[j] με S[j] ≤ i */

/* Φάση 3 */
Για j = n Μέχρι 1 με Βήμα -1
B[C[S[j]]] = S[j]
C[S[j]] = C[S[j]]-1
Τέλος Επανάληψης
Αποτελέσματα // Tαξινομημένος πίνακας B[1..n] //
Τέλος Countingsort

Ακολουθεί ένα παράδειγμα εκτέλεσης του αλγόριθμου για n = 8 και k = 6:

Σχήμα 25: Λειτουργία του αλγόριθμου countingsort

Η χρονική πολυπλοκότητα του αλγόριθμου εξαρτάται από το k και το n. Από τον


ψευδοκώδικα είναι εύκολο να διαπιστώσει κανείς ότι η Φάση 1 έχει κόστος Ο(k), η Φάση 2
κόστος Ο(n+k) και η Φάση 3 Ο(k), άρα στο συνολικό κόστος του αλγόριθμου είναι Ο(n+k).
Αν το k = O(n) τότε o αλγόριθμος εκτελείται σε χρόνο O(n), δηλ. είναι ασυμπτωτικά
καλύτερος από κάθε άλλον αλγόριθμο που περιγράψαμε νωρίτερα και ο οποίος χρησιμοποιεί
συγκρίσεις. Το ίδιο συμβαίνει και στην περίπτωση που k=O(nloglogn). Όμως, οι συγκριτικοί
αλγόριθμοι είναι γενικοί με την έννοια ότι δεν εξετάζουν τη μορφή της εισόδου (πχ. ακέραια ή

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 87


Αλγόριθμοι και Δομές Δεδομένων

αλφαριθμητικά στοιχεία) παρά εκτελούν συγκρίσεις για να για να βρουν τη σειρά μεταξύ των
στοιχείων εισόδου.
Αν k = O(n2) ή ακόμα χειρότερα k = O(n3), ο αλγόριθμος απαιτεί υπερβολικό χρόνο.

3.1.8 Radixsort
Ο αλγόριθμος ταξινόμησης Radixsort χρησιμοποιείται και αυτός σε περιπτώσεις που
γνωρίζουμε συγκεκριμένες πληροφορίες για την μορφή εισόδου.
Έτσι, στην περίπτωση ταξινόμησης δεκαδικών αριθμών δημιουργούμε δέκα κάδους (όσα
και τα στοιχεία του δεκαδικού συστήματος) και διαβάζουμε τα ψηφία των αριθμών της
εισόδου από αριστερά προς τα δεξιά. Όσοι αρχίζουν πχ. από 3 τοποθετούνται στον τρίτο
κάδο, όσοι αρχίζουν από 8 στον όγδοο κάδο κ.ο.κ. Mε τον τρόπο αυτό τοποθετούμε σε κάδους
όλα τα στοιχεία του πίνακα. Στη συνέχεια χωρίζουμε κάθε κάδο σε 10 υποκάδους και
ακολουθούμε την ίδια διαδικασία για το δεύτερο ψηφίο.
Ο αλγόριθμος είναι αναδρομικός ενώ είναι μη αποδοτικός αφού για να ταξινομήσει n
αριθμούς με d ψηφία, όπου κάθε ψηφίο μπορεί να πάρει k διαφορετικές τιμές από 0 μέχρι k-
1, απαιτεί O(ndk) χρόνο και χώρο. Υπάρχει μία διαφορετική τεχνική που κοστίζει Ο(d(n+k))
χρόνο στην οποία διατρέχουμε τους αριθμούς από δεξιά προς τ’ αριστερά και τους
ταξινομούμε προχωρώντας από το λιγότερο σημαντικό προς το περισσότερο σημαντικό
ψηφίο τους. Υποθέτοντας ότι το ψηφίο 1 είναι το λιγότερο σημαντικό και το ψηφίο d το
περισσότερο σημαντικό (δηλ. ο αριθμός γράφεται ως adad-1ad-2a2a1), ο αλγόριθμος μπορεί να
περιγραφεί εύκολα με τον παρακάτω ψευδοκώδικα:

Aλγόριθμος Radixsort
Είσοδος (S, n, d, k)
Δεδομένα // i: ακέραιος //
Aρχή
Για i = 1 Μέχρι d με Βήμα 1
<Ταξινόμησε τους αριθμούς του πίνακα S με βάση το ψηφίο i>
Τέλος Επανάληψης
Αποτελέσματα // Tαξινομημένος πίνακας S[1..n] //
Τέλος Radixsort

Aν το k δεν είναι μεγάλο, τότε η προφανής επιλογή για την ταξινόμηση είναι ο αλγόριθμος
countingsort και άρα κάθε επανάληψη του παραπάνω loop κοστίζει Ο(n+k) χρόνο. Επειδή
γίνονται d επαναλήψεις, ο συνολικός χρόνος εκτέλεσης γίνεται Ο(d(n+k)). Αν το d είναι
σταθερό και k = O(n), τότε ο radixsort τρέχει σε γραμμικό χρόνο. Το μειονέκτημα φυσικά της
χρήσης του countingsort είναι ότι δεν εκτελείται in-place σε αντίθεση με τους περισσότερους
συγκριτικούς αλγόριθμους που είδαμε.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 88


Αλγόριθμοι και Δομές Δεδομένων

3.1.9 Εξωτερική Ταξινόμηση (External Sorting)


Στην εξωτερική ταξινόμηση τα στοιχεία της δομής δεδομένων τα οποία καλείται ο
αλγόριθμος τα ταξινομήσει είναι αποθηκευμένα στην δευτερεύουσα μνήμη.
Ο λόγος που επιβάλει την αποθήκευση των στοιχείων της δομής στην δευτερεύουσα
μνήμη είναι το μικρό μέγεθος της κεντρικής μνήμης σε σχέση με το σύνολο των στοιχείων που
απαιτείται να διαταχθούν. Για το λόγω αυτό υποθέτουμε ότι η κύρια μνήμη έχει μέγεθος M
όπου n >> M, ενώ χρησιμοποιούνται και 2 p μαγνητικές ταινίες (δευτερεύουσα μνήμη)
εναλλάξ, p για είσοδο και p για έξοδο.
Ο αλγόριθμος περιλαμβάνει την εκτέλεση 2 φάσεων:
Φάση 1: Παραγωγή ταξινομημένων υπακολουθιών. Διαβάζονται τα στοιχεία από την ταινία
εισόδου στην κύρια μνήμα σε μπλοκ των M, ταξινομούνται στην κύρια μνήμη και
στη συνέχεια οι ταξινομημένες ομάδες γράφονται εναλλάξ στην κατάλληλη ταινία
εξόδου.
Φάση 2: Συγχώνευση ταξινομημένων υπακολουθιών. Οι p ταινίες εισόδου που παρήχθησαν
στη Φάση 1 συγχωνεύονται και το αποτέλεσμα γράφεται στις ταινίες εξόδου
δημιουργώντας μπλοκ μεγαλύτερου μήκους. Στη συνέχεια οι ταινίες εισόδου
γίνονται ταινίες εξόδου και αντίστροφα και η συγχώνευση επαναλαμβάνεται μέχρι
να προκύψει ένα μόνο μπλοκ που θα περιέχει την ταξινομημένη έξοδο.
Για την συγχώνευση των ταινιών μπορεί να χρησιμοποιηθεί μία παραλλαγή του αλγόριθμου
Merge ο οποίος συγχωνεύει p > 2 ταξινομημένες ακολουθίες αντί των 2 που έχουμε δει στον
κλασικό αλγόριθμο.

Έστω ότι η είσοδος στον αλγόριθμο είναι η ακολουθία χαρακτήρων:


Α SORTING AND MERGING EXAMPLE
όπου αγνοούμε τα κενά και Μ = p = 3.

Φάση 1
To i-oστό μπλοκ (1 ≤ i ≤ n/M) που διαβάζουμε το γράφουμε στην ταινία (i-1) mod p + 1.
Το περιεχόμενο των ταινιών θα είναι:
Ταινία 1: AOS ■ DMN ■ AEX ■
Ταινία 2: IRT ■ EGR ■ LMP ■
Ταινία 3: AGN ■ GIN ■ E ■

Φάση 2
Βήμα 1: Συγχωνεύουμε στην κύρια μνήμη τα στοιχεία της i-οστής ομάδας μπλοκ (1 ≤ i ≤
(n/M)/p)) από τις p ταινίες εισόδου και τα γράφουμε στην ταινία (i-1) mod p + p+1. Εδώ
χρησιμοποιούμε τον αλγόριθμο Merge p ταξινομημένων ακολουθιών. Οι ταινίες θα είναι τώρα:
Ταινία 4: ΑΑGINORST ■
Ταινία 5: DEGGIMNNR ■
Ταινία 6: AEELMPX ■

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 89


Αλγόριθμοι και Δομές Δεδομένων

Βήμα 2: Οι ταινίες εισόδου γίνονται ταινίες εισόδου και αντίστροφα. Προχωρούμε όπως στο
βήμα 1 μέχρι να πάρουμε μια ταξινομημένη ακολουθία και άρα έχουμε:
Έξοδος στην Ταινία 1: ΑΑΑDEEEGGGIILMNNNOPRRSTX ■

Η χρονική συμπεριφορά του αλγόριθμου εξαρτάται από το μέγεθος του μπλοκ που παράγεται
στη Φάση 1 αφού επηρεάζει το πλήθος των βημάτων συγχώνευσης που απαιτούνται μέχρι
την τελική ταξινόμηση της ακολουθίας εισόδου. Μετά από κάθε συγχώνευση το πλήθος των
ταξινομημένων μπλοκ μειώνεται κατά ένα παράγοντα p, πχ. στο προηγούμενο παράδειγμα
αρχικά έχουμε 9 ταξινομημένα μπλοκ (τα 8 έχουν 3 στοιχεία και το τελευταίο μόνο 1), στη
συνέχεια 3 ταξινομημένα μπλοκ (τα 2 πρώτα έχουν 9 στοιχεία και το τρίτο 7) και στην έξοδο 1
μπλοκ με 25 στοιχεία. Επομένως, το πλήθος βημάτων συγχώνευσης είναι logpn/M.
Υπάρχει παραλλαγή του προηγούμενου αλγόριθμου που ονομάζεται Replacement Selection
η οποία χρησιμοποιεί ως βασική δομή στην κύρια μνήμη μία ουρά προτεραιότητας (πχ. ένα
δυαδικό σωρό) και κάνει κατά μέσο όρο logpn/2M βήματα συγχώνευσης (το μέγεθος των
μπλοκ που παράγονται στη Φάση 1 είναι κατά μέσο όρο το διπλάσιο σε σύγκριση με τον
προηγούμενο αλγόριθμο).

3.1.9 Πόσο γρήγορα μπορούμε να ταξινομούμε;


Έως τώρα είδαμε αρκετούς συγκριτικούς αλγόριθμους ταξινόμησης με διαφορετικές
πολυπλοκότητες που στη μέση και χειρότερη περίπτωση κυμαίνονταν από Ο(nlogn) μέχρι
Ο(n2). To ερώτημα που μπαίνει φυσιολογικά είναι το εξής: μπορούμε να ταξινομήσουμε n
στοιχεία σε χρόνο λιγότερο από Θ(nlogn) σε οποιαδήποτε περίπτωση; Η απάντηση είναι ότι
κάθε αλγόριθμος ταξινόμησης που χρησιμοποιεί συγκρίσεις για να προσδιορίσει τη σειρά των
στοιχείων κάνει Ω(nlogn) συγκρίσεις και στη μέση και στη χειρότερη περίπτωση. Στη συνέχεια
αποδεικνύουμε αυτό το κάτω φράγμα.
Υποθέστε ότι το μέγεθος εισόδου n είναι σταθερό και τα στοιχεία που θέλουμε να
ταξινομήσουμε είναι τα s1, s2, …, sn, όλα διαφορετικά μεταξύ τους. Η λειτουργία κάθε
συγκριτικού αλγόριθμου ταξινόμησης n στοιχείων μπορεί να παρασταθεί με ένα δυαδικό
δένδρο αποφάσεων (binary decision tree) ή απλά δένδρο συγκρίσεων που περιγράφει
την ακολουθία συγκρίσεων που εκτελεί ο αλγόριθμος σε οποιαδήποτε είσοδο μεγέθους n.
Είναι εύκολο να ορίσουμε αυτά τα δένδρα απόφασης για αλγόριθμους που δεν περιέχουν
loops και αποτελούνται από τρία μόνο είδη εντολών: μια εντολή σύγκρισης όπου η ροή
ελέγχου διακλαδίζεται σε δύο διαφορετικά μονοπάτια, μια εντολή εκτύπωσης που δίνει ως
έξοδο την ταξινομημένη ακολουθία των n στοιχείων και μία εντολή stop που τερματίζει την
εκτέλεση του αλγόριθμου. Είναι εύκολο επίσης να δούμε ότι οποιοσδήποτε συγκριτικός
αλγόριθμος ταξινόμησης, έστω sort, είναι ισοδύναμος με έναν τέτοιο «απλοϊκό» αλγόριθμο με
την έννοια ότι για κάθε είσοδο μεγέθους n o απλοϊκός αλγόριθμος κάνει τις ίδιες ακριβώς
συγκρίσεις και με την ίδια σειρά και τυπώνει την ίδια μετάθεση των n στοιχείων (ταξινομημένη
ακολουθία) όπως και ο sort.
Ένα παράδειγμα δένδρου απόφασης για n = 3 φαίνεται στο επόμενο σχήμα για τον
αλγόριθμο insertionsort:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 90


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 26: Το δένδρο αποφάσεων παράγει ο insertionsort για 3 στοιχεία

Στο δένδρο αποφάσεων κάθε εσωτερικός κόμβος (που στο σχήμα παριστάνεται με έλλειψη
και αντιστοιχεί σε μία εντολή σύγκρισης) έχει ακριβώς 2 παιδιά ενώ τα φύλλα (που στο σχήμα
παριστάνονται με παραλληλόγραμμα και αντιστοιχούν σε εντολές εκτύπωσης της
ταξινομημένης ακολουθίας των n στοιχείων) έχουν 0 παιδιά. Για κάθε στιγμιότυπο εισόδου ο
αλγόριθμος sort ακολουθεί στο δένδρο αποφάσεων ένα μοναδικό μονοπάτι από τη ρίζα σε
κάποιο φύλλου του δένδρου που εξαρτάται μόνο από τη σχετική θέση που έχουν τα στοιχεία
μεταξύ τους και όχι από τις πραγματικές τους τιμές. Το πλήθος των εσωτερικών κόμβων πάνω
στο μακρύτερο μονοπάτι του δένδρου αποφάσεων, ή αλλιώς το ύψος του δένδρου, καθορίζει
το μέγιστο πλήθος συγκρίσεων που εκτελεί ο sort. Για να πάρουμε ένα κάτω φράγμα για τη
χειρότερη περίπτωση πρέπει να βρούμε ένα κάτω φράγμα στο ύψος του δένδρου αποφάσεων.
To δένδρο αποφάσεων πρέπει να έχει τουλάχιστον n! διαφορετικά φύλλα εφόσον υπάρχουν
n! διαφορετικές μεταθέσεις n στοιχείων. Αν h είναι το ύψος του δένδρου, τότε ισχύει:

Mία πιο ακριβής εκτίμηση για το n! είναι (n/e)n, όπου e=2,717… η βάση των νεπέρειων
λογάριθμων οπότε h ≥ nlogn-1,44n.
Το προηγούμενο φράγμα δεν μας λέει ότι η ταξινόμηση n στοιχείων μπορεί να γίνει με
Θ(nlogn) συγκρίσεις παρά μόνο ότι Ω(nlogn) συγκρίσεις είναι απαραίτητες. Oι heapsort και
mergesort που είδαμε νωρίτερα κάνουν Θ(nlogn) συγκρίσεις, δηλ. πιάνουν το κάτω φράγμα
και άρα είναι ασυμπτωτικά βέλτιστοι.

Το φράγμα Ω(nlogn) ισχύει και για τη μέση συμπεριφορά των συγκριτικών αλγόριθμων
ταξινόμησης. Για την απόδειξη εισάγουμε την παρακάτω έννοια:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 91


Αλγόριθμοι και Δομές Δεδομένων

External path length (epl): το άθροισμα των μηκών όλων των μονοπατιών από τη ρίζα σε
ένα φύλλο του δένδρου αποφάσεων
Σε ένα δυαδικό δένδρο όπου κάθε κόμβος έχει 2 ή 0 παιδιά ισχύει eplmin(n!) ≥ n!log(n!).
Υποθέτοντας ότι όλες οι n! μεταθέσεις n στοιχείων έχουν την ίδια πιθανότητα να εμφανιστούν
ως είσοδοι στον αλγόριθμο, τότε έχουμε:
Μέσο πλήθος συγκρίσεων ≥ 1/n! eplmin(n!)
≥ 1/n!(n!log(n!)) = log(n!) = Ω(nlogn)

3.2 Αναζήτηση στοιχείων


Στην παράγραφο αυτή θα αναλύσουμε τις βασικές μεθόδους αναζήτησης σε ταξινομημένα
σύνολα στοιχείων τα οποία αποθηκεύονται σε κάποια δομή δεδομένων. Το πρόβλημα της
αναζήτησης συνίσταται στην εύρεση ενός στοιχείου στη δομή μας. Η ταξινόμηση εδώ έχει
ουσιαστικό ρόλο και, όπως θα δούμε, επιτρέπει η πράξη της αναζήτησης να εκτελείται
αποδοτικά.

3.2.1 Γενικός αλγόριθμος αναζήτησης


Έστω ότι η δομή δεδομένων που αποθηκεύει τα ταξινομημένα στοιχεία είναι ένας
πίνακας. Παρακάτω δίνεται ένας γενικός αλγόριθμος αναζήτησης που παίρνει ως είσοδο τον
πίνακα S ο οποίος περιέχει n στοιχεία ταξινομημένα κατ΄ αύξουσα σειρά και το στοιχείο x
που αναζητούμε και παράγει στην έξοδο τη θέση του x στον πίνακα, αν βρεθεί, και 0
διαφορετικά (ή ενδεχομένως ένα μήνυμα ότι το x δεν υπάρχει στον πίνακα).

Γενικός Αλγόριθμος Αναζήτησης


Είσοδος (S, n, x) /* Ο S είναι ταξινομημένος κατ΄ αύξουσα σειρά) */
Δεδομένα // left, right, next: ακέραιοι; found: δυαδική //
Αρχή
left =1; right=n
found = FALSE
Όσο (left <= right) AND (found = FALSE) Εκτέλεσε
next = ένας αριθμός από το [left..right]
Αν S[next] = x Τότε found = TRUE
Αλλιώς
Aν S[next] > x Τότε right = next-1
Aλλιώς left = next+1
Tέλος Αν
Τέλος Αν
Τέλος Όσο
Αποτελέσματα // found, next //
Τέλος Γενικού Αλγόριθμου Αναζήτησης

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 92


Αλγόριθμοι και Δομές Δεδομένων

Στον παραπάνω αλγόριθμο left και right είναι το αριστερό και δεξί όριο του υποπίνακα
στον οποίο ψάχνουμε κάθε φορά για το x. Η αναζήτηση γίνεται στη θέση next και αν το x
βρεθεί ο αλγόριθμος τερματίζει επιτυχώς (η μεταβλητή found γίνεται TRUE), διαφορετικά
ενημερώνονται οι δείκτες left και right ώστε την επόμενη φορά η αναζήτηση να περιοριστεί σε
μικρότερο υποπίνακα.
Ο αλγόριθμος τερματίζει γιατί η τιμή right-left σε κάθε διέλευση του επαναληπτικού loop
Όσο … Τέλος όσο μειώνεται τουλάχιστον κατά 1 και πάντα συγκρίνεται right ≥ left. Αν στο
τέλος η found είναι FALSE τότε η αναζήτηση ήταν ανεπιτυχής (το x δεν βρέθηκε στον πίνακα),
διαφορετικά επιστρέφεται στη μεταβλητή next η θέση του x στον πίνακα.
Στον γενικό αλγόριθμο αναζήτησης, επιλέγοντας κάθε φορά με διαφορετικό τρόπο τη θέση
next στην οποία ψάχνουμε το x, έχουμε και διαφορετική μέθοδο αναζήτησης όπως
περιγράφεται στα επόμενα.

3.2.2 Γραμμική (ακολουθιακή) Αναζήτηση (Linear Search)


Η γραμμική μέθοδος διατρέχει όλα τα στοιχεία του πίνακα από αριστερά προς τα δεξιά
συγκρίνοντάς τα με την τιμή που αναζητούμε. Δηλαδή η αναζήτηση του x γίνεται στη θέση
next του πίνακα ο οποίος στον γενικό αλγόριθμο αναζήτησης τίθεται κάθε φορά ίσος με την
τιμή left. Έτσι έχουμε τον παρακάτω ψευδοκώδικα:

Αλγόριθμος Γραμμικής αναζήτησης


Είσοδος (S, n, x)
Δεδομένα // left, right, next: ακέραιοι; found: δυαδική //
left =1; right=n
found = FALSE
Όσο (left <= right) AND (found = FALSE) Εκτέλεσε
next = left
Αν S[next] = x Τότε found = TRUE
Aλλιώς
Αν S[next] > x Τότε right = next-1
Aλλιώς left = next+1
Τέλος Αν
Τέλος Αν
Τέλος Όσο
Αποτελέσματα // found, next //
Τέλος Γραμμικής αναζήτησης

Δεδομένου ότι τα στοιχεία του πίνακα είναι ταξινομημένα κατ΄ αύξουσα σειρά, την πρώτη
φορά που θα βρεθεί το S[next] > x τότε θα γίνει right = left - 1 και ο αλγόριθμος θα
τερματιστεί ανεπιτυχώς.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 93


Αλγόριθμοι και Δομές Δεδομένων

Πρέπει να τονιστεί ο απλός και άμεσος τρόπος αναζήτησης της γραμμικής μεθόδου. Όμως
η απόδοση της μεθόδου είναι η χειρότερη σε σύγκριση με τις υπόλοιπες που θα εξετάσουμε
παρακάτω γιατί ακριβώς δεν εκμεταλλεύεται το γεγονός ότι ο πίνακας είναι ταξινομημένος.
Πιο συγκεκριμένα, εφόσον έχουμε n στοιχεία, ο χρόνος της χειρότερης περίπτωσης προκύπτει
όταν το στοιχείο που αναζητούμε στην δομή μας είναι ≥ S[n]. Στην περίπτωση αυτή η
γραμμική μέθοδος θα απαιτήσει n συγκρίσεις ενώ το μέσο πλήθος συγκρίσεων είναι n/2, ό,τι
δηλ. ισχύει με τη γραμμική αναζήτηση σε έναν τυχαίο (μη ταξινομημένο) πίνακα.

3.2.3 Δυαδική Aναζήτηση (Binary Search)


Η δυαδική αναζήτηση εφαρμόζει την τεχνική Διαίρει και Βασίλευε.
Αρχικά συγκρίνεται το στοιχείο αναζήτησης με το μεσαίο στοιχείο (το στοιχείο που
βρίσκεται στη μέση) του πίνακα. Αν είναι ίσα, ο αλγόριθμος τερματίζει επιτυχώς. Αν το
μεσαίο στοιχείο είναι μικρότερο του στοιχείου αναζήτησης τότε ο αλγόριθμος συνεχίζεται στο
δεύτερο μισό του πίνακα ενώ αν είναι μεγαλύτερο ο αλγόριθμος συνεχίζεται στο πρώτο μισό
του πίνακα. Έχουμε δηλ. εφαρμογή της τεχνικής Διαίρει και Βασίλευε όπου ο πίνακας
χωρίζεται στη μέση δημιουργώντας δύο στιγμιότυπα εισόδου και ο αλγόριθμος συνεχίζει την
εκτέλεσή του στο ένα από τα δύο κάθε φορά.
Στη συνέχεια παρουσιάζεται ο αλγόριθμος της δυαδικής αναζήτησης με τη μορφή
ψευδοκώδικα. Η αναζήτηση γίνεται κάθε φορά στη θέση next = (left+right)/2.

Αλγόριθμος Δυαδικής Αναζήτησης


Είσοδος (S, n, x)
Δεδομένα // left, right, next: ακέραιοι; found: δυαδική //
left = 1
right = n
found=FALSE
Όσο (left <= right) AND (found = FALSE) Εκτέλεσε
next = (left+right)/2
Aν x = S[next] Τότε found = TRUE
Aλλιώς
Αν S[next] > x Τότε right = next-1
Aλλιώς left = next+1
Τέλος Αν
Τέλος Αν
Τέλος Όσο
Αποτελέσματα // found, next //
Tέλος Δυαδικής Αναζήτησης

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 94


Αλγόριθμοι και Δομές Δεδομένων

Στην περίπτωση που το στοιχείο x που ψάχνουμε υπάρχει στον πίνακα, με το τέλος του
αλγόριθμου, η μεταβλητή next δείχνει τη θέση του μέσα σ’ αυτόν.
Η διαδικασία της δυαδικής αναζήτησης μπορεί να παρασταθεί με ένα δυαδικό δένδρο
αναζήτησης n στοιχείων. Υποθέτουμε για ευκολία ότι n = 2k -1 για κάποιο k ≥ 1 που σημαίνει
ότι το δένδρο είναι πλήρες και έχει k = log(n+1) επίπεδα. Πχ. για 15 στοιχεία το δένδρο θα έχει
την παρακάτω μορφή:

Σχήμα 27: Δένδρο αναζήτησης του αλγόριθμου binary search

Εφόσον έχουμε n στοιχεία, ο χρόνος της χειρότερης περίπτωσης προκύπτει όταν το


στοιχείο που αναζητούμε δεν υπάρχει στην δομή μας (ή είναι το τελευταίο που εξετάζεται).
Στην περίπτωση αυτή ο αλγόριθμος της δυαδικής αναζήτησης θα απαιτήσει ακριβώς log(n+1)
συγκρίσεις, δηλ. έχει πολυπλοκότητα O(logn).

3.2.4 Αναζήτηση Παρεμβολής (Ιnterpolation Search)


Η μέθοδος αναζήτησης παρεμβολής είναι μια διαφορετική εκδοχή της δυαδικής
αναζήτησης η οποία λαμβάνει υπόψη της την κατανομή των στοιχείων στον πίνακα. Πιο
συγκεκριμένα, ακολουθεί τον τρόπο που κάποιος ψάχνει μια λέξη σε ένα λεξικό ή ένα όνομα
στον τηλεφωνικό κατάλογο. Η αναζήτηση δεν γίνεται κατευθείαν στη μέση, έστω θέση n/2 του
λεξικού, μετά στη θέση n/4 ή 3n/4 κ.ο.κ. όπως συμβαίνει στην δυαδική αναζήτηση. Ανοίγει το
λεξικό αρχικά ανάλογα με το γράμμα που τον ενδιαφέρει. Αν αναζητεί μία λέξη που αρχίζει
από Ψ πηγαίνει προς το τέλος, αν αναζητεί μία λέξη που αρχίζει από το B πηγαίνει στην
αρχή. Στη συνέχεια κινείται κατά ομάδες ανάλογα αν η λέξη αναζήτησης προηγείται ή έπεται
αλφαβητικά παραλείποντας σελίδες.
Επομένως, ο αλγόριθμος λαμβάνει υπόψη το περιεχόμενο του πίνακα σε σχέση με το
στοιχείο αναζήτησης x, προκειμένου να καθορίσει το ακριβές σημείο αναζήτησης. Tώρα η
θέση αναζήτησης next καθορίζεται από τη σχέση:
next = left+ (x-S[left]) / (S[right]-S[left]) * (right-left)
όπου η ποσότητα (x-S[left]) / (S[right]-S[left]) αποτελεί μία εκτίμηση πού μπορεί να βρίσκεται
το x στον πίνακα (στη δυαδική αναζήτηση η ποσότητα αυτή είναι ίση με ½ αφού (left+right)/2
= left + 1/2(right-left)). Έτσι, αρχικά το x αναζητείται στα στοιχεία ανάμεσα στα S[1] και S[n].
Άρα, η θέση αναζήτησης είναι η 1+ (n-1)*(x-S[1]) / (S[n]-S[1]). Στη συνέχεια κι αν δεν βρεθεί
το x, ενημερώνεται το δεξί ή το αριστερό όριο του πίνακα και η αναζήτηση συνεχίζεται με τον

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 95


Αλγόριθμοι και Δομές Δεδομένων

ίδιο τρόπο στον υποπίνακα που δημιουργείται. Επειδή η θέση του στοιχείου που συγκρίνεται
κάθε φορά με το στοιχείο x που ψάχνουμε εξαρτάται και από το x, θα πρέπει να ελέγχουμε σε
κάθε επανάληψη αν το x είναι εντός των ορίων του υποπίνακα που εξετάζουμε.
Παρακάτω δίνεται ο αλγόριθμος της αναζήτησης με παρεμβολή σε μορφή ψευδοκώδικα:

Αλγόριθμος Αναζήτησης παρεμβολής


Είσοδος (S, n, x)
Δεδομένα // left, right, next: ακέραιοι; found: δυαδική //
left = 1
right = n
found=FALSE
Όσο (S[left] <= x AND x <= S[right]) AND (found = FALSE) Εκτέλεσε
next = left+ x-S[left]) / (S[right]-S[left]) * (right-left)
Aν x = S[next] Τότε found = TRUE
Aλλιώς
Αν S[next] > x Τότε right = next-1
Aλλιώς left = next+1
Τέλος Αν
Τέλος Αν
Τέλος Όσο
Αποτελέσματα // found, next //
Tέλος Αναζήτησης Παρεμβολής

Εφόσον έχουμε n στοιχεία, ο χρόνος της χειρότερης περίπτωσης στην αναζήτηση


παρεμβολής συμβαίνει όταν δεν υπάρχει το στοιχείο που αναζητούμε στην δομή μας (ή είναι
το τελευταίο που εξετάζεται). Στην περίπτωση αυτή θα απαιτήσει O(n) συγκρίσεις, και άρα η
συμπεριφορά χειρότερης περίπτωσης του αλγόριθμου είναι κακή.
Για να είναι η αναζήτηση παρεμβολής αποδοτική απαιτείται μία καλή κατανομή των
στοιχείων του πίνακα μεταξύ του πρώτου και του τελευταίου. Στην περίπτωση που τα στοιχεία
είναι ομοιόμορφα κατανεμημένα, τότε αποδεικνύεται ότι το μέσο πλήθος συγκρίσεων που
εκτελεί ο αλγόριθμος είναι O(loglogn) (μπορούμε να φανταστούμε τη μέθοδο παρεμβολής ως
δυαδική αναζήτηση πάνω στους O(logn) κόμβους ενός μονοπατιού στο αντίστοιχο δυαδικό
δένδρο αναζήτησης). Όταν η κατανομή των στοιχείων δεν είναι ομοιόμορφη, ή πολλά από τα
στοιχεία του πίνακα είναι ίσα, τότε η μέθοδος είναι χειρότερη από τη δυαδική. Πειραματικά
αποτελέσματα έχουν δείξει ότι είναι προτιμότερο τα πρώτα βήματα να εκτελούνται με
αναζήτηση παρεμβολής, έτσι ώστε το διάστημα να μειώνεται δραστικά, και στη συνέχεια να
εκτελείται δυαδική αναζήτηση.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 96


Αλγόριθμοι και Δομές Δεδομένων

3.2.5 Αναζήτηση κατά Oμάδες (Block Search)


Εδώ τα στοιχεία του πίνακα χωρίζονται σε ομάδες στοιχείων. Σε κάθε ομάδα βασικό ρόλο
παίζει το πρώτο στοιχείο της.
Η πιο απλή εκδοχή αναζήτησης κατά ομάδες συγκρίνει το στοιχείο x που ψάχνουμε με το
πρώτο στοιχείο κάθε ομάδας. Έτσι, αρχικά τo x συγκρίνεται με το πρώτο στοιχείο της πρώτης
ομάδας. Αν είναι μικρότερο αυτού, τότε δεν υπάρχει στην δομή μας. Στην αντίθετη περίπτωση
συγκρίνεται με το πρώτο στοιχείο της δεύτερης ομάδας. Αν είναι μικρότερο αυτού τότε, αν
υπάρχει, βρίσκεται ανάμεσα στα στοιχεία της πρώτης ομάδας. Στην αντίθετη περίπτωση
συγκρίνεται με το πρώτο στοιχείο της τρίτης ομάδας κ.ο.κ.
Έστω ότι χωρίζουμε τα n στοιχεία του πίνακα σε m ομάδες. Στην γενική περίπτωση, το
μέσο πλήθος συγκρίσεων είναι (n/m)/2 + m/2. Είναι σαφές ότι η απόδοση του αλγόριθμου
εξαρτάται από το πλήθος των ομάδων άρα και του πλήθους των στοιχείων που περιέχει κάθε
μία από αυτές. Αποδεικνύεται ότι ο αλγόριθμος δίνει την καλύτερη απόδοση όταν κάθε ομάδα
περιέχει √n στοιχεία.
Ένας άλλος τρόπος αναζήτησης κατά ομάδες χρησιμοποιεί αναζήτηση παρεμβολής για το
στοιχείο που ψάχνουμε και λειτουργεί αναδρομικά. Η βασική ιδέα είναι η εξής:
Βήμα 1: Ψάξε το x εφαρμόζοντας interpolation
Βήμα 2: Ψάξε ανά √n αποστάσεις για τον υποπίνακα μεγέθους √n που μπορεί να περιέχει
το x
Βήμα 3: Επανέλαβε τα βήματα 1 και 2 στον υποπίνακα αυτό
Η παραπάνω μέθοδος λέγεται Αναζήτηση με Δυαδική Παρεμβολή (Binary Interpolation
Search) και αποτυπώνεται σχηματικά παρακάτω:

Σχήμα 28: Λειτουργία μεθόδου αναζήτησης με δυαδική παρεμβολή

Στη χειρότερη περίπτωση η αναζήτηση με δυαδική παρεμβολή κάνει Ο(√n) συγκρίσεις


ενώ στη μέση περίπτωση Ο(loglogn). Η διαίσθηση για το χρόνο μέσης περίπτωσης είναι η
εξής: το Βήμα 2 της μεθόδου περιμένουμε να στοιχίζει Ο(1) χρόνο κατά μέσο όρο και σε κάθε
αναδρομική κλήση το μέγεθος του υποπίνακα από k μειώνεται σε √k μέχρι να γίνει 1.
Επομένως, εκτελούνται loglogn βήματα (αναδρομικές κλήσεις) που δίνουν συνολική μέση
πολυπλοκότητα Ο(loglogn).
Ο χρόνος χειρότερης περίπτωσης μπορεί να μειωθεί σε Ο(logn) όταν μετά την παρεμβολή
κάνουμε εκθετικά μεγάλα βήματα και συγκρίνουμε το x με τα στοιχεία σε απόσταση 2i√n κάθε
φορά (δηλ. η αναζήτηση γίνεται στα στοιχεία √n, 2√n, 22√n, 23√n, …, 2log√n-1√n) κι όταν
εντοπίσουμε την ομάδα που μπορεί να περιέχει το x εφαρμόσουμε δυαδική αναζήτηση στα
στοιχεία της ομάδας που απέχουν μεταξύ τους κατά √n (δηλ. στα στοιχεία 2k√n, (2k+1)√n,
(2k+2)√n, …, 2k+1√n). Έτσι καταλήγουμε τελικά σε έναν υποπίνακα μεγέθους √n που μπορεί
να περιέχει το x στον οποίο ο αλγόριθμος συνεχίζει αναδρομικά την αναζήτηση.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 97


Αλγόριθμοι και Δομές Δεδομένων

3.3 Eπιλογή του i-οστού μεγαλύτερου στοιχείου


Στην παράγραφο 2.1 είδαμε πώς μπορούμε να εφαρμόσουμε την πράξη της σάρωσης σε
έναν τυχαίο πίνακα για να βρούμε το μεγαλύτερο στοιχείο του. Για n στοιχεία ο αλγόριθμος
εκτελούσε ακριβώς n-1 συγκρίσεις και ήταν βέλτιστος αναφορικά με το πλήθος συγκρίσεων.
Υποθέστε ότι έχουμε έναν πίνακα S[1..n] με τυχαίους ακέραιους που είναι διαφορετικοί
μεταξύ τους και ψάχνουμε να βρούμε το i-oστό μεγαλύτερο στοιχείο του, δηλ. το στοιχείο
εκείνο που είναι μεγαλύτερο από ακριβώς i-1 στοιχεία του S. Ο ενδιάμεσος ή μέσος
(median) του πίνακα είναι το στοιχείο με το μεσαίο μέγεθος. Αν το n είναι περιττός, τότε ο
ενδιάμεσος είναι μοναδικός και i = (n+1)/2. Aν το n είναι άρτιος, τότε υπάρχουν δύο
ενδιάμεσοι για i=n/2 και i = n/2+1.
Mε την πρώτη ματιά το πρόβλημα της εύρεσης του i-oστού μεγαλύτερου στοιχείου
φαίνεται δυσκολότερο από το αντίστοιχο πρόβλημα εύρεσης του μεγαλύτερου. Ένας
προφανής τρόπος να πάρουμε το i-oστό μεγαλύτερο στοιχείο είναι να ταξινομήσουμε πρώτα
τον πίνακα, οπότε μετά παίρνουμε το ζητούμενο στοιχείο σε χρόνο Ο(1). Όμως έχουμε ήδη
πληρώσει το κόστος Ο(nlogn) της ταξινόμησης. Στη συνέχεια θα δούμε πως η ασυμπτωτική
πολυπλοκότητα για το πρόβλημα του i-oστού μεγαλύτερου είναι Θ(n), δηλ. ίδια με αυτήν του
προβλήματος εύρεσης του μεγαλύτερου και σαφώς καλύτερη από αυτήν της ταξινόμησης.

3.3.1 Ο αλγόριθμος Find


O πρώτος αλγόριθμος που θα δούμε είναι ο αλγόριθμος Find που αναπτύχθηκε από τον
Ηοare. Βασίζεται στην ίδια τεχνική που εφαρμόζει και ο quicksort για το διαχωρισμό του
πίνακα με βάση ένα στοιχείο διαχωρισμού.
Έστω x το στοιχείο διαχωρισμού, S1 o υποπίνακας που περιέχει το x και όλα τα
στοχιεία που βρίσκονται αριστερά του x μετά το διαχωρισμό και S2 ο υποπίνακας με τα
υπόλοιπα στοιχεία που βρίκονται δεξιά του x. To μέγεθος του S1 είναι αυτό που καθοδηγεί την
εύρεση του i-οστού μεγαλύτερου στοιχείου στον S. Aν |S1| = i, τότε το i-oστό μεγαλύτερο
στοιχείο είναι το x. Διαφορετικά, αν |S1| > i, τότε το i-oστό μεγαλύτερο στοιχείο βρίσκεται στον
υποπίνακα S1 και αριστερά του x, oπότε εφαρμόζουμε αναδρομικά τον αλγόριθμο στο σύνολο
S1 –{x}. Αν τέλος |S1| < i, τότε το i-oστό μεγαλύτερο στοιχείο βρίσκεται στον υποπίνακα S2,
oπότε εφαρμόζουμε αναδρομικά τον αλγόριθμο στον S2 αλλά ψάχνουμε για το (i-|S1|)-oστό
μεγαλύτερο στοιχείο.
Η παραπάνω ιδέα μπορεί αλγοριθμικά να περιγραφεί ως εξής:

Aλγόριθμος Find(S, l, r, i)
Δεδομένα // l, r, i, k, s: ακέραιοι //
Aρχή
Αν (l=r) Τότε επέστρεψε S[l]
k = θέση του στοιχείου διαχωρισμού που επιστρέφει η Partition με είσοδο τον
υποπίνακα S[l ..r]
s= k-l+1 /* Μέγεθος υποπίνακα S1 */
Aν (s = i) Τότε επέστρεψε (S[k])
Αλλιώς Αν (s > i) Τότε επέστρεψε (Find(S, l, k-1, i))

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 98


Αλγόριθμοι και Δομές Δεδομένων

Αλλιώς επέστρεψε (Find(S, k+1, r, i-s))


Τέλος Αν
Τέλος Αν
Αποτελέσματα // i-oστό μεγαλύτερο στοιχείο του S[l..r] //
Τέλος Find

Για τον πίνακα S[1..n] η αρχική κλήση στον αλγόριθμο θα έχει τη μορφή Find(S, 1, n, i). Η
συνάρτηση Partition() είναι ίδια με αυτήν που έχουμε δει στον quicksort όπου σαν στοιχείο
διαχωρισμού επιλέγεται το αριστερότερο στοιχείο του πίνακα (πρέπει κι εδώ να προβλεφθεί
το στοιχείο-φρουρός στη θέση n+1 του πίνακα). Φυσικά, μπορούμε να χρησιμοποιήσουμε
και οποιαδήποτε παραλλαγή της Partition() όπου το στοιχείου διαχωρισμού επιλέγεται τυχαία
ή είναι ο μέσος από τρεις που στην πράξη δίνει καλύτερα αποτελέσματα.
Η ανάλυση του αλγόριθμου Find είναι ίδια με αυτήν του quicksort. Υπενθυμίζουμε ότι ο
διαχωρισμός του πίνακα απαιτεί χρόνο Ο(n). H χειρότερη περίπτωση για τον Find για
οποιοδήποτε i προκύπτει όταν σε κάθε αναδρομική κλήση το στοιχείο διαχωρισμού είναι κακό
και χωρίζει τον πίνακα με μη ισορροπημένο τρόπο πχ. όταν συμβαίνει το στοιχείο
διαχωρισμού να είναι το μεγαλύτερο στοιχείο κάθε φορά. Στην περίπτωση αυτή, όπως είδαμε
και στον quicksort, ο αλγόριθμος Find απαιτεί Ο(n2) χρόνο. Ευτυχώς όμως στη μέση
περίπτωση ο αλγόριθμος είναι κατά πολύ καλύτερος. Χρησιμοποιώντας τις ίδιες υποθέσεις
που κάναμε και στην ανάλυση του quicksosrt προκύπτει ότι η πιθανότητα το στοιχείο
διαχωρισμού για έναν πίνακα μεγέθους n να είναι το i-oστό μεγαλύτερο στοιχείο του είναι 1/n.
__
Έστω T (n, i) o μέσος χρόνος που κάνει ο Find για να βρει το i-oστό μεγαλύτερο στοιχείο
__ __
από n στοιχεία και T (n) = maxi T (n, i) ο μέγιστος χρόνος που χρειάζεται στη μέση
περίπτωση για οποιοδήποτε i, 1 ≤ i ≤ n. Ισχύουν οι παρακάτω σχέσεις:
i -1 n

∑ ∑
__ __ __
T (n,i) = cn + 1/n* [ T (n - k,i - k) + T (k - 1,i) ] για κάποια σταθερά c > 0
k =1 k =i +1
i-1 n-1

∑ ∑
__ __ __ __
T (n) = max T (n,i) ≤ Ο(n)+ 1/n * max[ T (n - k) + T (k) ]
i i
k=1 k=i

__
και μπορεί να αποδειχθεί με επαγωγή ότι T (n) ≤ 4cn = O(n).

Ο γραμμικός χρόνος στη μέση περίπτωση προκύπτει επειδή το μέσο μέγεθος των υποπινάκων
όπου εφαρμόζεται αναδρομικά ο Find είναι κλάσμα του n ενώ ο τετραγωνικός χρόνος
χειρότερης περίπτωσης όταν το μέγεθος των υποπινάκων είναι μεγάλο. Με καλύτερο
διαχωρισμό μπορούμε να πάρουμε γραμμικό χρόνο και στη χειρότερη περίπτωση.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 99


Αλγόριθμοι και Δομές Δεδομένων

3.3.2 Ο αλγόριθμος Select


O αλγόριθμος Select προτάθηκε από τους Blum, Floyd, Pratt, Lewis και Tarjan το 1973.
Διορθώνει την κακή συμπεριφορά του Find επιλέγοντας το στοιχείο διαχωρισμού μέσα από
ένα καλό δείγμα στοιχείων. Το δείγμα αυτό δημιουργείται χωρίζοντας τον πίνακα S σε ομάδες
για παράδειγμα των 5 στοιχείων, λαμβάνοντας απευθείας τους ενδιάμεσους κάθε ομάδας m1,
m2, …, mn/5 (πχ. με χρήση του insertionsort) και βρίσκοντας στο τέλος αναδρομικά τον
ενδιάμεσο m των n/5 ενδιάμεσων. Ο ενδιάμεσος αυτός είναι εγγυημένα ένα καλό στοιχείο
διαχωρισμού. Μετά το διαχωρισμό ο αλγόριθμος προχωρά αναδρομικά στους υποπίνακες
που παράγονται όπως στον αλγόριθμο Find. Για να αποφύγουμε το μεγάλο βάθος
αναδρομής, όταν φτάσουμε σε υποπίνακα με ≤ 100 στοιχεία τον ταξινομούμε και παίρνουμε
απευθείας τον i-oστό μεγαλύτερο.
H διαδικασία διαχωρισμού αποτυπώνεται στο επόμενο σχήμα:

Σχήμα 29: Διαχωρισμός σε πεντάδες. Τα στοιχεία του S1 είναι μικρότερα ή ίσα του m και τα
στοιχεία του S2 μεγαλύτερα του m

Τα στοιχεία που βρίσκονται μέσα στο λευκό ορθογώνιο είναι σίγουρα μεγαλύτερα από τον
m , άρα ανήκουν στο υποσύνολο S2 (τα στοιχεία αυτά είναι μεγαλύτερα από τον ενδιάμεσο
της στήλης όπου ανήκουν και ο ενδιάμεσος κάθε στήλης είναι μεγαλύτερος από τον m ).
Ομοίως, τα στοιχεία του γκρι ορθογωνίου είναι σίγουρα μικρότερα ή ίσα από τον m και
ανήκουν στο υποσύνολο S1. Tα S1, S2 περιέχουν πιθανώς και άλλα στοιχεία αλλά αυτά που
περικλείονται από τα ορθογώνια ξέρουμε με βεβαιότητα ότι ανήκουν στα δύο υποσύνολα.
Φράσσοντας το μέγιστο πλήθος στοιχείων που μπορεί να έχει κάθε ένα από τα S1, S2
μπορούμε να φράξουμε το μέγιστο χρόνο κάθε αναδρομικής κλήσης του αλγόριθμου Select.
Eπειδή |S1| + |S2| = n κάθε υποσύνολο θα περιέχει θα περιέχει τουλάχιστον 3n/10
στοιχεία αν το n διαιρείται ακριβώς με το 10 ή τουλάχιστον 3n/11 στοιχεία αν δεν διαιρείται
(τόσα είναι τα στοιχεία κάθε ορθογωνίου στο προηγούμενο σχήμα). Άρα, κάθε υποσύνολο θα
περιέχει το πολύ 8n/11 στοιχεία. Το πλήθος επίσης των πεντάδων που δημιουργούνται κατά
το διαχωρισμό είναι n/5 αν το n διαιρείται με το 5 ή το πολύ 21n/100 αν το n δεν διαιρείται. Αν
Τ(n) είναι ο μέγιστος χρόνος του αλγόριθμου Select για κάθε πίνακα μεγέθους n και κάθε i,
τότε υπάρχουν θετικές σταθερές a, b ώστε:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 100


Αλγόριθμοι και Δομές Δεδομένων

Mπορεί τώρα να αποδειχθεί με επαγωγή ότι Τ(n) ≤ cn = O(n) για c = max(a, 1100b/69).

Ερωτήσεις Κεφαλαίου
1. Σε ποιες κατηγορίες ταξινομούνται οι μέθοδοι ταξινόμησης; Ποια είναι η βασική τους
διαφορά;
2. Περιγράψτε τον αλγόριθμο Ταξινόμησης Φυσαλίδας. Δώστε τους χρόνους μέσης και
χειρότερης περίπτωσης.
3. Περιγράψτε τον αλγόριθμο Ταξινόμησης με Εισαγωγή. Δώστε τους χρόνους μέσης και
χειρότερης περίπτωσης.
4. Περιγράψτε τον αλγόριθμο Ταξινόμησης με Επιλογή. Δώστε τους χρόνους μέσης και
χειρότερης περίπτωσης.
5. Περιγράψτε τον αλγόριθμο Ταξινόμησης Σωρού. Δώστε τους χρόνους μέσης και
χειρότερης περίπτωσης.
6. Περιγράψτε τον αλγόριθμο Γρήγορης Ταξινόμησης. Δώστε τους χρόνους μέσης και
χειρότερης περίπτωσης.
7. Περιγράψτε τον αλγόριθμο Ταξινόμησης με Συγχώνευση. Δώστε τους χρόνους μέσης και
χειρότερης περίπτωσης.
8. Δώστε τα κάτω όρια στις πολυπλοκότητες μέσης και χειρότερης περίπτωσης των
συγκριτικών αλγόριθμων ταξινόμησης.
9. Περιγράψτε τον αλγόριθμο Ταξινόμησης με Μέτρηση. Δώστε την χρονική πολυπλοκότητα
του αλγόριθμου.
10. Περιγράψτε τον αλγόριθμο Radix Sort.
11. Περιγράψτε τον αλγόριθμο External Sorting. Δώστε την πολυπλοκότητα χρόνου του
αλγόριθμου.
12. Σε ποιες δύο κατηγορίες ταξινομούνται οι μέθοδοι αναζήτησης; Ποια η βασική τους
διαφορά;
13. Περιγράψτε τον αλγόριθμο γραμμικής αναζήτησης. Δώστε τους χρόνους μέσης και
χειρότερης περίπτωσης.
14. Περιγράψτε τον αλγόριθμο δυαδικής αναζήτησης. Δώστε τους χρόνους μέσης και
χειρότερης περίπτωσης.
15. Περιγράψτε τον αλγόριθμο αναζήτησης παρεμβολής. Δώστε τους χρόνους μέσης και
χειρότερης περίπτωσης. Κάντε μία σύγκριση με την μέθοδο δυαδικής αναζήτησης.
16. Περιγράψτε τον αλγόριθμο αναζήτησης κατά ομάδες. Δώστε τους χρόνους χειρότερης και
μέσης περίπτωσης. Εξηγείστε πώς μπορούν να βελτιωθούν αυτοί οι χρόνοι.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 101


Αλγόριθμοι και Δομές Δεδομένων

17. Περιγράψτε τον αλγόριθμο Find και δώστε τους χρόνους μέσης και χειρότερης
περίπτωσης.
18. Περιγράψτε τον γραμμικό αλγόριθμο Select.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 102


Αλγόριθμοι και Δομές Δεδομένων

ΚΕΦΑΛΑΙΟ 4: ΜΗ ΓΡΑΜΜΙΚΕΣ ΔΟΜΕΣ ΔΕΔΟΜΕΝΩΝ


Στις μη γραμμικές δομές οι σχέσεις μεταξύ των δεδομένων είναι σύνθετες (ιεραρχικές).
Σκοπός του κεφαλαίου είναι να παρουσιάσει τρεις βασικές μη γραμμικές δομές δεδομένων, τα
δέντρα, τους γράφους και τις δομές UNION-FIND. Για κάθε δομή περιγράφονται ενδεικτικές
εφαρμογές, αναλύεται η λειτουργία τους καθώς και οι πράξεις που υποστηρίζονται και στο
τέλος δίνονται οι αλγόριθμοι υλοποίησης των πράξεων.

4.1 Δένδρα
4.1.1 Γενική περιγραφή - Ορισμοί
Αντίθετα με τις λίστες,, σε ένα δένδρο από κάθε κόμβο δεν ξεκινά ένας μόνο δείκτης που
να δείχνει σε ένα κόμβο αλλά περισσότεροι οι οποίοι δείχνουν σε πολλούς κόμβους. Δηλαδή
τα δένδρα υλοποιούν σχέσεις ιεραρχικές μεταξύ των στοιχείων και άρα είναι δομές μη
γραμμικές. Οι σχέσεις που συνδέουν τα στοιχεία μεταξύ τους προσδιορίζουν κάθε φορά το
είδος του δένδρου και τις πράξεις που εκτελούνται σ’ αυτό. Επειδή ακριβώς η ιεραρχική
οργάνωση επιτρέπει ταχύτερη πρόσβαση στα δεδομένα, τα δένδρα είναι μία από τις πιο
αποτελεσματικές δομές για αποθήκευση και επεξεργασία δεδομένων και χρησιμοποιούνται
ευρύτατα σε κάθε είδος εφαρμογής διαχείρισης δεδομένων.
Ένα δένδρο μπορεί να παρασταθεί με ένα σύνολο κόμβων που συνδέονται με ακμές. Άρα
για οποιουσδήποτε δύο κόμβους υπάρχει ένα μοναδικό μονοπάτι που τους ενώνει. Η δομή
του ουσιαστικά έχει την μορφή ενός πραγματικού δέντρου και για το λόγο αυτό κάποια από τα
στοιχεία του ακολουθούν την γενική ονοματολογία δένδρων. Έτσι ο αρχικός κόμβος από τον
οποίο ξεκινούν ακμές αλλά δεν υπάρχει ακμή που να φτάνει σ’ αυτόν ονομάζεται ρίζα (root)
του δένδρου. Σε κάθε κόμβο καταλήγει μία ακμή ενώ από αυτόν ξεκινούν περισσότερες που
κάθε μία δείχνει σε έναν κόμβο. Οι κόμβοι στους οποίους καταλήγουν μόνο ακμές
ονομάζονται φύλλα (leaves) του δένδρου ενώ οι υπόλοιποι εσωτερικοί κόμβοι (internal
nodes) του δένδρου.
Παρακάτω δίνονται συνοπτικά οι ορισμοί των πιο συνηθισμένων εννοιών που
χρησιμοποιούνται στα δένδρα (κάποιους απ΄ αυτούς τους έχουμε δει ήδη στην παρουσίαση
του αλγόριθμου ταξινόμησης heapsort).
1. Πρόγονοι (ancestors) ενός κόμβου ή φύλλου είναι οι κόμβοι που βρίσκονται πάνω στο
μονοπάτι από τον κόμβο αυτό προς τη ρίζα.
2. Υπόδενδρο (subtree) ονομάζεται το δένδρο που σχηματίζεται, αν ως ρίζα ληφθεί ένας
οποιασδήποτε κόμβος.
3. Απόγονοι (ενός κόμβου είναι οι κόμβοι και τα φύλλα που βρίσκονται στο υπόδενδρο
κάτω από αυτόν.
4. Πατέρας (parent) ενός κόμβου ή ενός φύλλου είναι ο κοντινότερος πρόγονός του.
5. Παιδιά (childs) ενός κόμβου είναι οι απόγονοι του για τους οποίους είναι πατέρας.
6. Δύο κόμβοι με τον ίδιο πατέρα καλούνται αδέλφια (brothers).
7. Ο βαθμός (degree) ενός κόμβου είναι ίσος με το πλήθος των παιδιών του.
8. Μονοπάτι (path) δένδρου είναι η διαδρομή από τη ρίζα του δένδρου προς κάποιο φύλλο.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 103


Αλγόριθμοι και Δομές Δεδομένων

9. Το βάθος (depth) ενός κόμβου ή ενός φύλλου είναι ίσο με το μήκος (πλήθος ακμών) της
διαδρομής από αυτόν προς την ρίζα του δένδρου.
10. Το ύψος (height) ενός δένδρου είναι το μήκος του μακρύτερου μονοπατιού του δένδρου.
Ισούται με το βάθος του φύλλου με το μεγαλύτερο βάθος.
11. Επίπεδο (level) ενός κόμβου είναι ο αριθμός των προγόνων του μέχρι τη ρίζα συν 1.
12. Φυλλοπροσανατολιζόμενα (leaf-oriented) δένδρα καλούνται τα δένδρα στα οποία τα
δεδομένα είναι αποθηκευμένα μόνο στα φύλλα ενώ στους κόμβους αποθηκεύεται μόνο
βοηθητική πληροφορία.
13. Κομβοπροσανατολισμένα (node-oriented) δένδρα καλούνται τα δένδρα στα οποία τα
δεδομένα είναι αποθηκευμένα στα φύλλα και στους κόμβους.
14. κ-δικό καλείται το δένδρο όπου κάθε κόμβος έχει βαθμό το πολύ κ.

Οι προηγούμενοι ορισμοί επεξηγούνται στο δυαδικό δένδρο του επόμενου σχήματος:

Σχήμα 30: Επεξήγηση των βασικότερων εννοιών σε ένα (δυαδικό) δένδρο

Ανάλογα με τις πράξεις που υποστηρίζουν, τα δένδρα διακρίνονται σε 3 μεγάλες


κατηγορίες:
• τα στατικά (static) δέντρα όταν αυτά δεν επιδέχονται καμιά αλλαγή στην μορφή τους,
• τα ημι-δυναμικά (semi-dynamic) στα οποία επιτρέπεται η εισαγωγή αλλά όχι η
διαγραφή στοιχείων και,
• τα (πλήρως) δυναμικά (fully dynamic) στα οποία επιτρέπονται τόσο οι εισαγωγές
όσο και οι διαγραφές στοιχείων.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 104


Αλγόριθμοι και Δομές Δεδομένων

4.1.3 Δυαδικά δένδρα


Τα δυαδικά δένδρα είναι τα πλέον γνωστά στην επιστήμη των υπολογιστών. Μια
ενδιαφέρουσα εφαρμογή δυαδικού δένδρου είναι το προγονικό δένδρο (ancestor tree) που
αποτελεί μια παραλλαγή του γενεαλογικού δένδρου. Σε ένα προγονικό δένδρο, το αριστερό
παιδί ενός κόμβου περιέχει τον (φυσικό) πατέρα και το δεξί τη μητέρα του ατόμου που
αποθηκεύεται στον κόμβο αυτό, όπως φαίνεται στο επόμενο παράδειγμα:

Σχήμα 31: Παράδειγμα προγονικού δένδρου

Οι δυαδικές αριθμητικές εκφράσεις μπορούν να παρασταθούν πολύ φισικά με δυαδικά


δένδρα, όποτε προκύπτουν τα δένδρα εκφράσεων (expression trees) που
χρησιμοποιούνται στην αναπαράσταση αριθμητικών εκφράσεων. Κάθε εσωτερικός κόμβος
περιέχει έναν δυαδικό αριθμητικό τελεστή και κάθε φύλλο την τιμή μιας μεταβλητής ή
σταθεράς, δηλ. έναν τελεστέο της έκφρασης. Στο επόμενο σχήμα φαίνεται το δένδρο για την
έκφραση c^d/a+(e+f)*b η οποία είναι γραμμένη με την ένθετη (κανονική) μορφή:

Σχήμα 32: Δένδρο εκφράσεων για την παράσταση c^d/a+(e+f)*b

Τα δυαδικά δένδρα μπορούν επίσης να χρησιμοποιηθούν ως εργαλεία μοντελοποίησης και


αναπαράστασης γνώσης. Είδαμε στο κεφάλαιο της ταξινόμησης ότι τα δυαδικά δένδρα
αποφάσεων (binary decision trees) μπορούν να παραστήσουν πιθανές ακολουθίες από
ενέργειες με βάση το αποτέλεσμα μιας λογικής απόφασης. Τα δένδρα αποφάσεων, εκτός από
θεωρητικά εργαλεία, μπορούν να χρησιμοποιηθούν και σαν μηχανισμοί εξαγωγής
συμπερασμάτων (πχ. έμπειρα συστήματα – expert systems).

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 105


Αλγόριθμοι και Δομές Δεδομένων

4.1.4 Ιδιότητες δυαδικών δένδρων


Όταν σε κάθε επίπεδο το δένδρο περιέχει το μέγιστο βαθμό κόμβων καλείται πλήρες
(complete). Σε ένα πλήρες δυαδικό δένδρο ύψους h ισχύει:
▪ Το πλήθος των κόμβων που βρίσκονται στο επίπεδο k είναι 2k-1 (υπενθυμίζουμε ότι η
αρίθμηση των επιπέδων αρχίζει από το 1)
h
▪ Το πλήθος όλων των κόμβων του δένδρου είναι ∑ 2i = 2h+1-1 (Στη γενική περίπτωση
i =0
πλήρους δένδρου όπου κάθε κόμβος έχει βαθμό d, το πλήθος των κόμβων του είναι
h
∑ di = (dh+1-1)/(d-1).
i =0

▪ Αν το δένδρο έχει n συνολικά κόμβους (εσωτερικοί κόμβοι και φύλλα) τότε h = log(n+1) –
1.

Σε κάθε δυαδικό δένδρο όπου κάθε κόμβος έχει ακριβώς 2 παιδιά (εσωτερικός κόμβος) ή 0
παιδιά (φύλλο) ισχύει:
Πλήθος φύλλων = Πλήθος εσωτερικών κόμβων +1

4.1.5 Υλοποίηση δυαδικού δένδρου


Η υλοποίηση ενός δυαδικού δένδρου παρουσιάζει ομοιότητες με την υλοποίηση της
λίστας που είδαμε στο κεφάλαιο 2. Η βασική διαφορά είναι ότι κάθε κόμβος του δένδρου έχει
χώρο για τα δεδομένα και τις ακμές που τον συνδέουν με τα δύο παιδιά του – αριστερό και
δεξί. Επομένως, ενώ στη απλά συνδεδεμένη λίστα από κάθε κόμβο ξεκινούσε ένας δείκτης
προς τον επόμενο, στο δυαδικό δένδρο από κάθε κόμβο ξεκινούν δύο δείκτες (στη γενική
περίπτωση ενός κ-δικού δένδρου έχουμε κ τέτοιους δείκτες). Στα φύλλα του δένδρου οι δείκτες
έχουν την τιμή NULL (0).
Στον υπολογιστή, το δυαδικό δένδρο μπορεί να υλοποιηθεί είτε με τη χρήση πίνακα είτε με
χρήση εγγραφών.

Υλοποίηση με χρήση πίνακα


Η υλοποίηση με πίνακα χρησιμοποιεί έναν διδιάστατο πίνακα (ή ισοδύναμα με τρεις
μονοδιάστατους πίνακες). Κάθε γραμμή του πίνακα αντιστοιχεί σε ένα εσωτερικό κόμβο ή
φύλλο. Επομένως, αν h είναι το ύψος του δένδρου το συνολικό μέγεθος του πίνακα στα πλήρη
δυαδικά δένδρα είναι 2h+1-1. Σε κάθε μία γραμμή, έστω ότι αυτή αντιστοιχεί στον κόμβο u,
αποθηκεύεται τόσο το περιεχόμενο του κόμβου όσο και οι δείκτες στα παιδιά του. Οι δείκτες
αυτοί είναι οι γραμμές του πίνακα στις οποίες αποθηκεύονται κάθε ένα από τα παιδιά του
κόμβου u. Συνήθως στην πρώτη γραμμή του πίνακα αποθηκεύεται η ρίζα του δένδρου ή
αλλιώς χρησιμοποιείται μία μεταβλητή που να δείχνει τη θέση της ρίζας μέσα στον πίνακα.
Έστω το παρακάτω δυαδικό δένδρο:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 106


Αλγόριθμοι και Δομές Δεδομένων

B C

D E F G

Για την υλοποίησή του μπορεί να χρησιμοποιηθεί ο παρακάτω πίνακας:


Θέση Θέση Περιεχόμενο Θέση
Πίνακα Αριστερού Κόμβου Δεξιού
Παιδιού Παιδιού
1 2 A 3
2 4 B 5
3 6 C 7
4 0 D 0
5 0 E 0
6 0 F 0
7 0 G 0

Για τη σειρά αποθήκευσης των κόμβων του δένδρου στον πίνακα, αρχικά αυτοί
αριθμούνται από αριστερά προς τα δεξιά και από το επίπεδο 1 της ρίζας μέχρι το τελευταίο
επίπεδο των φύλλων του δένδρου. Έτσι, σε ένα πλήρες δυαδικό δένδρο ύψους h με πλήθος
κόμβων 2h+1-1 ισχύει για κάθε κόμβο i:

1. Ο πατέρας του βρίσκεται στη θέση └i/2┘ του πίνακα.


2. Το αριστερό παιδί του κόμβου i βρίσκεται στη θέση 2i, αν 2i≤n. Αλλιώς ο κόμβος i δεν έχει
αριστερό παιδί.
3. Το δεξί παιδί του κόμβου i βρίσκεται στη θέση 2i+1, αν 2i+1≤n. Αλλιώς ο κόμβος i δεν έχει
δεξί παιδί.
Υπενθυμίζουμε ότι τις σχέσεις αυτές τις έχουμε ήδη δει στην παρουσίαση του αλγόριθμου
ταξινόμησης heapsort όπου ο σωρός είναι ένα πλήρες δυαδικό δένδρο.

Υλοποίηση με τη χρήση εγγραφών και δεικτών


Στην περίπτωση αυτή κάθε κόμβος ή φύλλο ενός δυαδικού δένδρου αποθηκεύεται σαν μία
εγγραφή (δομή) η οποία περιέχει το περιεχόμενο του κόμβου και 2 δείκτες ως μεταβλητές (ή
έναν πίνακα δεικτών 2 θέσεων) που δείχνουν στα δύο παιδιά του. Παράλληλα
χρησιμοποιείται και μία μεταβλητή τύπου δείκτη που δείχνει την εγγραφή της ρίζας. Το
παρακάτω σχήμα δείχνει τον τυπικό κόμβο ενός δυαδικού δένδρου ακεραίων και τη δήλωση
της εγγραφής-κόμβου του δένδρου στη γλώσσα C (root είναι ο δείκτης στη ρίζα του δένδρου):

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 107


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 33: Δήλωση ενός δυαδικού δένδρου στη C

Η υλοποίηση με του δείκτες πλεονεκτεί από την αντίστοιχη με πίνακες αφού η δέσμευση
της απαραίτητης μνήμης γίνεται δυναμικά και ανάλογα με τις τρέχουσες απαιτήσεις. Στην
περίπτωση του πίνακα, όπως είδαμε και στην περίπτωση της λίστας, είναι δυνατό να γεμίσει
και να μην “χωράει” άλλα στοιχεία ή και να έχει λίγα στοιχεία ενώ έχει δεσμευτεί το σύνολο
του αποθηκευτικού χώρου (σπατάλη μνήμης).

4.1.6 Δυαδικά δένδρα αναζήτησης


Σε εφαρμογές διαχείρισης δεδομένων όπου έχουμε συχνά αναζητήσεις στοιχείων,
χρησιμοποιούμε τα δυαδικά δένδρα αναζήτησης (binary search trees) στα οποία η
αποθήκευση των δεδομένων στους κόμβους του δένδρου γίνεται με συγκεκριμένη σειρά: για
κάθε κόμβο του δένδρου που αποθηκεύει την τιμή x, το αριστερό του υπόδενδρο αποθηκεύει
στοιχεία με τιμές ≤ x και το δεξί του υπόδενδρο στοιχεία με τιμές > x, όπως φαίνεται
σχηματικά παρακάτω:

Σχήμα 34: Δυαδικό δένδρο αναζήτησης

Στο επόμενο σχήμα δίνονται η κομβοπροσανατολισμένη (αριστερά) και


φυλλοπροσανατολισμένη (δεξιά) έκδοση ενός δυαδικού δένδρου αναζήτησης για τα στοιχεία
1, 3, 5, 7, 9, 11, 13. Στο φυλλοπροσανατολισμένο δένδρο, αποθηκεύουμε σε κάθε εσωτερικό
κόμβο τη μεγαλύτερη από τις τιμές του αριστερού του υποδένδρου η οποία βρίσκεται στο
δεξιότερο φύλλο του υποδένδρου.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 108


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 35: Κομβοπροσανατολισμένο και φυλλοπροσανατολισμένο δυαδικό δένδρο


αναζήτησης για το ίδιο σύνολο στοιχείων

Ένα φυλλοπροσανατολισμένο δυαδικό δένδρο αναζήτησης μπορεί να δομηθεί με τέτοιο τρόπο


ώστε κάθε εσωτερικός του κόμβος να έχει ακριβώς 2 παιδιά. Αν το δένδρο αυτό αποθηκεύει n
στοιχεία (έχει δηλαδή n φύλλα), τότε έχει συνολικά 2n-1 κόμβους, δηλ. περίπου τους
διπλάσιους από ένα κομβοπροσανατολισμένο δυαδικό δένδρο αναζήτησης για τα ίδια
στοιχεία. Παρόλα αυτά τα φυλλοπροσανατολισμένα δένδρα οδηγούν γενικά σε κομψότερους
αλγόριθμους για τις βασικές πράξεις αναζήτησης, εισαγωγής και διαγραφής.

4.1.7 Μέθοδοι διαπέρασης δυαδικών δένδρων


Όπως στις περισσότερες δομές δεδομένων, έτσι και στα δένδρα χρειαζόμαστε έναν
συστηματικό τρόπο για να επισκεπτόμαστε όλους τους κόμβους ενός δένδρου. Ωστόσο, ενώ
στους πίνακες και στις διασυνδεδεμένες λίστες συνήθως χρησιμοποιούμε τις προφανείς
(φυσικές) διαπεράσεις (πχ. πλήρη σάρωση), στα δυαδικά δένδρα μπορούμε να ορίσουμε
περισσότερες από μία διαπεράσεις λόγω της μη γραμμικότητας στην αποθήκευση των
στοιχείων τους. Ο αναδρομικός ορισμός του δυαδικού δένδρου βοηθάει να ορίσουμε και τους
αλγόριθμους διαπέρασης αναδρομικά.
Στη γενική περίπτωση, όταν μία μέθοδος διαπέρασης εφαρμόζεται σε ένα δυαδικό
δένδρο, τότε είτε το δένδρο είναι άδειο και η διαπέραση δεν έχει αποτέλεσμα ή ο αλγόριθμος
εκτελείται αναδρομικά και στα δύο παιδιά της ρίζας. Ανάλογα με τη σειρά επίσκεψης της ρίζας
και κάθε υπόδενδρου ορίζονται τρεις διαφορετικές μέθοδοι διαπέρασης:
▪ Προδιάταξη (preorder): ο αλγόριθμος επισκέπτεται πρώτα τη ρίζα του δένδρου (1), μετά
το αριστερό (2) και τελευταίο το δεξί υπόδενδρο (3). Σχηματικά ο αλγόριθμος
απεικονίζεται παρακάτω (Τu είναι το υπόδενδρο με ρίζα u):

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 109


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 36: Αλγόριθμος preorder διαπέρασης

▪ Ενδοδιάταξη ή συμμετρική διάταξη (inorder ή symmetric order): ο αλγόριθμος


επισκέπτεται πρώτα το αριστερό υπόδενδρο (1), μετά τη ρίζα (2) και τελευταίο το δεξί (3).

Σχήμα 37: Αλγόριθμος inorder διαπέρασης

▪ Μεταδιάταξη (postorder): ο αλγόριθμος επισκέπτεται πρώτα το αριστερό υπόδενδρο (1),


μετά το δεξί (2) και τελευταία τη ρίζα (3).

Σχήμα 38: Αλγόριθμος postorder διαπέρασης

Παραδείγματα διαπεράσεων
Παράδειγμα 1: Εφαρμογή αλγόριθμων διαπέρασης

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 110


Αλγόριθμοι και Δομές Δεδομένων

Παράδειγμα 2: Μετατροπή αριθμητικής παράστασης


Αν εφαρμόσουμε τους αλγόριθμους διαπέρασης στο δένδρο εκφράσεων που είδαμε στην
παράγραφο 4.1.3 (Σχήμα 32) παίρνουμε τις εξής μορφές παράστασης:
Ιnorder: c^d/a+(e+f)*b
Preorder: +/^cda*+efb
Postorder cd^a/ef+b*+

δηλ. η διαπέραση του δένδρου εκφράσεων με την μέθοδο της προδιάταξης δίνει τον
πολωνικό συμβολισμό της έκφρασης στην προθεματική (prefix) μορφή, η διαπέραση με τη
μέθοδο της μεταδιάταξης δίνει την παράσταση γραμμένη σε μεταθετική (postfix) μορφή και η
συμμετρική διαπέραση την παράσταση στην κανονική ένθετη (infix) μορφή.

Παράδειγμα 3: Συμμετρική διαπέραση κομβοπροσανατολισμένου δυαδικού δένδρου


αναζήτησης

Στο δένδρο του παραπάνω σχήματος, η συμμετρική διαπέραση δίνει την ταξινομημένη
(κατ΄ αύξουσα σειρά) ακολουθία στοιχείων που αποθηκεύει.

Αλγόριθμοι διαπέρασης στη C


Δεδομένου ότι και οι τρεις μέθοδοι διαπέρασης είναι εξαιρετικά απλοί, στη συνέχεια
δίνουμε απευθείας τις αντίστοιχες C συναρτήσεις (υποθέτουμε ότι για το δένδρο ισχύουν οι
δηλώσεις της παραγράφου 4.1.5).

void Preorder(struct btnode *r)


{
if (r)
{
printf(“\t %d “, r->num);

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 111


Αλγόριθμοι και Δομές Δεδομένων

Preorder(r->left);
Preorder(r->right);
}
}

void Inorder(struct btnode *r)


{
if (r)
{
Inorder(r->left);
printf(“\t %d “, r->num);
Inorder(r->right);
}
}

void Postorder(struct btnode *r)


{
if (r)
{
Postorder(r->left);
Postorder(r->right);
printf(“\t %d “, r->num);
}
}

Και οι τρεις αλγόριθμοι διαπέρασης απαιτούν γραμμικό χρόνο αφού επισκέπτονται κάθε
κόμβο του δένδρου μόνο μία φορά (υποθέτουμε ότι η επίσκεψη ενός κόμβου κοστίζει Ο(1)
χρόνο). Πιο φορμαλιστικά, αν n είναι το μέγεθος του δένδρου (= πλήθος των κόμβων του) και
k το μέγεθος του αριστερού υποδένδρου της ρίζας, τότε ο χρόνος εκτέλεσης T(n)
οποιουδήποτε αλγόριθμου διαπέρασης είναι:
T(n) = O(1) + T(k) + T(n-k-1)
που έχει λύση Ο(n).

Ένα ενδιαφέρον σημείο για τη διαπέραση inorder είναι ότι μπορεί να χρησιμοποιηθεί για
να απαντήσει ερωτήσεις περιοχής (range queries) του τύπου «βρες τις τιμές του δένδρου που
ανήκουν σε ένα συγκεκριμένο διάστημα [x1..x2], είναι δηλ. ≥ x1 και ≤ x2 για δοσμένα x1, x2».
Δεδομένου ότι η inorder επιστρέφει τις τιμές που αποθηκεύει το δένδρο κατ’ αύξουσα σειρά,
βρίσκουμε αρχικά το μικρότερο στοιχείο που είναι ≥ x1 και στη συνέχεια με τη διαπέραση
τυπώνουμε ένα – ένα όλα τα στοιχεία του δένδρου μέχρι να συναντήσουμε το πρώτο
στοιχείο που είναι > x2. Οι αλλαγές που απαιτούνται στον παραπάνω κώδικα της συνάρτησης

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 112


Αλγόριθμοι και Δομές Δεδομένων

Inorder() είναι απλές (θα πρέπει να προστεθούν οι κατάλληλες if εντολές) και αφήνονται ως
άσκηση. Αν Α είναι το σύνολο των στοιχείων της απάντησης, τότε η διαδικασία αυτή εκτελείται
σε χρόνο Ο(h + |A|), όπου h το ύψος του δένδρου και |Α| το μέγεθος του συνόλου Α.

Παρατήρηση:
Οι μέθοδοι διαπέρασης preorder και postorder μπορούν να οριστούν και σε δένδρα
οποιουδήποτε βαθμού d > 2 ενώ η συμμετρική διαπέραση έχει εφαρμογή μόνο σε δυαδικά
δένδρα.

4.1.8 Βασικοί υπολογισμοί σε δυαδικά δένδρα


Ο αναδρομικός ορισμός των δένδρων επιτρέπει το σχεδιασμό κομψών αναδρομικών
αλγόριθμων για τον υπολογισμό διαφόρων μεγεθών ή την υλοποίηση πράξεων. Στη συνέχεια
δίνονται μερικοί τέτοιοι αλγόριθμοι στη γλώσσα C.

Ύψος δένδρου
Το ύψος (Height) ενός δυαδικού δένδρου T μπορεί να οριστεί αναδρομικά ως εξής:
Height(T) = -1, αν το Τ είναι άδειο
Height(T) = 1+max(Height(αριστερό υποδένδρο Τ), Height(δεξί υποδένδρο Τ))
Η παρακάτω συνάρτηση σε C επιστρέφει το ύψος του T:

int Height(struct btnode *r)


{
int lh, rh, max;

if (!r) return (-1);


lh = Height(r->left);
rh = Height(r->right);
max = (lh < rh ? rh : lh);
return (1+max);
}

Αν root είναι ο δείκτης στη ρίζα του Τ, τότε η Ηeight(root) επιστρέφει το ύψος του T. Eίναι
εύκολο να δει κανείς ότι, αν το Τ έχει n συνολικά κόμβους, τότε ο χρόνος εκτέλεσης της
Height() είναι Ο(n) αφού ο αλγόριθμος ξοδεύει σε κάθε κόμβο Ο(1) χρόνο για να υπολογίσει το
μέγιστο από τα ύψη των δύο υποδένδρων του.

Πλήθος κόμβων δένδρου


Η παρακάτω συνάρτηση επιστρέφει το πλήθος των κόμβων (εσωτερικών κόμβων και
φύλλων) ενός δυαδικού δένδρου:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 113


Αλγόριθμοι και Δομές Δεδομένων

int Nodes(struct btnode *r)


{
if (!r) return (0);
return (1+Nodes(r->left)+Nodes(r->right));
}
Και εδώ ο χρόνος εκτέλεσης είναι Ο(n). Προφανώς για τον ίδιο σκοπό θα μπορούσε να
χρησιμοποιηθεί και οποιοσδήποτε αλγόριθμος διαπέρασης με την κατάλληλη τροποποίηση
ώστε να επιστρέφει τη ζητούμενη τιμή.

Πλήθος φύλλων δένδρου


Η παρακάτω συνάρτηση επιστρέφει το πλήθος των φύλλων ενός δυαδικού δένδρου.
Yπενθυμίζουμε ότι ένα φύλλο δεν έχει κανένα παιδί.

int Leaves(struct btnode *r)


{
if (!r) return (0);
if (!r->left && !r->right) return (1);
return (Leaves(r->left) + Leaves(r->right));
}

Ομοίως, ο χρόνος εκτέλεσης της Leaves() είναι Ο(n).

Ελάχιστο και μέγιστο στοιχείο δυαδικού δένδρου αναζήτησης


Οι συναρτήσεις που δίνονται στη συνέχεια βρίσκουν και επιστρέφουν ένα δείκτη στον
κόμβο με την ελάχιστη και μέγιστη τιμή (τιμή πεδίου num) που είναι αποθηκευμένες στο
δένδρο T αντίστοιχα. Για τον κόμβο με την ελάχιστη τιμή η αναζήτηση ξεκινά από τη ρίζα και
προχωρά μόνο προς τα αριστερά (ακολουθώντας τους δείκτες προς το αριστερό παιδί κάθε
φορά) μέχρι να φτάσουμε σε άδειο δένδρο. Κατ΄ αντιστοιχία, για να βρούμε τον κόμβο με τη
μέγιστη τιμή προχωρούμε μόνο δεξιά μέχρι να φτάσουμε σε άδειο δένδρο. Για ένα άδειο
αρχικά δένδρο κάθε συνάρτηση επιστρέφει την τιμή ΝULL.

struct btnode *Min(struct btnode *r)


{
if (!r) return (NULL);
while (r->left)
r = r->left;
return (r);

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 114


Αλγόριθμοι και Δομές Δεδομένων

struct btnode *Max(struct btnode *r)


{
if (!r) return (NULL);
while (r->right)
r = r->right;
return (r);
}

Aν h είναι το ύψος του T τότε ο χρόνος εκτέλεσης κάθε συνάρτησης είναι O(h) αφού η
αναζήτηση γίνεται κατά μήκος ενός μονοπατιού.

Δημιουργία ισοζυγισμένου κομβοπροσανατολισμένου δυαδικού δένδρου


αναζήτησης
Έστω ότι θέλουμε να κατασκευάσουμε ένα ισοζυγισμένο κομβοπροσανατολισμένο
δυαδικό δένδρο αναζήτησης Τ με n στοιχεία. Υποθέτουμε ότι τα στοιχεία αυτά είναι ακέραιοι
που βρίσκονται αποθηκευμένοι κατ΄ αύξουσα σειρά στον πίνακα S[1..n]. H παρακάτω
συνάρτηση Balanced_tree() εφαρμόζει την τεχνική «διαίρει και βασίλευε» και κατασκευάζει se
O(n) χρόνο ένα τέτοιο δένδρο όπου το μήκος κάθε μονοπατιού από τη ρίζα σε ένα φύλλο του Τ
είναι ≈ logn. Aν υποθέσουμε για παράδειγμα ότι n=2k-1, τότε η Balanced_tree() κτίζει ένα
πλήρες δυαδικό δένδρο αναζήτησης ύψους k-1.

struct btnode *Balanced_tree(int l, int r)


{
struct btnode *t;
int m;

if (l > r) return (NULL);


t = (struct btnode *) malloc(sizeof(struct btnode));
m = (l+r)/2;
t->num = S[m];
t->left = Balanced_tree(l, m-1);
t->right = Balanced_tree(m+1, r);
return (t);
}

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 115


Αλγόριθμοι και Δομές Δεδομένων

Οι δύο παράμετροι της συνάρτησης είναι το αριστερό και δεξί όριο του πίνακα S
αντίστοιχα ενώ η αρχική κλήση είναι root = Balanced_tree(1, n) (υποθέτουμε ότι ο πίνακας S
έχει δηλωθεί ως σφαιρική – global – μεταβλητή στο πρόγραμμα).
To δένδρο Τ μπορεί να κατασκευαστεί σε γραμμικό χρόνο. Πράγματι, αν T(n) είναι ο
χρόνος εκτέλεσης της Balanced_tree(), τότε:
T(n) = O(1) + 2Τ(n/2) = O(n)

Απεικόνιση δένδρου
Η επόμενη συνάρτηση «ζωγραφίζει» ένα δυαδικό δένδρο Τ τυπώνοντας τα στοιχεία που
αποθηκεύει στους κόμβους του κατά γραμμές ξεκινώντας από το δεξιότερο κόμβο: τα στοιχεία
των κόμβων που βρίσκονται πιο βαθιά στο δένδρο τυπώνονται δεξιότερα στην οθόνη.

void Print_tree(struct btnode *r, int k)


/* η παράμετρος k μετράει τα κενά για την εκτύπωση */
{
int i;

if (r)
{
Print_tree (r->right, k+1);
for (i=1; i<=k; i++)
printf(“ “);
printf(“ %d \n“, r->num);
Print_tree(r->left, k+1);
}
}

Στο επόμενο σχήμα φαίνεται η έξοδος της συνάρτησης για ένα κομβοπροσανατολισμένο
και ένα φυλλοπροσανατολισμένο δυαδικό δένδρο αναζήτησης αντίστοιχα που αποθηκεύει τα
στοιχεία 1, 2, 3, 5, 6, 8, 12 (η συνάρτηση τυπώνει μόνο τους αριθμούς, οι γκρι γραμμές έχουν
προστεθεί για να δείξουν καλύτερα την τοπολογία του δένδρου).

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 116


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 39: Η έξοδος της Print_tree() για ένα κομβοπροσανατολισμένο και το αντίστοιχο
φυλλοπροσανατολισμένο δυαδικό δένδρο αναζήτησης

4.1.9 Νηματοειδή δένδρα


Όπως είδαμε στα προηγούμενα στα δυαδικά δένδρα, ένας ή και οι δύο δείκτες ενός κόμβου
μπορεί να έχουν την τιμή NULL (σε ένα φύλλο και οι δύο δείκτες έχουν την τιμή NULL).
Μπορούμε λοιπόν να εκμεταλλευτούμε τις τιμές των δεικτών αυτών και να κάνουμε πιο εύκολη
τη διέλευση μέσω των κόμβων του δένδρου. Είναι δυνατή η δεικτοδότηση από τους κόμβους
αυτούς σε άλλους κόμβους του δένδρου μέσω ακμών που ονομάζονται νήματα (threads).
Έτσι ο δεξιός δείκτης κάθε κόμβου που έχει τιμή NULL μπορεί να δείχνει στον επόμενο κόμβο
με βάση τη συμμετρική διάταξη. Το δένδρο που προκύπτει λέγεται δεξιό ενδονηματοειδές
(right in-threaded) και ένα παράδειγμα φαίνεται στο επόμενο σχήμα:

Σχήμα 40: Παράδειγμα δεξιού ενδονηματοειδούς δυαδικού δένδρου

Με ανάλογο τρόπο ορίζεται και το αριστερό ενδονηματοειδές δένδρο στο οποίο η


δεικτοδότηση γίνεται μέσω του αριστερού κόμβου o oποίος μπορεί να δείχνει στον
προηγούμενο με βάση τη συμμετρική διάταξη κόμβο. Φυσικά, μπορούμε να υλοποιήσουμε
ταυτόχρονα και τις δύο μορφές, οπότε το δένδρο καλείται απλά ενδονηματοειδές.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 117


Αλγόριθμοι και Δομές Δεδομένων

Το πρόβλημα στα νηματοειδή δένδρα είναι πώς θα γνωρίζουμε αν ένας δείκτης είναι
κανονικός δείκτης ή δείκτης-νήμα. Η επίλυσή του είναι απλή αν στη δήλωση του κόμβου ενός
δένδρου χρησιμοποιήσουμε για κάθε δείκτη ένα ακόμη πεδίο που κρατάει το τύπο του
(κανονικός ή νήμα).
Το τελευταίο ερώτημα που θα μας απασχολήσει είναι πώς βρίσκουμε τον κόμβο που
προηγείται ή έπεται ενός δοσμένου κόμβου p σύμφωνα με τη συμμετρική διάταξη. Όπως θα
δούμε σε λίγο, η αναζήτηση του κόμβου αυτού είναι πιθανόν να απαιτήσει την επίσκεψη
κόμβων που βρίσκονται ψηλότερα από τον p στο δένδρο. Αυτό σημαίνει ότι από κάθε κόμβο
πρέπει να έχουμε πρόσβαση στον πατέρα του. Για το σκοπό αυτό επεκτείνουμε τον ορισμό
του δένδρου προσθέτοντας σε κάθε κόμβο του κι ένα πεδίο parent που είναι ο δείκτης στον
πατέρα του. Οι αντίστοιχες δηλώσεις στη C είναι οι εξής:

struct btnode {
int num;
struct btnode *left, *right, *parent;
};

struct btnode *root;

Εύρεση επόμενου κατά τη συμμετρική διάταξη


Έστω το δυαδικό δένδρο αναζήτησης T, όπως ορίστηκε παραπάνω και θέλουμε να
βρούμε τον κόμβο που έπεται του p (successor(p)) στη συμμετρική διάταξη. Διακρίνουμε δύο
περιπτώσεις:
α) Αν ο p έχει μη κενό δεξί υπόδενδρο (δηλ. p->right ≠ NULL) τότε ο επόμενος του p είναι
ο αριστερότερος κόμβος αυτού του δεξιού υπόδενδρου. Παρατηρείστε ότι η τιμή που
αποθηκεύει ο εν λόγω κόμβος είναι η μικρότερη σ΄ αυτό το δεξί υπόδενδρο κατά τη συμμετρική
διάταξη.
β) Διαφορετικά, ανεβαίνουμε το μονοπάτι από τον p προς τη ρίζα του Τ μέσω των
δεικτών parent. Ο πρώτος κόμβος που θα συναντήσουμε ανεβαίνοντας από αριστερό παιδί
είναι και ο επόμενος του p.
Οι δύο περιπτώσεις απεικονίζονται σχηματικά παρακάτω:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 118


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 41: Εύρεση του successor(p)

Προσέξτε ότι στην περίπτωση β) είναι δυνατόν, καθώς ανεβαίνουμε το μονοπάτι, να φτάσουμε
στη ρίζα του Τ διαμέσου μιας διαδρομής δεξιών μόνο παιδιών. Στην περίπτωση αυτή ο p
αποθηκεύει τη μεγαλύτερη τιμή του T κι επομένως δεν υπάρχει ο successor(p) και η αντίστοιχη
συνάρτηση επιστρέφει τιμή NULL.
Στη συνέχεια δίνεται η συνάρτηση successor (p) σε C:

struct btnode *Successor(struct btnode *p)


{
struct btnode *q;

if (p->right)
{
p = p->right;
while (p->left)
p = p->left;
return (p);
}
q = p->parent;
while (q && p == q->right)
{
p = q;
q = p->parent;
}
return (q);
}

An h είναι το ύψος του Τ, ο χρόνος εκτέλεσης της successor(p) είναι Ο(h).

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 119


Αλγόριθμοι και Δομές Δεδομένων

Εύρεση προηγούμενου κατά τη συμμετρική διάταξη


H διαδικασία εύρεσης του κόμβου που προηγείται του p (predecessor(p)) στη συμμετρική
διάταξη είναι εντελώς συμμετρική. Οι δύο περιπτώσεις είναι τώρα οι εξής:
α) Αν ο p έχει μη κενό αριστερό υπόδενδρο (δηλ. p->left ≠ NULL) τότε ο επόμενος του p
είναι ο δεξιότερος κόμβος αυτού του αριστερού υπόδενδρου ο οποίος και αποθηκεύει τη
μεγαλύτερη τιμή του υπόδενδρου.
β) Διαφορετικά, ανεβαίνουμε το μονοπάτι από τον p προς τη ρίζα του Τ μέσω των
δεικτών parent. Ο πρώτος κόμβος που θα συναντήσουμε ανεβαίνοντας από δεξί παιδί είναι
και ο προηγούμενος του p.
Οι δύο περιπτώσεις απεικονίζονται σχηματικά παρακάτω:

Σχήμα 42: Εύρεση του predecessor(p)

Και εδώ είναι δυνατόν ανεβαίνοντας το μονοπάτι να φτάσουμε στη ρίζα του Τ διαμέσου μιας
διαδρομής αριστερών μόνο παιδιών, δηλ. ο p αποθηκεύει τη μικρότερη τιμή του T και άρα
predecessor(p) = NULL.
Ακολουθεί η συνάρτηση predecessor (p) σε C η οποία ομοίως εκτελείται σε χρόνο Ο(h):

struct btnode *Predecessor(struct btnode *p)


{
struct btnode *q;

if (p->left)
{
p = p->left;
while (p->right)
p = p->right;

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 120


Αλγόριθμοι και Δομές Δεδομένων

return (p);
}
q = p->parent;
while (q && p == q->left)
{
p = q;
q = p->parent;
}
return (q);
}

Κλείνοντας τη σύντομη αυτή παρουσίαση για τα νηματοδειδή δένδρα, αξίζει να αναφερθεί


ότι η δεικτοδότηση μπορεί να ακολουθεί και διαφορετική διάταξη από τη συμμετρική, για
παράδειγμα preorder ή postorder oπότε προκύπτουν τα προνηματοδειδή και μετανηματοειδή
δένδρα αντίστοιχα.

4.1.10 Αλγόριθμοι βασικών πράξεων σε κομβοπροσανατολισμένα δυαδικά


δένδρα αναζήτησης
Οι βασικές πράξεις που χρησιμοποιούνται στα δένδρα, γνωστές στη βιβλιογραφία ως
πράξεις του λεξικού (dictionary), είναι οι ακόλουθες:
1. Αναζήτηση (search) στοιχείου
2. Εισαγωγή (insertion) στοιχείου
3. Διαγραφή (deletion) στοιχείου
Η πράξη της αναζήτησης είναι στατική πράξη ενώ οι πράξεις της εισαγωγής και διαγραφής
είναι δυναμικές με την έννοια ότι καταστρέφουν τη προηγούμενη έκδοση του συνόλου των
στοιχείων που αποθηκεύει το δένδρο και παράγουν ένα νέο σύνολο.
Στην παράγραφο αυτή μελετώνται και αναλύονται οι αλγόριθμοι υλοποίησης των
παραπάνω πράξεων. Yποθέτουμε ότι έχουμε ένα κομβοπροσανατολισμένο δυαδικό δένδρο
αναζήτησης Τ που αποθηκεύει ακέραιους αριθμούς για τον ορισμό του οποίου
χρησιμοποιούμε τις δηλώσεις του κεφ. 4.1.5:
struct btnode {
int num;
struct btnode *left, *right;
};

struct btnode *root;

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 121


Αλγόριθμοι και Δομές Δεδομένων

Η πράξη Search()
Αρχικά θα μελετήσουμε την πράξη της αναζήτησης. Για να βρούμε το στοιχείο x στο
δένδρο ακολουθούμε την ιδέα της δυαδικής αναζήτησης ως εξής (υποθέτουμε ότι όλα τα
στοιχεία που αποθηκεύει το δένδρο είναι διαφορετικά μεταξύ τους):
Αρχικά προσπελαύνουμε το πρώτο στοιχείο του δένδρου που είναι η ρίζα και συγκρίνουμε
το x με το περιεχόμενο της ρίζας. Αν το x είναι μικρότερο συνεχίζουμε την αναζήτηση στο
αριστερό υπόδενδρο της ρίζας. Αν είναι μεγαλύτερο συνεχίζουμε την αναζήτηση στο δεξί
υπόδενδρο. Η διαδικασία τερματίζεται είτε μόλις βρούμε τον κόμβο με το στοιχείο x ή
φτάσουμε σε άδειο δένδρο (δηλ. βρισκόμαστε σε κάποιον κόμβο και προσπελαύνουμε το
δείκτη ενός παιδιού του που είναι NULL).

O παραπάνω αλγόριθμος μπορεί να παρασταθεί σχηματικά ως εξής:

Σχήμα 43: Λειτουργία αλγόριθμου Search()

Παρότι ο αλγόριθμος μπορεί να υλοποιηθεί αναδρομικά, στη συνέχεια παραθέτουμε την


επαναλητική εκδοχή για λόγους ευκολότερης κατανόησης.
Ο αλγόριθμος καλείται με ορίσματα ένα δείκτη στη ρίζα r του τρέχοντος δένδρου και το
στοιχείο αναζήτησης x. Ως έξοδο επιστρέφει τον δείκτη στο κόμβο-ρίζα του νέου δένδρου μετά
την εισαγωγή. Χρησιμοποιεί τη βοηθητική μεταβλητή δείκτη current_node που δείχνει στον
τρέχοντα κόμβο όπου αναζητούμε το x. Ως έξοδο επιστρέφει το δείκτη στον κόμβο του
δένδρου που περιέχει το x ή την τιμή NULL αν το x δεν υπάρχει στο δένδρο.

Aλγόριθμος Search(r, x)
/* r είναι o δείκτης στη ρίζα του τρέχοντος δένδρου */
Δεδομένα // r, current_node: δείκτης σε struct btnode; x ακέραιος //
Aρχή
current_node = r
Όσο (current_node <> NULL) Εκτέλεσε
Αν (x = current_node->num) Τότε
επέστρεψε (current_node)
Αλλιώς Αν (x < current_node->num) Τότε
current_node = current_node->left
Αλλιώς current_node = current_node->right

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 122


Αλγόριθμοι και Δομές Δεδομένων

Tέλος Αν
Τέλος Αν
Τέλος Όσο
επέστρεψε (NULL)
Αποτελέσματα // Ο κόμβος που δείχνει η current_node αν το x υπάρχει στο δένδρο
και ΝULL διαφορετικά //
Τέλος Search

Η πράξη Ιnsert()
Η εισαγωγή του στοιχείου x στο δένδρο γίνεται ακολουθώντας τον παρακάτω αναδρομικό
αλγόριθμο:
Συγκρίνουμε το x με το περιεχόμενο της ρίζας. Αν το x είναι μικρότερο εκτελούμε αναδρομικά
την εισαγωγή στο αριστερό υπόδενδρο της ρίζας ενώ αν είναι μεγαλύτερο στο δεξί. Αν
φτάσουμε σε άδειο δένδρο τότε δημιουργούμε ένα νέο φύλλο με τιμή το x και ο αλγόριθμος
τερματίζεται.

Ο αλγόριθμος καλείται με ορίσματα ένα δείκτη στη ρίζα r του τρέχοντος δένδρου και το
στοιχείο εισαγωγής x. Ως έξοδο επιστρέφει τον δείκτη στο κόμβο-ρίζα του νέου δένδρου μετά
την εισαγωγή.

Aλγόριθμος Insert(r, x)
/* r είναι o δείκτης στη ρίζα του τρέχοντος δένδρου */
Δεδομένα // r: δείκτης σε struct btnode; x: ακέραιος //
Aρχή
Aν (r = NULL) Τότε /* Άδειο δένδρο */
Δέσμευσε χώρο για τον κόμβο r
r->num = x
r->left = NULL
r->right = NULL
Aλλιώς Αν (x < r ->num) Τότε /* Eισαγωγή του x στο αριστερό υπόδενδρο */
r->left = Insert(r->left, x)
Αλλιώς Aν (x > r ->num) Τότε /* Εισαγωγή του x στο δεξί υπόδενδρο */
r->right = Insert(r->right, x)
Tέλος Αν
Tέλος Αν
Τέλος Αν
επέστρεψε (r)
Αποτελέσματα // Δείκτης στη ρίζα του νέου δένδρου μετά την εισαγωγή του x //
Τέλος Insert

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 123


Αλγόριθμοι και Δομές Δεδομένων

Ο αλγόριθμος εξετάζει καταρχήν αν ο κόμβος r είναι NULL ή όχι. Αν ναι, τότε δεσμεύεται
δυναμικά χώρος στη μνήμη για το δείκτη r για τη δημιουργία του νέου κόμβου στον οποίο θα
αποθηκευτεί η τιμή x ενώ οι δείκτες r->left και r->right τίθενται NULL.
Στην περίπτωση που r <> NULL τότε η εισαγωγή του x θα γίνει είτε στο αριστερό υπόδενδρο
του r αν x < r->num (αναδρομική κλήση Insert(r->left, x)) ή στο δεξί υπόδενδρο αν x > r->num
(αναδρομική κλήση Insert(r ->right, x)) ενώ ενημερώνεται ταυτόχρονα ο δείκτης r->left ή r-
>right αντίστοιχα.
Σε κάθε περίπτωση ο αλγόριθμος επιστρέφει το δείκτη στη ρίζα του νέου δένδρου που
δημιουργείται μετά την εισαγωγή.
Η κλήση της Insert() έχει τη μορφή
root = insert(root, x)
ενώ αρχικά το δένδρο είναι άδειο και root = NULL. Προσέξτε ότι αν η τιμή x υπάρχει ήδη στο
δένδρο ο αλγόριθμος δεν το ξαναεισάγει.

Η πράξη Delete()
Η πράξη της διαγραφής ενός στοιχείου x από το δένδρο είναι λίγο πιο πολύπλοκη.
Υποθέτουμε ότι το x υπάρχει στο δένδρο. Ο αλγόριθμος είναι επίσης αναδρομικός και σε
πρώτο επίπεδο μπορεί να περιγραφεί ως εξής:
Συγκρίνουμε το x με το περιεχόμενο της ρίζας. Αν το x είναι μικρότερο τότε το διαγράφουμε
αναδρομικά από το αριστερό υπόδενδρο της ρίζας. Αν το x είναι μεγαλύτερο, τότε το
διαγράφουμε αναδρομικά από το δεξί υπόδενδρο. Αν το x είναι ίσο με το περιεχόμενο της
ρίζας, τότε διακρίνουμε τις εξής περιπτώσεις:
Έστω v ο κόμβος που περιέχει την τιμή x.
α) Aν ο v είναι φύλλο, το διαγράφουμε και ο αλγόριθμος τερματίζεται.
β) Αν ο v έχει ένα μόνο παιδί, έστω w, αντικαθιστούμε τον v με τον w και ο αλγόριθμος
τερματίζεται.
γ) Αν ο v έχει δύο παιδιά, έστω u το δεξί παιδί του. Bρίσκουμε τη μικρότερη τιμή στο
υπόδενδρο με ρίζα τον u (Tu), έστω στον κόμβο z και τη βάζουμε στον v. Στη συνέχεια
διαγράφουμε αναδρομικά τον z όπως στις περιπτώσεις α) και β).

Στο επόμενο σχήμα απεικονίζονται οι περιπτώσεις τρεις περιπτώσεις α), β), γ):

Σχήμα 44: Λειτουργία αλγόριθμου Delete()

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 124


Αλγόριθμοι και Δομές Δεδομένων

Σημείωση: Αν στην περίπτωση γ) επιλέξουμε να αντικαταστήσουμε το περιεχόμενο του v με


την μεγαλύτερη τιμή του αριστερού του υποδένδρου αντί με τη μικρότερη του δεξιού του
υπόδενδρου και διαγράψουμε μετά την τιμή αυτή, ο αλγόριθμος εξακολουθεί να λειτουργεί
σωστά δεδομένου ικανοποιείται πάλι η συμμετρική διάταξη στο δένδρο (η τιμή που
μεταφέρουμε στον κόμβο v είναι η αμέσως επόμενη ή προηγούμενη του x στη συμμετρική
διάταξη αντίστοιχα).

Ο αλγόριθμος παίρνει ως όρισμα τη ρίζα του τρέχοντος υποδένδρου όπου αναζητούμε το


x κάθε φορά και επιστρέφει ένα δείκτη στη ρίζα του νέου δένδρου μετά τη διαγραφή:
Aλγόριθμος Delete(r, x)
/* r είναι o δείκτης στη ρίζα του τρέχοντος δένδρου */
Δεδομένα // r, p: δείκτες σε struct btnode; x: ακέραιος //
Aρχή
Αν (r->num = x) Τότε
Αν (r->left = r->right) Τότε /* Διαγράφουμε φύλλο */
αποδέσμευσε το χώρο του κόμβου που δείχνει ο r
επέστρεψε (ΝULL) /* Άδειο δένδρο */
Αλλιώς Αν (r->left = NULL) Τότε /* Ο κόμβος έχει μόνο δεξί παιδί */
p = r->right
αποδέσμευσε το χώρο του κόμβου που δείχνει ο r
επέστρεψε (p)
Αλλιώς Αν (r->right = NULL) Τότε /* O κόμβος έχει μόνο αριστερό παιδί */
p = r->left
αποδέσμευσε το χώρο του κόμβου που δείχνει ο r
επέστρεψε (p)
Αλλιώς /* Ο κόμβος έχει δύο παιδιά */
p = r->right
Όσο (p->left <> ΝULL) Εκτέλεσε
p = p->left
Τέλος Όσο
r->num = p->num
r->right = Delete(r->right, r->num)
επέστρεψε (r)
Tέλος Αν

Τέλος Αν
Τέλος Αν
Αλλιώς Αν (x < r->num) Τότε r->left = Delete(r->left, x)
Aλλιώς r->right = Delete(r->right, x)
Τέλος Αν
επέστρεψε (r)
Τέλος Αν
Αποτελέσματα // Δείκτης στο νέο δένδρο //
Τέλος Delete

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 125


Αλγόριθμοι και Δομές Δεδομένων

Όπως φαίνεται παραπάνω, μόλις βρούμε τον κόμβο που περιέχει το x εξετάζουμε ποια
από τις περιπτώσεις α), β) και γ) που είδαμε νωρίτερα ισχύει:
Περίπτωση α): Ο κόμβος r που επιθυμούμε να διαγράψουμε είναι φύλλο, γεγονός που
σημαίνει ότι και οι δύο δείκτες του r->left και r->right είναι NULL. Τότε σβήνεται ο r και
επιστρέφεται η τιμή NULL στον πατέρα του για να ενημερωθεί κατάλληλα ο αντίστοιχος
αριστερός ή δεξιός δείκτης του.
Περίπτωση β): Ο κόμβος διαγραφής r είναι εσωτερικός κόμβος και έχει ένα μόνο παιδί. Τότε ο
r σβήνεται αποδεσμεύοντας το χώρο που κατείχε και επιστρέφεται στον πατέρα του ο δείκτης
προς το παιδί του (στον αλγόριθμο είναι ο p) για την ενημέρωση.
Περίπτωση γ): Ο κόμβος διαγραφής r έχει δύο παιδιά. Tότε ο αλγόριθμος βρίσκει πρώτα την
μικρότερη τιμή στο δεξί υπόδενδρο του r προχωρώντας προς τα αριστερά (εντολή p = p->left
στο while loop του ψευδοκώδικα), στη συνέχεια αποθηκεύει την τιμή αυτή (p->num) στον
κόμβο r και σβήνει αναδρομικά από το δεξί υπόδενδρο του r την τιμή (αναδρομική κλήση
Delete(r->right, r->num)) ενημερώνοντας παράλληλα το δεξί δείκτη του r να δείχνει στο
υπόδενδρο που προέκυψε από τη διαγραφή.

Aνάλυση της συμπεριφοράς των δυαδικών δένδρων αναζήτησης


Και οι τρεις βασικές πράξεις που είδαμε νωρίτερα κοστίζουν Ο(ύψος του δένδρου) χρόνο
αφού επισκέπτονται τους κόμβους ενός μονοπατιού από τη ρίζα μέχρι κάποιο φύλλο του
δένδρου. Το ύψος αυτό μπορεί να γίνει γραμμικό στο πλήθος των στοιχείων και το δένδρο να
εκφυλιστεί σε μία λίστα επειδή ούτε η Insert() oύτε η Delete() μπορούν να εγγυηθούν ότι τα
υπόδενδρα κάθε κόμβου θα έχουν περίπου το ίδιο μέγεθος.
Aν για παράδειγμα εκτελέσουμε n διαδοχικές πράξεις Insert() σε μια αύξουσα
ταξινομημένη ακολουθία στοιχείων τότε το δένδρο που προκύπτει έχει την παρακάτω μορφή
(όταν η ακολουθία είναι φθίνουσα το δένδρο γέρνει από την αριστερή πλευρά):

Έτσι η πολυπλοκότητα χειρότερης περίπτωσης για την αναζήτηση σε ένα δυαδικό δένδρο
δεν είναι καλύτερη από την αντίστοιχη για την αναζήτηση σε έναν πίνακα ή μία λίστα όπου η
σειρά των στοιχείων είναι τυχαία.
Αν ξέρουμε τα στοιχεία από την αρχή, τότε μπορούμε να λύσουμε το προηγούμενο
πρόβλημα κτίζοντας ένα ισοζυγισμένο δένδρο με τη βοήθεια της συνάρτησης Balanced_tree(l,
r) που περιγράψαμε στο κεφ. 4.1.9 όπου το δένδρο έχει ύψος ≈ logn αν n=r-l+1 και η
κατασκευή του κοστίζει χρόνο Ο(n).
Όμως και πάλι δεν λύσαμε το πρόβλημα πλήρως. Η συνάρτηση Balanced_tree() απαιτεί
να ξέρουμε από την αρχή όλα τα στοιχεία και είναι καθαρά στατικό. Κάθε εκτέλεση μιας
πράξης Insert() ή Delete() μπορεί να χαλάσει τη ζύγιση του δένδρου και να κάνει ακριβές τις

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 126


Αλγόριθμοι και Δομές Δεδομένων

επόμενες πράξεις. Παρακάτω, στην παράγραφο με τα ισοζυγισμένα δένδρα θα δούμε μια


κομψή και αποτελεσματική λύση σ΄ αυτό το πρόβλημα.
Το γραμμικό ύψος σε ένα δυαδικό δένδρο αναζήτησης οφείλεται κύρια στο γεγονός ότι οι
πράξεις Insert() και Delete() εκτελούνται σε μια κακή ακολουθία στοιχείων (προσπαθήστε να
δείτε την αναλογία με την κακή συμπεριφορά του quicksort). Oι κακές όμως ακολουθίες δεν
είναι πάντα ο κανόνας κι αυτό υποδεικνύει ότι ίσως να πρέπει να εξετάσουμε τη μέση
συμπεριφορά ενός δυαδικού δένδρου αναζήτησης για να αποκομίσουμε μια πιο ρεαλιστική
εικόνα για την αποδοτικότητά του.
Έστω λοιπόν T ένα κομβοπροσανατολισμένο δυαδικό δένδρο αναζήτησης που έχει
κτιστεί με διαδοχικές εκτελέσεις της πράξεις Insert() πάνω σε μια (ψευδο) τυχαία ακολουθία n
στοιχείων σύμφωνα με τον παρακάτω αλγόριθμο (η συνάρτηση rand() της C επιστρέφει
επιστρέφει έναν τυχαίο ακέραιο αριθμό):

struct btnode Random_tree(int n)


{
struct btnode *root = NULL;
int i;

for (i=1; i<=n; i++)


root = Insert(root, rand());
return (root);
}

Πόσος είναι ο μέσος χρόνος για μια επιτυχή αναζήτηση στο δένδρο T; Για να
εκτιμήσουμε αυτό το χρόνο αρκεί να υπολογίσουμε το μέσο πλήθος των ανεπιτυχών
συγκρίσεων που γίνονται για μία επιτυχή αναζήτηση. Για το σκοπό αυτό θα
χρησιμοποιήσουμε την έννοια του:
Ιnternal path length (ipl): το άθροισμα των μηκών όλων των μονοπατιών από τη ρίζα σε έναν
κόμβο του δένδρου
Το ipl μας λέει ακριβώς πόσες ανεπιτυχείς συγκρίσεις γίνονται για όλες τις δυνατές επιτυχείς
αναζητήσεις αν το περιεχόμενο κάθε κόμβου του δένδρου συγκρίνεται μία φορά με το στοιχείο
που ψάχνουμε. Θέλουμε λοιπόν να υπολογίσουμε το μέσο ipl (n), το οποίο συμβολίζουμε με
ipl (n) για το προηγούμενο τυχαίο δένδρο των n στοιχείων. Χωρίς οποιαδήποτε άλλη
πληροφορία, μπορούμε επίσης να υποθέσουμε ότι κάθε μια από τις n! δυνατές μεταθέσεις των
n στοιχείων είναι ισοπίθανη για τη σειρά εισαγωγής.
Για να μπορέσουμε να βρούμε μια αναδρομική σχέση για το ipl (n) αρκεί να
παρατηρήσουμε ότι αν τα n στοιχεία είναι σε τυχαία σειρά, τότε η πιθανότητα ένα
οποιοδήποτε να βρίσκεται στη ρίζα του T είναι 1/n και τα υπόλοιπα n-1 είναι πάλι σε τυχαία
σειρά. Επιπλέον, αν η ρίζα του Τ είναι το στοιχείο xi = i-oστό μεγαλύτερο στοιχείο, τότε τα
στοιχεία που είναι μικρότερα του xi (δηλ. τα x1, x2, …, xi-1) είναι και αυτά σε τυχαία σειρά όπως
και τα μεγαλύτερα του xi (δηλ. τα xi+1, xi+2, …, xn). Άρα και το αριστερό και δεξί υποδένδρο της
ρίζας του Τ είναι τυχαία δένδρα, οπότε η αναδρομική σχέση που προκύπτει για το ipl (n) θα
έχει τη μορφή:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 127


Αλγόριθμοι και Δομές Δεδομένων

n 1
ipl (n) = ∑ n [n − 1 + ipl (k − 1) + ipl (n − k)] = O(nlogn), ipl (0) = ipl (1) = 0
k =1

όπου ο όρος n-1 προκύπτει απ΄το γεγονός ότι η ρίζα του δένδρου συνεισφέρει κατά 1 στο ipl
των υπόλοιπων κόμβων του. Η προηγούμενη σχέση θυμίζει αυτήν που είδαμε στην ανάλυση
της μέσης συμπεριφοράς του quicksort και έχει λύση Θ(nlogn) που σημαίνει ότι το πλήθο
πλήθος συγκρίσεων για μία επιτυχή αναζήτηση θα είναι clogn για κάποια μικρή σταθερά c (το
c για την ακρίβεια αποδεικνύεται ότι είναι γύρω στο 1,39, δηλ. ο μέσος χρόνος μιας επιτυχούς
αναζήτησης είναι περίπου 40% χειρότερος από το βέλτιστο χρόνο logn που έχουμε σε ένα
πλήρες ισοζυγισμένο δένδρο). Η ανάλυση του μέσου χρόνου μιας ανεπιτυχούς αναζήτησης
είναι περίπου η ίδια μόνο που αντί του ipl πρέπει να χρησιμοποιήσουμε το epl το οποίο
έχουμε δει ήδη στον υπολογισμό του κάτω φράγματος στην πολυπλοκότητα των συγκριτικών
αλγόριθμων ταξινόμησης, κεφ. 3.1.9.
Η προηγούμενη ανάλυση αναφέρεται σε ένα ημιδυναμικό τυχαίο κομβοπροσανατολισμένο
δυαδικό δένδρο αναζήτησης. Τί ισχύει με τις διαγραφές στοιχείων; Μέχρι τώρα δεν υπάρχει
κάποια γνωστή ανάλυση για το μέσο χρόνο αναζήτησης σε ένα δένδρο που έχει
κατασκευαστεί με μια τυχαία ακολουθία πράξεων insert και delete. Εξάλλου, σε πρακτικές
εφαρμογές οι ακολουθίες αυτές συνήθως δεν είναι τυχαίες και το γεγονός αυτό κάνει τα
τυχαία δυναμικά δένδρα αναξιόπιστα. Πάντως υπάρχει γενικά η πεποίθηση ότι μάλλον
αποκλείεται να ισχύει κάποιο αποτέλεσμα καλύτερο απ΄ αυτό της προηγούμενης
παραγράφου. Αυτό δικαιολογείται από την ασυμμετρία που υπάρχει στη λειτουργία του
αλγόριθμου Delete(): για να σβήσουμε ένα στοιχείο x απ΄ το δένδρο, το αντικαθιστούμε είτε
με το προηγούμενο ή με το επόμενό του στη συμμετρική διάταξη και κατόπιν σβήνουμε το
στοιχείο αυτό. Το μόνο γνωστό αποτέλεσμα που έχει αποδειχθεί είναι το εξής: Αν σε κάθε
διαγραφή αντικαθιστούμε το στοιχείο που σβήνουμε με το προηγούμενό του στη συμμετρική
διάταξη, τότε n τυχαίες εισαγωγές ακολουθούμενες από μία σειρά τυχαίων εισαγωγών και
διαγραφών εναλλάξ παράγουν ένα δένδρο με ipl (n) = Θ(n3/2) που δίνει μέσο χρόνο
αναζήτησης Θ(√n). Για να φανεί όμως μια τέτοια συμπεριφορά, η ακολουθία των εισαγωγών
και διαγραφών πρέπει να είναι αρκετά μεγάλη.
Mια τελευταία παρατήρηση για την πράξη της διαγραφής είναι η παρακάτω. Είναι γενικά
σύνηθες φαινόμενο στους αλγόριθμους αναζήτησης να έχουμε πιο πολύπλοκες υλοποιήσεις
για την πράξη της διαγραφής: τα στοιχεία τείνουν να είναι ενσωματωμένα στη δομή με ένα
αρκετά ‘σφικτό’ τρόπο και η απομάκρυνση κάποιου απ΄ αυτά μπορεί να επιφέρει σύνθετες
ανακατασκευές. Γι’ αυτό ένας διαφορετικός τρόπος υλοποίησης, που μπορεί για κάποιες
εφαρμογές να είναι πιο κατάλληλος, είναι να χρησιμοποιήσουμε τη σταδιακή διαγραφή (lazy
deletion). Σύμφωνα με την τεχνική αυτή ένας κόμβος δεν απομακρύνεται από τη δομή αλλά
μόνο μαρκάρεται ως «διαγραμμένος» για τις ανάγκες της αναζήτησης. Ο κώδικας για μια
τέτοια διαγραφή πρέπει να περιλαμβάνει έναν επιπλέον έλεγχο για την ύπαρξη τέτοιων
κόμβων ώστε να μπορεί να σταματήσει η αναζήτηση. Φυσικά, η προσέγγιση αυτή επιβάλει να
έχουμε πάντα υπόψη μας ότι ένα μεγάλο πλήθος από μαρκαρισμένους κόμβους σημαίνει και
σπατάλη χώρου. Ένας πιο αποδοτικός τρόπος είναι να ξανακτίζουμε περιοδικά τη δομή μας
από την αρχή εξαιρώντας όλους τους μαρκαρισμένους κόμβους.

4.1.11 Ισοζυγισμένα δένδρα


Οι αλγόριθμοι στα δυαδικά δένδρα αναζήτησης που είδαμε στα προηγούμενα
λειτουργούν καλά για μια ποικιλία εφαρμογών αλλά υποφέρουν από το μειονέκτημα της κακής
συμπεριφοράς σε ακραίες καταστάσεις. Επιπλέον, όπως είδαμε και στον quicksort, έτσι κι

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 128


Αλγόριθμοι και Δομές Δεδομένων

εδώ αυτή η κακή συμπεριφορά μπορεί να συμβαίνει αρκετά συχνά στην πράξη αν ο χρήστης
δεν επιχειρεί να την προλάβει. Ακολουθίες στοιχείων που είναι ήδη ταξινομημένα κατ΄
αύξουσα ή φθίνουσα σειρά ή ακολουθίες με στοιχεία μικρά και μεγάλα εναλλάξ τοποθετημένα
έχουν ως αποτέλεσμα τον εκφυλισμό των δυαδικών δένδρων σε γραμμικές λίστες με φτωχή
απόδοση.
Με τον quicksort ο μόνος τρόπος για να βελτιώσουμε την κατάσταση ήταν να
εφαρμόσουμε την τυχαιότητα: επιλέγοντας το στοιχείο διαχωρισμού τυχαία, οι νόμοι των
πιθανοτήτων μπορούν να εγγυηθούν ότι ο αλγόριθμος δεν έχει σχεδόν ποτέ κακή
συμπεριφορά. Ευτυχώς, για τα δυαδικά δένδρα αναζήτησης μπορούμε να κάνουμε κάτι
καλύτερο: υπάρχει μια τεχνική που εξασφαλίσει με βεβαιότητα ότι η κακή περίπτωση δεν θα
συμβεί ποτέ. Η τεχνική αυτή είναι γνωστή με το όνομα ζύγιση (balancing) και
χρησιμοποιείται ως βάση στους περισσότερους αλγόριθμους που εφαρμόζονται σε
ισοζυγισμένα δένδρα. Δυστυχώς όμως, λόγω περιορισμού της ύλης, θα συζητήσουμε αυτούς
τους αλγόριθμους πολύ συνοπτικά.
Τα ισοζυγισμένα δένδρα (balanced trees) είναι δένδρα αναζήτησης (είτε
κομβοπροσανατολισμένα ή φυλλοπροσανατολισμένα) τα οποία κτίζονται έτσι ώστε να
ικανοποιούν συγκεκριμένα κριτήρια ζύγισης τα οποία μπορούν να εγγυηθούν ότι το ύψους του
δένδρου για n στοιχεία θα είναι πάντα Θ(logn) και άρα όλες οι βασικές πράξεις μπορούν να
γίνουν σε Ο(logn) χρόνο. Γενικά τα ισοζυγισμένα δένδρα διακρίνονται σε τρεις μεγάλες
κατηγορίες:
(i) Tα υψοζυγισμένα δένδρα (heigh balanced trees)
(ii) Tα βαροζυγισμένα δένδρα (weight balanced trees)
(iii) Tα πολυδιακλαδισμένα τέλεια ισοζυγισμένα δένδρα (perfectly balanced multiway
trees)
Oι δύο πρώτες κατηγορίες περιλαμβάνουν δυαδικά δένδρα ενώ στην τρίτη ο αριθμός των
παιδιών κάθε κόμβου ποικίλει ανάλογα με το είδος του δένδρου. Ας δούμε στα γρήγορα κάθε
κατηγορία χωριστά.

Υψοζυγισμένα δένδρα
Στα υψοζυγισμένα δένδρα, κάθε κόμβος πληρεί μία συνθήκη ζύγισης που ααναφέρεται
στο ύψος του υποδένδρου που κρέμεται από τον κόμβο αυτό. Το δένδρο είναι ζυγισμένο αν τα
ύψη των υποδένδρων κάθε κόμβου δεν διαφέρουν πολύ μεταξύ τους. Η ζύγιση όμως αυτή
μπορεί να χαλάσει με την εκτέλεση μιας δυναμικής πράξης, γι΄ αυτό σε κάθε λειτουργία insert
ή delete μετά την εισαγωγή ή διαγραφή ενός στοιχείου ξεκινάει μια διαδικασία επαναζύγισης
(rebalancing) του δένδρου που περιλαμβάνει τις εξής επαναζυγιστικές πράξεις:
(α) απλές αλλαγές ζύγισης που ενημερώνουν την πληροφορία ζύγισης ενός κόμβου και οι
οποίες χαρακτηρίζονται ως φτηνές πράξεις και,
(β) δύο άλλες δομικές επαναζυγιστικές πράξεις που είναι η απλή περιστροφή (single
rotation) και η διπλή περιστροφή (double rotation) η οποία αποτελείται από δύο απλές
που εκτελούνται με διαφορετική φορά.
Η πράξη της απλής και διπλής περιστροφής απεικονίζεται παρακάτω. Χαρακτηρίζονται
ως δομικές και ακριβές πράξεις επειδή ακριβώς μεταβάλλουν τη δομή του δένδρου. Οι
περιστροφές είναι οι μόνες πράξεις που διορθώνουν τη ζύγιση στο δένδρο και συνάμα δεν
επηρεάζουν τη συμμετρική διάταξη των στοιχείων του.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 129


Αλγόριθμοι και Δομές Δεδομένων

α β
Δεξιά απλή
β περιστροφή στον α α

Αριστερή απλή
περιστροφή στον β

α γ

β β α
Δεξιά διπλή περιστροφή στον α:
γ αριστερή απλή στον β και δεξιά
απλή στον α

Σχήμα 45: Η πράξη της περιστροφής σε ένα δυαδικό ισοζυγισμένο δένδρο

Οι περιστροφές επιφέρουν τοπικές αλλαγές στο δένδρο οι οποίες μεταδίδονται κατά μήκος
του μονοπατιού αναζήτησης από κάτω προς τα πάνω (bottom up). Η περιστροφή, απλή ή
διπλή, που αποκαθιστά τη ζύγιση καλείται τερματική. Mας ενδιαφέρει σε κάθε λειτουργία
insert και delete οι περιστροφές να είναι όσο το δυνατόν λιγότερες.
Κύριοι αντιπρόσωποι της κατηγορίας των υψοζυγισμένων δένδρων είναι τα ΑVL δένδρα
(Αdel’son-Velskιi & Landis, 1962) και τα κόκκινα-μαύρα (red-black) δένδρα (Βayer, 1972 και
Γκίμπας & Sedgewick,1978).

AVL δένδρα
Σε ένα AVL δένδρο (που ιστορικά είναι και το παλαιότερο ισοζυγισμένο δένδρο) η
πληροφορία ζύγισης που αποθηκεύεται σε κάθε κόμβο είναι η διαφορά των υψών μεταξύ
δεξιού και αριστερού υποδένδρου του κόμβου. Η διαφορά αυτή απαιτούμε να είναι -1, 0 ή 1
(είναι δυνατή η γενίκευση από –c έως c για κάποια μικρή σταθερά c). Παράδειγμα ενός
κομβοπροσανατολισμένου AVL δένδρου δίνεται παρακάτω. Στο εν λόγω δένδρο η διαφορά
των υψών των υποδένδρων κάθε κόμβου είναι 1.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 130


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 46: Παράδειγμα AVL δένδρου

Η συνθήκη ζύγισης ενός κομβοπροσανατολισμένου AVL δένδρου με n≥1 στοιχεία


εγγυάται ότι ανά πάσα στιγμή το ύψος του h θα κυμαίνεται μεταξύ των παρακάτω ορίων:
log(n+1)-1 ≤ h ≤ 1,44log(n+1)-1
και άρα οποιαδήποτε αναζήτηση στο δένδρο κοστίζει χρόνο Θ(logn).
Πράγματι, οι δύο ακραίες καταστάσεις είναι ένα πλήρες δυαδικό δένδρο και το δένδρο
Fibonacci .
Στο πλήρες δυαδικό δένδρο ισχύει n = 2h+1 -1, άρα h = log(n+1)-1.
Ένα Fibonacci δένδρο ορίζεται αναδρομικά ως εξής. Έστω Fh το Fibonacci δένδρο με
ύψος h. To F0 αποτελείται από ένα μόνο φύλλο, το F1 από ένα κόμβο με αριστερό παιδί ένα
φύλλο και το Fh+2 αποτελείται από μια ρίζα με αριστερό παιδί το Fh+1 και δεξί το Fh όπως
απεικονίζεται στο επόμενο σχήμα:

Σχήμα 47: Oρισμός του Fibonacci δένδρου

Παρατηρείστε ότι το AVL δένδρο του προηγούμενου παραδείγματος είναι το F4. Έστω Nh το
πλήθος των στοιχείων του Fh δένδρου. Tότε από την κατασκευή του δένδρου ισχύει Nh = Nh -1
+ Nh-2 + 1 με Νο = 1, Ν1 = 2, Ν3 = 4 και άρα Nh = Fib(h+3)-1, όπου Fib(h+3) είναι ο (h+3)-οστός

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 131


Αλγόριθμοι και Δομές Δεδομένων

όρος της ακολουθίας Fibonacci4. Aν το δένδρο αποθηκεύει n στοιχεία (Nh = n) το ζητούμενο


άνω φράγμα προκύπτει από τον κλειστό τύπο της ακολουθίας Fibonacci.

Σημειώνουμε εδώ ότι και για τα φυλλοπροσανατολισμένα δένδρα o υπολογισμός του ύψους
γίνεται με παρόμοιο τρόπο και δίνει αντίστοιχα όρια.

Σε σχέση με τις πράξεις insert και delete αναφέρουμε ότι απαιτούν στη χειρότερη
περίπτωση Ο(logn) αλλαγές στην πληροφορία ζύγισης (μη δομικές πράξεις) στους κόμβους
κατά μήκος του μονοπατιού από τη θέση εισαγωγής μέχρι τη ρίζα. Επιπλέον, η insert
χρειάζεται το πολύ μία απλή ή διπλή περιστροφή (ακριβή δομική πράξη) η οποία εκτελείται
τελευταία ενώ η delete Ο(logn) περιστροφές (για την ακρίβεια h/2 περιστροφές αν το ύψος του
δένδρου είναι h) στο μονοπάτι από τον κόμβο διαγραφής μέχρι τη ρίζα του δένδρου.

Kόκκινα-μαύρα δένδρα
Είναι τα πλέον δημοφιλή δένδρα. Σ΄ ένα κόκκινο-μαύρο δένδρο η πληροφορία ζύγισης
είναι ένα bit που παριστάνει ένα χρώμα, κόκκινο ή μαύρο. Για τους κόμβους του δένδρου
ισχύουν τα εξής:
- Η ρίζα και τα φύλλα του δένδρου βάφονται εξ’ ορισμού μαύρα,
- Τα παιδιά κάθε κόκκινου κόμβου είναι μαύρα,
- Όλα τα μονοπάτια από τη ρίζα στα φύλλα του δένδρου έχουν τον ίδιο αριθμό μαύρων
κόμβων.
Στο επόμενο σχήμα δίνεται παράδειγμα ενός κόκκινου-μαύρου δένδρου αναζήτησης:

Σχήμα 48: Παράδειγμα κόκκινου-μαύρου δένδρου

Μπορούμε εύκολα να δούμε από τον παραπάνω ορισμό ότι ο λόγος του μακρύτερου προς
το συντομότερο μονοπάτι του δένδρου είναι το πολύ 2 (το μακρύτερο μονοπάτι περιέχει
μαύρους και κόκκινους κόμβους εναλλάξ και το συντομότερο μόνο μαύρους).

4Η ακολουθία αυτή είναι διπλά αναδρομική και ορίζεται ως εξής: Fib(0) = 0, Fib(1) = 1 και Fib(k) = Fib(k-1) +
Fib(k-2) για k ≥ 2. Η λύση της είναι Fib(k) = (ak+1 – bk+1)/√5 όπου a = (1+√5)/2 και b = (1-√5)/2

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 132


Αλγόριθμοι και Δομές Δεδομένων

Από την προηγούμενη παρατήρηση σε συνδυασμό με το γεγονός ότι το ελάχιστο ύψος


ενός κόκκινου-μαύρου δένδρου συμβαίνει σε ένα πλήρες δένδρο (που αποτελείται για
παράδειγμα μόνο από μαύρους κόμβους) προκύπτει ότι σε ένα κομβοπροσανατολισμένο
κόκκινο-μαύρο δυαδικό δένδρο αναζήτησης με n ≥1 κόμβους και ύψος h ισχύει:
log(n+1)-1 ≤ h ≤ 2log(n+1)-2

To κόκκινο-μαύρο δένδρο δεν έχει την αυστηρή μορφή ζύγισης των AVL δένδρων, γι΄ αυτό
και το ύψος στη χειρότερη περίπτωση είναι μεγαλύτερο. Έτσι, μια πράξη search κοστίζει
περισσότερο, όμως εξαιτίας ακριβώς της ασθενέστερης συνθήκης ζύγισης τα κόκκινα-μαύρα
δένδρα επιτρέπουν ταχύτερη εκτέλεση των πράξεων insert και delete. Συγκεκριμένα, και οι δύο
δυναμικές πράξεις απαιτούν Ο(logn) αλλαγές χρώματος στους κόμβους αλλά μόνο Ο(1)
περιστροφές. Επιπλέον, για τα κόκκινα-μαύρα δένδρα έχουν αποδειχθεί και οι παρακάτω
ιδιότητες:
• Για m πράξεις insert και delete το πλήθος των επαναζυγίσεων είναι Ο(m),
• Ένας κόμβος σε ύψος k θα επαναζυγιστεί (με αλλαγή χρώματος ή μια περιστροφή) με
πιθανότητα Ο(1/ck) για κάποια σταθερά c>1.
Τέλος, τα κόκκινα-μαύρα δένδρα επιτρέπουν η επαναζύγιση να γίνεται από πάνω προς τα
κάτω (top down) όπου απαιτούνται Ο(logn) περιστροφές και άρα είναι ιδανικά σε
παράλληλες εφαρμογές όπου πολλοί χρήστες θέλουν να προσπελάσουν το ίδιο κομμάτι του
δένδρου ταυτόχρονα.

Βαροζυγισμένα δένδρα
Τα βαροζυγισμένα δένδρα χρησιμοποιούν για τη ζύγιση πληροφορία σχετικά με το
πλήθος των κόμβων ή των φύλλων των υποδένδρων και χρησιμοποιούν τις ίδιες
επαναζυγιστικές πράξεις όπως και τα υψοζυγισμένα για να επιτύχουν ζύγιση. Κυριότερος
αντιπρόσωπος των βαροζυγισμένων δένδρων είναι τα BB[α] δένδρα (Nievergelt και Reingold,
1973).

O ορισμός ενός BB[α] δένδρου είναι ο εξής:


Έστω α μια παράμετρος ζύγισης με 1/4 < α ≤ 1-√2/2. Το πλήθος των φύλλων του
υποδένδρου Τv με ρίζα το κόμβο v καλείται βάρος (weight) του v. Για τον κόμβο v ορίζουμε τη
ζύγισή του ίση με το λόγο του βάρους του αριστερού του παιδιού διά το βάρος του v. To
δένδρο είναι ΒΒ[α] αν για κάθε κόμβο του v ισχύει:
α ≤ ζύγιση(v) ≤ 1-α
Για α = ½ παίρνουμε ένα τέλεια ζυγισμένο δένδρο. Κι εδώ μπορεί να αποδειχθεί ότι το
ύψος ενός BB[α] δένδρου με n κόμβους είναι Θ(logn) ενώ για τις πράξεις insert και delete
έχουμε τα ακόλουθα αποτελέσματα: κάθε πράξη απαιτεί Ο(logn) αλλαγές ζύγισης και (απλές
ή διπλές) περιστροφές αλλά για τη συχνότητα εκτέλεσης των περιστροφών ισχύει ένας πολύ
σημαντικός κανόνας:
Αν ένας κόμβος u επαναζυγίστηκε κάποια στιγμή με μια απλή ή διπλή περιστροφή, τότε θα
επαναζυγιστεί πάλι εφόσον γίνουν Ω(βάρος(u)) εισαγωγές ή διαγραφές στο υπόδενδρο Τu.
Η παραπάνω ιδιότητα ονομάζεται ιδιότητα βάρους (weight property) και εγγυάται μια
ικανοποιητική καθυστέρηση των ακριβών επαναζυγιστικών πράξεων σε κάποιον κόμβο και

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 133


Αλγόριθμοι και Δομές Δεδομένων

μπορεί να γίνει καταμερισμός (amortization) των εξόδων στις εισαγωγές και διαγραφές που
πέρασαν από τον κόμβο αυτό. Για τον λόγο αυτό είναι πολύ σημαντική σε παράλληλες
εφαρμογές καθώς και εφαρμογές πολυδιάστατων δομών δεδομένων.

Πολυδιακλαδισμένα τέλεια ισοζυγισμένα δένδρα


Kλείνουμε τη σύντομη αναφορά μας στα ισοζυγισμένα δένδρα με τα πολυδιακλαδισμένα
τέλεια ισοζυγισμένα δένδρα. Τα δένδρα αυτά είναι γνωστά με το όνομα (a, b) δένδρα και
αποτελεούν μια απ΄πο τις πιο κομψές υλοποιήσεις υψοζυγισμένων δένδρων αναζήτησης.
Πρόκειται για φυλλοπροσανατολισμένα δένδρα όπου κάθε κόμβος περιέχει από a έως b
παιδιά Ο τυπικός τους ορισμός είναι ο εξής:
Ένα δένδρο Τ είναι (a, b) για τους ακέραιους a, b με a ≥ 2 και b ≥ 2a-1 αν:
- Όλα τα φύλλα του Τ έχουν το ίδιο βάθος (τέλεια ζύγιση),
- Κάθε εσωτερικός κόμβος μπορεί να έχει το λιγότερο a και το πολύ b παιδιά με εξαίρεση τη
ρίζα του δένδρου που μπορεί να έχει το λιγότερο 2 παιδιά.
Όταν b = 2a-1 τότε το δένδρο ονομάζεται B δένδρο.
Με εξαίρεση ίσως τη ρίζα, σε κάθε εσωτερικό κόμβο v του δένδρου αποθηκεύονται από
a-1 μέχρι b-1 τιμές σύμφωνα με τη συμμετρική διάταξη. Aυτό σημαίνει ότι αν ο κόμβος v έχει
ρ(v) παιδιά και αποθηκεύει τις τιμές k1(v) < k2(v) < … < kρ(v)-1(v) και τα υποδένδρα που
κρέμονται από τον v από αριστερά προς τα δεξιά είναι τα T1(v), … Tρ(v)(v), τότε για κάθε φύλλο
w του Ti(v) ισχύει ki-1(v) < τιμή του w ≤ ki(v) ∀i: 2 ≤ i ≤ ρ(v) ενώ αν το w ανήκει στο T1(v) τότε
τιμή του w ≤ k1(v). Αυτή ακριβώς η διάταξη χρησιμοποιείται για να καθοδηγεί την αναζήτηση
σε μια πράξη search(x) στο δένδρο μέχρι να φτάσουμε τελικά στο φύλλο με τιμή = x (επιτυχής
αναζήτηση) ή τιμή ≠ x (ανεπιτυχής αναζήτηση).
Παρακάτω δίνεται ένα (2, 4) δένδρο για τα στοιχεία 8, 15, 20, 30, 40, 42, 50.

Σχήμα 49: Παράδειγμα (2, 4) δένδρου

Παρατηρείστε ότι όσο μεγαλύτερος είναι ο βαθμός διακλάδωσης b, τόσο πιο γεμάτοι είναι
οι κόμβοι του δένδρου και άρα έχουμε μικρότερο ύψος για το ίδιο πλήθος στοιχείων που
αποθηκεύει στα φύλλα του το δένδρο. Για παράδειγμα, χρησιμοποιώντας βαθμό
διακλάδωσης b = 103 μπορούμε να αποθηκεύσουμε 109 στοιχεία στα φύλλα ενός δένδρου με
ύψος μόλις 3. Το πλεονέκτημα αυτό κάνει τα (a, b) δένδρα κατάλληλα για εφαρμογές όπου το
πλήθος των κόμβων που επισκεπτόμαστε κατά την εκτέλεση μιας πράξης (search, insert,
delete κλπ.) πρέπει να είναι πολύ μικρός. Χαρακτηριστικό παράδειγμα αποτελούν οι

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 134


Αλγόριθμοι και Δομές Δεδομένων

εφαρμογές διαχείρισης μεγάλου όγκου δεδομένων όπου μεγάλο μέρος των στοιχείων είναι
αποθηκευμένο στο σκληρό δίσκο του υπολογιστή (πχ. σε συστήματα διαχείρισης βάσεων
δεδομένων).
Αναφορικά με το ύψος ενός (a, b) δένδρου ισχύουν τα παρακάτω.
Έστω h το ύψος του δένδρου το οποίο περιέχει n στοιχεία Το ελάχιστο ύψος συμβαίνει
όταν κάθε εσωτερικός κόμβος έχει ακριβώς b παιδιά. Tότε το πλήθος των φύλλων του δένδρου
είναι bh = n, oπότε ελάχιστο ύψος h = logn/logb. To μεγαλύτερο ύψος συμβαίνει όταν η ρίζα
έχει 2 παιδιά και κάθε άλλος εσωτερικός κόμβος a παιδιά. Τότε το πλήθος των φύλλων του
δένδρου είναι 2ah-1, oπότε μέγιστο ύψος h = log(n/2)/loga+1. Δηλαδή, για το ύψος h ενός (a, b)
δένδρου ισχύει:
logn / logb ≤ h ≤ log(n/2) / loga + 1
και άρα είναι Θ(logn) για σταθερά a, b των οποίων οι ακριβείς τιμές εξαρτώνται από την
εφαρμογή όπου χρησιμοποιείται το δένδρο. Άμεση απόρροια αυτού είναι ότι κάθε πράξη
insert εκτελείται σε χρόνο Θ(logn).
Μια σημαντική διαφορά των (a, b) δένδρων με τα ισοζυγισμένα δένδρα των
προηγούμενων κατηγοριών είναι ότι η πληροφορία ζύγισης είναι σφαιρική και όχι τοπική σε
κάθε κόμβο: όλα τα φύλλα απέχουν το ίδιο από τη ρίζα και η μόνη ιδιότητα που πρέπει να
εξασφαλίζεται είναι ο αριθμός των παιδιών κάθε κόμβου να είναι από a μέχρι b. Αυτό σημαίνει
ότι τώρα αλλάζει και η φύση των επαναζυγιστικών πράξεων.
Μια πράξη insert μπορεί να οδηγήσει σε ένα κόμβο με b+1 παιδιά (λέμε ότι ο κόμβος
αυτός υπερχείλισε), οπότε ο ορισμός παραβιάζεται. Τότε ο κόμβος με τα b+1 παιδιά δίνει
κάποια σε έναν γειτονικό του αδερφό, αν υπάρχει ένας που δεν είναι γεμάτος και η insert
τερματίζεται (η πράξη αυτή καλείται διαμοιρασμός – sharing). Διαφορετικά, δημιουργείται
ένας νέος κόμβος που παίρνει τα μισά παιδιά αυτού που υπερχείλισε (πράξη διαχωρισμού –
split) και η διαδικασία συνεχίζεται προς τα πάνω.
Στη λειτουργία delete τώρα, αν σβήσουμε κάποιο φύλλο που κρέμεται από έναν κόμβο με
a παιδιά, τότε αυτός είτε δανείζεται από κάποιον γειτονικό αδερφό του, αν υπάρχει κάποιος με
> a παιδιά και η πράξη τερματίζεται. Διαφορετικά, τα δύο αδέρφια συγχωνεύονται σε έναν
κόμβο με 2a-1 παιδιά (πράξη συγχώνευσης – fusion ή merging) και η διαδικασία
συνεχίζεται προς τα πάνω.
Και στις δύο δυναμικές πράξεις, στη χειρότερη περίπτωση η διαταραχή θα φτάσει μέχρι
τη ρίζα του δένδρου και μόνε τότε μπορεί να αυξηθεί κατά 1 το ύψος του δένδρου σε μια insert
ή να μειωθεί κατά 1 σε μια delete. Δηλ. κάθε πράξη στη χειρότερη περίπτωση απαιτεί Ο(logn)
επαναζυγιστικές πράξεις που περιλαμβάνουν διαχωρισμούς ή συγχωνεύσεις κόμβων
αντίστοιχα μέχρι να γίνει η τερματική πράξη διαμοιρασμού.
Μια τελευταία ιδιότητα των (a, b) δένδρων είναι η εξής: το συνολικό κόστος επαναζύγισης
n εισαγωγών και διαγραφών στοιχείων σε ένα αρχικά άδειο (a, b) δένδρο είναι Ο(n) όταν b >
2a-1 ενώ για b = 2a-1 αυτό δεν ισχύει.
Στις περισσότερες εφαρμογές τα (a, b) δένδρα εμφανίζονται με τη μορφή (2, 4) δένδρων
τα οποία είναι εύκολα και πρακτικά στην υλοποίησή τους και απαντώνται σχεδόν σε όλα τα
συστήματα διαχείρισης βάσεων δεδομένων. Επιπλέον, έχουν αποδειχθεί ισοδύναμα με τα
κόκκινα-μαύρα δένδρα.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 135


Αλγόριθμοι και Δομές Δεδομένων

4.1.12 Πολυδιάστατα δένδρα αναζήτησης


Τα ισοζυγισμένα δυαδικά δένδρα αναζήτησης που είδαμε επιλύουν αποδοτικά το
πρόβλημα του λεξικού σε μία διάσταση. Σε πολλές εφαρμογές ωστόσο ανάκτησης
πληροφορίας (γραφικά, ρομποτική, στατιστική κλπ.) ένα βασικό πρόβλημα είναι ο
προσδιορισμός των σημείων που κείνται εντός κάποιων προκαθορισμένων ορίων περιοχής
σε διάφορες διαστάσεις. Για την επίλυση αυτού του προβλήματος χρησιμοποιούνται τα
πολυδιάστατα δένδρα αναζήτησης (multidimensional search trees).
Ένα στοιχείο δεδομένων καλείται πολυδιάστατο αν χαρακτηρίζεται από ένα πλήθος
χαρακτηριστικών (attributes). Ένα τέτοιο στοιχείο k χαρακτηριστικών μπορούμε να το
φανταστούμε ως ένα ‘σημείο’ στο χώρο των k διαστάσεων. Η επίλυση ενός πολυδιάστατου
προβλήματος απαιτεί συνήθως την οργάνωση των σημείων αυτών σε ένα πολυδιάστατο
δένδρο αναζήτησης. Κλασικό παράδειγμα τέτοιου δένδρου είναι το δένδρο περιοχής (range
tree) το οποίο περιγράφεται συνοπτικά στα επόμενα.

Το δένδρο περιοχής
Το δένδρο περιοχής προτάθηκε από τον Willard το 1985 για την επίλυση του
προβλήματος του παραθύρου (windowing ή orthogonal range searching) που διατυπώνεται
ως εξής:

Δοθέντων n σημείων στο επίπεδο κι ενός


ορθογωνίου Q που έχει τις πλευρές του
παράλληλες στους άξονες Χ-Υ, βρες τα
σημεία που ανήκουν στο Q (διπλανό
σχήμα)

Έστω P είναι το σύνολο των n σημείων στο επίπεδο και Q = [xQ1..xQ2] x [yQ1..yQ2] η
ορθογώνια περιοχή (παράθυρο) της ερώτησης. Για να απαντήσουμε το πρόβλημα: α) πρώτα
εστιάζουμε στα σημεία με x-συντεταγμένη που ανήκει στο διάστημα [xQ1..xQ2] και β) από τα
σημεία αυτά βρίσκουμε εκείνα με y-συντεταγμένη εντός του [yQ1..yQ2].
Για το α) η απάντηση είναι εύκολη. Αρχικά αποθηκεύουμε τα σημεία με βάση τη x-
συντεταγμένη τους στα φύλλα ενός ισοζυγισμένου φυλλοπροσανατολισμένου δυαδικού
δένδρου αναζήτησης Τ το οποίο μπορεί να υλοποιηθεί πχ. ως ένα AVL ή ένα κόκκινο-μαύρο
δένδρο. Στη συνέχεια ψάχνουμε στο Τ με βάση τα xQ1, xQ2. Έστω PxQ1, PxQ2 τα δύο
μονοπάτια αναζήτησης προς τα xQ1 και xQ2 αντίστοιχα τα οποία διαχωρίζονται στον κόμβο
vsplit όπως απεικονίζεται στο επόμενο σχήμα:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 136


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 50: Εύρεση των σημείων με x-συντεταγμένη μεταξύ των xQ1 και xQ2

Ως απάντηση αναφέρουμε τα σημεία που αποθηκεύονται στα φύλλα των υπόδενδρων Τv,
όπου v είναι είτε δεξιό παιδί κάποιου κόμβου στο μονοπάτι PxQ1 κάτω από τον vsplit ή
αριστερό παιδί κάποιου κόμβου στο μονοπάτι PxQ2 κάτω από τον vsplit και δεν ανήκει στα
μονοπάτια PxQ1 και PxQ2 (στο παραπάνω σχήμα είναι τα φύλλα των γκρι υπόδενδρων).
Δεδομένου ότι επισκεπτόμαστε Ο(logn) ξένα μεταξύ τους υπόδενδρα, ο συνολικός χρόνος
απάντησης είναι Ο(logn + |Α|), όπου Α είναι το σύνολο των σημείων της απάντησης.
Για το σημείο β) επεκτείνουμε την παραπάνω δομή ως εξής. Για κάθε κόμβο v του
δένδρου Τ κατασκευάζουμε ένα ισοζυγισμένο φυλλοπροσανατολισμένο δυαδικό δένδρο
αναζήτησης για τα σημεία που αποθηκεύονται στα φύλλα του Tv και με βάση την y-
συντεταγμένη. Πρακτικά ο κόμβος v περιέχει ένα δείκτη σ’ αυτό το δευτερεύον δένδρο και με
τον τρόπο αυτό έχουμε δημιουργήσει ένα δένδρο δύο επιπέδων όπου το κύριο δένδρο Τ είναι
το δένδρο 1ου επιπέδου και τα δευτερεύοντα δένδρα των κόμβων του Τ είναι τα δένδρα 2ου
επιπέδου (τα δένδρα αυτά φωλιάζουν μέσα στους κόμβους του Τ). Τα συγκεκριμένα δένδρα
απεικονίζονται στο σχήμα που ακολουθεί που δείχνει το δένδρο περιοχής για ένα επιλεγμένο
σύνολο σημείων του επιπέδου:

Σχήμα 51: Παράδειγμα δένδρου περιοχής για ένα συγκεκριμένο σύνολο σημείων

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 137


Αλγόριθμοι και Δομές Δεδομένων

Ο αλγόριθμος που απαντά ερωτήσεις παραθύρου για το ορθογώνιο Q = [xQ1..xQ2] x


[yQ1..yQ2] είναι ο εξής:
1. Ψάχνουμε στο δένδρο του 1ου επιπέδου Τ με βάση τα xQ1, xQ2. Έτσι επιλέγουμε τα Ο(logn)
δένδρα του 2ου επιπέδου τα οποία αποθηκεύουν ξένα μεταξύ τους υποσύνολα σημείων
με x-συντεταγμένη στο [xQ1..xQ2].
2. Σε κάθε ένα από τα παραπάνω δένδρα ψάχνουμε με τον ίδιο τρόπο και αναφέρουμε τα
σημεία με y-συντεταγμένη στο διάστημα [yQ1..yQ2].
Ο χρόνος για το βήμα 1. είναι O(logn). Σε κάθε ένα από τα Ο(logn) δένδρα Τv του 2ου
επιπέδου του βήματος 2. ξοδεύουμε χρόνο Ο(logn + |Av|). Επομένως, ο συνολικός χρόνος για

την εκτέλεση μιας ερώτησης παραθύρου είναι ∑ Ο(logn +|Av|) = O(log2n + |A|), όπου Α το
v
σύνολο των σημείων της απάντησης (ο χρόνος αυτός μπορεί να μειωθεί σε O(logn + |A|)
εφαρμόζοντας μια έξυπνη τεχνική που ονομάζεται fractional cascading και την οποία δεν θα
αναφέρουμε εδώ).
Ο χώρος τώρα που καταλαμβάνει το δένδρο παύει να είναι γραμμικός. Kάθε σημείο p
αποθηκεύεται σε όλα τα δένδρα 2ου επιπέδου των κόμβων του μονοπατιού εύρεσης προς το
φύλλο που περιέχει το p στο δένδρο 1ου επιπέδου T. Άρα, για όλους τους κόμβους του Τ που
βρίσκονται σε ένα συγκεκριμένο ύψος, το p αποθηκεύεται μόνο σε ένα δένδρο 2ου επιπέδου το
οποίο απαιτεί χώρο Ο(n). Επειδή το ύψος του Τ είναι Ο(logn), o συνολικός χώρος γίνεται
Ο(nlogn).

Δένδρα περιοχής σε μεγαλύτερες διαστάσεις


Τα δένδρα περιοχής έχουν εφαρμογή και στο χώρο των d διαστάσεων με d > 2. Ένα
δένδρο περιοχής d διαστάσεων μπορεί να κατασκευαστεί σύμφωνα με τον παρακάτω
αλγόριθμο:
Έστω Ρ ένα σύνολο n σημείων στο χώρο Rd. Aρχικά κατασκευάζουμε ένα ισοζυγισμένο
φυλλοπροσανατολισμένο δυαδικό δένδρο Τ με βάση την πρώτη συντεταγμένη των σημείων
(δένδρο 1ου επιπέδου). Για κάθε κόμβο v του Τ κατασκευάζουμε ένα δένδρο περιοχής d-1
διαστάσεων για τα σημεία που αποθηκεύουμε στα φύλλα του Τv χρησιμοποιώντας όμως τις
υπόλοιπες d-1 συντεταγμένες των σημείων. Αυτό το d-1 διαστάσεων δένδρο χτίζεται
αναδρομικά με τον ίδιο τρόπο: είναι ένα ισοζυγισμένο φυλλοπροσανατολισμένο δυαδικό
δένδρο με βάση τη δεύτερη συντεταγμένη των σημείων (δένδρο 2ου επιπέδου) όπου κάθε
κόμβος έχει ένα δείκτη σε ένα d-2 διαστάσεων δένδρο περιοχής. Η αναδρομή σταματά όταν
φτάσουμε στην τελευταία συντεταγμένη με βάση την οποία χτίζουμε το δένδρο του τελευταίου
επιπέδου d.
Λόγω της αναδρομής είναι εύκολο να δείτε ότι ένα δένδρο περιοχής d διαστάσεων (d ≥ 2)
χρησιμοποιεί Ο(nlogd-1n) χώρο και απαντά σε ερωτήσεις παραθύρου σε χρόνο O(logdn + |A|),
όπου Α το σύνολο των σημείων της απάντησης.

Κλείνουμε την σύντομη παρουσίαση στα δένδρα περιοχής με την παρακάτω


παρατήρηση. Το σύνολο των σημείων P μπορεί να είναι είτε γνωστό εκ των προτέρων και
σταθερό, δηλ. έχουμε ένα στατικό δένδρο περιοχής, ή να αλλάζει δυναμικά με εισαγωγές και
διαγραφές σημείων. Για τη δυναμική περίπτωση μπορούμε να χρησιμοποιήσουμε ένα

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 138


Αλγόριθμοι και Δομές Δεδομένων

δυναμικό ισοζυγισμένο δυαδικό δένδρο αναζήτησης. Στην περίπτωση που το δένδρο είναι το
AVL ή το ΒΒ[α], τότε στη χειρότερη περίπτωση κάθε πράξη εισαγωγής/διαγραφής σε ένα
δένδρο περιοχής d διαστάσεων κοστίζει Ο(logdn) χρόνο.

4.1.13 Άλλα δένδρα


Τα δένδρα αναζήτησης που είδαμε στα προηγούμενα είναι τα βασικά εργαλεία για την
επίλυση του γενικού προβλήματος του λεξικού στη μία ή σε περισσότερες διαστάσεις. Στην
πραγματικότητα υπάρχει ένα τεράστιο πλήθος εφαρμογών που απαιτούν αποδοτική
επεξεργασία δεδομένων. Στις εφαρμογές αυτές χρησιμοποιούνται συνήθως ειδικά δένδρα, τα
οποία οργανώνουν και διαχειρίζονται τα δεδομένα ανάλογα με το πρόβλημα που καλούνται να
επιλύσουν. Ενδεικτικά αναφέρουμε τα R δένδρα που χρησιμοποιούνται στη διαχείριση
χωρικών βάσεων δεδομένων, τα τετραδικά δένδρα που χρησιμοποιούνται σε εφαρμογές
επεξεργασίας εικόνας, τα δένδρα Huffman που εφαρμόζονται στην κωδικοποίηση μηνυμάτων
στα δυαδικά τους ισοδύναμα για την μετάδοσή τους μέσα από ένα κανάλι επικοινωνίας, κλπ.
Στην συνέχεια θα αναφερθούμε συνοπτικά σε δύο μόνο τέτοια δένδρα, τα ψηφιακά και τα
τετραδικά δένδρα, καθένα από τα οποία έχει διαφορετικό πεδίο εφαρμογής.

Ψηφιακά δένδρα
Ένα ψηφιακό δένδρο (digital tree) αποθηκεύει τα στοιχεία ενός συνόλου ιεραρχικά
εκμεταλλευόμενο την αναπαράσταση των τιμών των στοιχείων.
Υποθέτουμε ότι η τιμή κάθε στοιχείου είναι μία ακολουθία ψηφίων ή χαρακτήρων οι οποίοι
ανήκουν σε ένα συγκεκριμένο αλφάβητο, πχ. τα δεκαδικά ψηφία 0-9, τα γράμματα a-z κλπ.
Μπορούμε να υλοποιήσουμε ένα ψηφιακό δένδρο αποθηκεύοντας σε κάθε κόμβο του ένα
μόνο ψηφίο και με τρόπο ώστε η τιμή κάθε στοιχείου που έχει εισαχθεί στο δένδρο να
σχηματίζεται ακολουθώντας ένα μονοπάτι από τη ρίζα του δένδρου σε κάποιο φύλλο του.
Έτσι, κάθε κόμβος του δένδρου έχει το πολύ τόσα παιδιά όσα είναι τα ψηφία του
αλφάβητου. Στο επόμενο σχήμα δίνεται μία τέτοια υλοποίηση για τους αριθμούς 123, 129,
140, 143, 148, 151, 155, 167 που θεωρείστε ότι αντιστοιχούν πχ. σε κωδικούς των φοιτητών
ενός τμήματος:

Σχήμα 52: Υλοποίηση ψηφιακού δένδρου για τους αριθμούς 123, 129, 140, 143, 148, 151,
155, 167

Παρατηρείστε ότι στο προηγούμενο παράδειγμα όλοι οι αριθμοί ξεκινούν με το ψηφίο 1.


Αν έπρεπε να αποθηκεύσουμε στο δένδρο και αριθμούς που ξεκινούσαν από 2, τότε θα
δημιουργούσαμε δύο ψηφιακά δένδρα. Το πλεονέκτημα του συγκεκριμένου τρόπου είναι ότι
το δένδρο μπορεί να αποθηκεύσει ακολουθίες μεταβλητού μήκους. Το σχήμα που ακολουθεί

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 139


Αλγόριθμοι και Δομές Δεδομένων

δείχνει πώς έχει τροποποιηθεί το προηγούμενο δένδρο για να συμπεριλάβει και τον αριθμό
1234.

Σχήμα 53: Το δένδρο που προκύπτει μετά την εισαγωγή του αριθμού 1234

Οι κόμβοι του σχήματος με μαύρο χρώμα χρησιμοποιούνται ως τερματικοί για να


σταματά η αναζήτηση μιας τιμής στο δένδρο. Για παράδειγμα ο αριθμός 151 υπάρχει στο
δένδρο ο κόμβος του τρίτου επιπέδου που αποθηκεύει το 1 είναι μαύρος. Αντίθετα ο 15 δεν
είναι αποθηκευμένος στο δένδρο, αφού ο κόμβος με την τιμή 5 του δευτέρου επιπέδου δεν
είναι μαύρος. Άρα, στον ορισμό του κόμβου του δένδρου θα πρέπει να προβλέψουμε τη
χρήση ενός πεδίου ακόμα που αποθηκεύει το χρώμα του κόμβου (ένα bit). Σε πρακτικές
εφαρμογές, η πληροφορία για το χρώμα του κόμβου αντικαθίσταται από έναν δείκτη προς το
αρχείο του δίσκου όπου φυλάγονται τα υπόλοιπα δεδομένα για το στοιχείο που έχουμε
προσπελάσει (πχ. στοιχεία των φοιτητών με το συγκεκριμένο κωδικό).

Ένας άλλος τρόπος για να υλοποιήσουμε ένα ψηφιακό δένδρο είναι να αποθηκεύσουμε
τις τιμές των στοιχείων στα φύλλα του δένδρου και σε κάθε κόμβο του να χρησιμοποιήσουμε
πίνακες που καθοδηγούν την αναζήτηση. Η συγκεκριμένη δομή είναι γνωστή με το όνομα
δένδρο ανάκτησης ή στα αγγλικά trie και προκύπτει από τη λέξη re-trie-val που σημαίνει
ανάκτηση.
Η δομή trie χρησιμοποιείται συνήθως για την αποθήκευση συμβολοσειρών σταθερού
μήκους. Έστω ότι κάθε σύμβολο επιλέγεται από ένα αλφάβητο k συμβόλων (ψηφίων). Αν το
μήκος κάθε συμβολοσειράς είναι λ, τότε το trie μπορεί να αποθηκεύσει συνολικά Ν = kλ
στοιχεία. Η προφανής υλοποίηση είναι σε κάθε κόμβο του trie να διατηρούμε έναν πίνακα από
k δείκτες. Κάθε θέση του πίνακα αντιστοιχεί σε ένα σύμβολο του αλφάβητου. Για έναν κόμβο
στο επίπεδο i, μία θέση που αντιστοιχεί στο σύμβολο d θα δείχνει σε έναν κόμβο του
επόμενου επιπέδου αν κάποια συμβολοσειρά περιέχει στην i-οστή της θέση από αριστερά το
σύμβολο d. Το ύψος του trie αυτού είναι ίσο με λ.
Ως παράδειγμα θεωρείστε ότι το αλφάβητό μας είναι το σύνολο {0, 1, 2} και θέλουμε να
αποθηκεύσουμε τα στοιχεία 102, 120, 121, 210, 211 και 212. To trie στην περίπτωσή μας θα
έχει την παρακάτω μορφή:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 140


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 54: Η δομή trie για τα στοιχεία 102, 120, 121, 210, 211 και 212

Οι πράξεις αναζήτησης, εισαγωγής και διαγραφής τώρα υλοποιούνται εύκολα και


περιλαμβάνουν τη διάσχιση ενός μονοπατιού από τη ρίζα σε κάποιο φύλλο του trie.
Επομένως, ο χρόνος χειρότερης περίπτωσης κάθε πράξης είναι Ο(λ) = Ο(logkN), δηλ.
ανεξάρτητος του πλήθους n των στοιχείων που αποθηκεύονται στη δομή.
Ο χώρος που χρησιμοποιεί ένα trie για να αποθηκεύσει n στοιχεία στη χειρότερη
περίπτωση είναι Ο(nλk). H χειρότερη περίπτωση συμβαίνει όταν πρέπει να αποθηκεύσουμε n
πλήρη μονοπάτια από λ κόμβους, καθένας εκ των οποίων χρησιμοποιεί χώρο Ο(k). Στην
περίπτωση αυτή, πολλοί κόμβοι έχουν βαθμό 1 με αποτέλεσμα οι υπόλοιπες k-1 θέσεις του
πίνακα να μένουν αναξιοποίητες με αποτέλεσμα σπατάλη χώρου.
Μια απλή μέθοδος για την αντιμετώπιση του προβλήματος αυτού είναι να
χρησιμοποιήσουμε ένα συμπαγές trie: αποθηκεύουμε μόνο τους κόμβους με βαθμό
μεγαλύτερο του 1 ενώ οι αλυσίδες των κόμβων με βαθμό 1 αντικαθίστανται με έναν αριθμό
που εκφράζει το πλήθος των κόμβων της αλυσίδας. Με την μέθοδο αυτή μπορεί να
αποδειχθεί ότι ο χώρος του trie από Ο(nλk) συμπτύσσεται σε Ο(nk) ενώ το μέσο ύψος του
συμπαγούς trie, άρα και ο μέσος χρόνος των πράξεων είναι Ο(logkn).

Τετραδικά δένδρα
Τα τετραδικά δένδρα (quad trees) έχουν ευρεία χρήση σε εφαρμογές γραφικών και
επεξεργασίας εικόνας.
Υποθέστε ότι στην οθόνη του υπολογιστή μας έχουμε την παρακάτω εικόνα, που για
λόγους ευκολίας θεωρούμε ότι είναι ασπρόμαυρη:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 141


Αλγόριθμοι και Δομές Δεδομένων

Η οθόνη περιλαμβάνει ένα πλήθος από μικρά τετράγωνα τα οποία καλούνται εικονοστοιχεία
(pixels) κι έστω το πλήθος των εικονοστοιχείων είναι 2n x 2n για κάποιο καλώς ορισμένο n>1.
Σε κάθε εικονοστοιχείο αντιστοιχεί μία τιμή η οποία δίνει το χρώμα του εικονοστοιχείου. Για
μια ασπρόμαυρη εικόνα, η τιμή αυτή παριστάνεται συνήθως με ένα byte (8 bits) κι επομένως
το χρώμα μπορεί να έχει τιμή από 1=20 έως 256=28 που αντιστοιχούν σε διαφορετικές
διαβαθμίσεις του γκρι , από το άσπρο έως το τελείως μαύρο (στις έγχρωμες εικόνες συνήθως
χρησιμοποιούνται 3 bytes = 24 bits για την κωδικοποίηση του χρώματος κι έχουμε επομένως
224 ≈ 16,7 εκατομμύρια χρώματα). Για το παράδειγμά μας υποθέτουμε ότι η τιμή κάθε
εικονοστοιχείου είναι μόνο ένα bit με τo 0 να αντιστοιχεί στο λευκό και το 1 στο μαύρο. Έτσι η
αρχική εικόνα παίρνει τώρα την παρακάτω μορφή:

0 0 0 0 0 0 0 0
0 0 0 0 1 1 0 0
0 0 0 0 1 1 1 1
0 0 0 0 1 1 1 0
1 1 1 0 1 1 1 1
1 0 0 1 1 1 1 1
1 1 0 0 1 1 1 1
1 1 0 0 1 1 1 1

Ο στόχος μας είναι να βρούμε μία δομή κατάλληλη για την αποθήκευση της παραπάνω
εικόνας στην μνήμη του υπολογιστή.
Για το σκοπό αυτό μπορούμε να χρησιμοποιήσουμε ένα διδιάστατο δυαδικό πίνακα με 2n
x 2n bits, ανεξάρτητα από το πλήθος των 1 (πόσο ‘γεμάτη’ δηλ. είναι η εικόνα). Όταν το n είναι
μεγάλο, αυτός ο τρόπος αποθήκευσης οδηγεί σε σπατάλη χώρου. Για παράδειγμα, για μια
οθόνη με ανάλυση 1.024x768 pixels απαιτούνται συνολικά 96 KB (φανταστείτε μία εφαρμογή
γραφικών όπου κανείς πρέπει να επεξεργαστεί ένα σύνολο τέτοιων εικόνων).
Η δομή δεδομένων που εφαρμόζεται συνήθως στο πρόβλημα αυτό είναι το τετραδικό
δένδρο κάθε κόμβος του οποίου εκτός των φύλλων έχει τέσσερα ακριβώς παιδιά και μπορεί
να οριστεί αναδρομικά σύμφωνα με την παρακάτω διαδικασία:
• Παριστάνουμε την εικόνα με τον αντίστοιχο δυαδικό πίνακα. Η ρίζα του τετραδικού
δένδρου αντιστοιχεί στον αρχικό αυτό πίνακα.
• Στη συνέχεια ο πίνακας χωρίζεται σε τέσσερις ισομεγέθεις υποπίνακες και κάθε τέτοιος
υποπίνακας αντιστοιχίζεται στον κόμβο ενός τετραδικού δένδρου. Αν ο υποπίνακας είναι
ομοιογενής, έχει δηλ. το ίδιο περιεχόμενο σε όλες τις θέσεις του (μόνο 0 ή μόνο 1), τότε ο
κόμβος είναι φύλλο του δένδρου. Αν δεν έχει το ίδιο περιεχόμενο σε όλες του τις θέσεις,
τότε ο υποπίνακας χωρίζεται εκ νέου σε τέσσερις υποπίνακες και η διαδικασία
επαναλαμβάνεται μέχρι να καταλήξουμε σε μία μόνο θέση – εικονοστοιχείο της αρχικής
μας εικόνας.
• Σε κάθε φύλλο αποθηκεύουμε κι ένα bit που μας δείχνει αν ο αντίστοιχος υποπίνακας
έχει σε όλα τα εικονοστοιχεία του την τιμή 0 ή 1.

Η διαδικασία διαχωρισμού του πίνακα παριστάνεται με τις έντονες γραμμές παρακάτω:

0 0 0 0 0 0 0 0
0 0 0 0 1 1 0 0

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 142


Αλγόριθμοι και Δομές Δεδομένων

0 0 0 0 1 1 1 1
0 0 0 0 1 1 1 0
1 1 1 0 1 1 1 1
1 0 0 1 1 1 1 1
1 1 0 0 1 1 1 1
1 1 0 0 1 1 1 1

Στο επόμενο σχήμα φαίνεται το τετραδικό δένδρο που προκύπτει για τον πίνακα του
παραδείγματος όπου κάθε φύλλο βάφεται λευκό ή μαύρο ανάλογα αν η πληροφορία για το
αντίστοιχο κομμάτι της εικόνας είναι 0 (άδεια) ή 1 (γεμάτη).

Σχήμα 55: Το τετραδικό δένδρο που αντιστοιχεί στον εικόνα του παραδείγματος

Η αναζήτηση σε ένα τετραδικό δένδρο γίνεται με τον ίδιο τρόπο όπως και στα δυαδικά
δένδρα μόνο που τώρα σε κάθε κόμβο θα πρέπει να αποφασίζουμε ποιο υπόδενδρο να
ακολουθήσουμε.
Ένα τετραδικό δένδρο που χρησιμοποιείται για την αποθήκευση μιας εικόνας μεγέθους
2n x 2n έχει ύψος το πολύ n, δηλ. λογαριθμικό στο μέγεθος της εικόνας. Το μέγιστο πλήθος
κόμβων N του δένδρου δίνεται από τον τύπο:
n
Ν=

k=0
4k ≈
4 n
3
4

δηλ. οι απαιτήσεις σε χώρο είναι περίπου τα 4/3 του μεγέθους της εικόνας.
Τέλος, επισημαίνεται ότι ο παραπάνω ορισμός του τετραδικού δένδρου που ισχύει για το
επίπεδο μπορεί να γενικευθεί και στις d διαστάσεις όπου κάθε κόμβος του δένδρου έχει τώρα
2d παιδιά.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 143


Αλγόριθμοι και Δομές Δεδομένων

4.2 Γράφοι
4.2.1 Τι είναι γράφος
Παραπάνω περιγράψαμε δομές δεδομένων οι οποίες αποτελούνται από κόμβους και
συνδέσεις ή αλλιώς ακμές. Τόσο η διάταξη των κόμβων όσο και το πλήθος και είδος των
ακμών καθόριζαν τον τρόπο οργάνωσης των δεδομένων και τη λειτουργία της δομής. Έτσι,
στις λίστες είχαμε ένα σύνολο από κόμβους που κάθε ένας συνδέονταν μέσω ενός συνδέσμου
– ακμής με τον επόμενο κόμβο σχηματίζοντας μία απλά συνδεδεμένη λίστα. Παράλληλα, στα
δένδρα σε κάθε κόμβο συνδέονταν περισσότερες ακμές που, ανάλογα με το πλήθος τους,
σχημάτιζαν διαφορετικά είδη δένδρων.
Στη γενική περίπτωση, όταν η δομή αποτελείται από κορυφές (ή κόμβους) και μερικά
ζεύγη αυτών συνδέονται με ακμές τότε αυτή καλείται γράφος (graph). Πιο αυστηρά, ένας
γράφος είναι ένα διατεταγμένο ζεύγος G = (V, E), όπου το V = {v1, v2,…, vn} είναι το σύνολο
των κορυφών ή κόμβων (vertices) ενώ Ε = {e1, e2, …, em} είναι ένα σύνολο από διμελή
σύνολα κορυφών (vi, vk) ∈ VxV που ορίζουν τις ακμές (edges) του γράφου. Ο γράφος που
ορίζεται με τον παραπάνω τρόπο καλείται μη κατευθυνόμενος (undirected), επειδή οι ακμές
του δεν έχουν συγκεκριμένη κατεύθυνση, σε αντίθεση με τον όπου κάθε ακμή έχει
συγκεκριμένο προσανατολισμό. Τα όσα ακολουθούν μέχρι και την παρ. 4.2.8 αναφέρονται σε
μη κατευθυνόμενους γράφους.

Παράδειγμα
Έστω έχουμε τον γράφο G=(V, E) με V = {v1, v2, v3} και Ε = {( v1, v2), ( v2, v3), (v1, v3)}. Στην
περίπτωση αυτή ο γράφος αποτελείται από 3 κορυφές και 3 ακμές όπως φαίνεται και στο
παρακάτω σχήμα.

Είναι προφανές ότι σε ένα γράφο G= (V, E) με n≥1 κορυφές (|V| = n), το πλήθος των
ακμών του |Ε| μπορεί να είναι μέχρι n(n-1)/2 (κάθε κορυφή συνδέεται με τις υπόλοιπες n-1).
Όταν το |Ε| είναι γραμμικό στο πλήθος των κορυφών, δηλ. |Ε| = Ο(n), τότε ο γράφος καλείται
αραιός (sparse) ενώ στην περίπτωση που το |Ε| = Θ(n2), τότε ο γράφος ονομάζεται πυκνός
(dense).
Σε ένα γράφο, οι ακμές μπορούν να φέρουν και κάποια πληροφορία που καλείται βάρος
(κόστος) και δίνεται από μια συνάρτηση βάρους w: E →R η οποία εξαρτάται από την
εφαρμογή. Για παράδειγμα, το βάρος μιας ακμής w(x, y) μπορεί να αντιστοιχεί στην
απόσταση ή στο κόστος μεταφοράς μεταξύ των κορυφών x, y (πχ. σε ένα γράφο που
αναπαριστά το οδικό δίκτυο μεταξύ κάποιων πόλεων) ή σε χωρητικότητα επικοινωνίας (πχ. σε
ένα γράφο που αναπαριστά ένα δίκτυο δεδομένων) κλπ. Στην περίπτωση αυτή ο γράφος
ονομάζεται βεβαρημένος ή βαροζυγισμένος (weighted).

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 144


Αλγόριθμοι και Δομές Δεδομένων

4.2.2 Βασικοί κανόνες απεικόνισης γράφων


Είναι σαφές ότι ένας γράφος ορίζεται μόνο από τα σύνολα V και E, και όχι από την θέση
των κορυφών ή το σχήμα τους ή το μήκος των ακμών κατά την γεωμετρική τους απεικόνιση.
Έτσι, ένας γράφος απεικονίζεται γραφικά σε ένα επίπεδο σημειώνοντας τις κορυφές και τις
ακμές του. Για την απεικόνιση κάθε γράφου ακολουθούνται κάποιοι κανόνες όπως:
• Δύο οποιασδήποτε ακμές του μπορούν να τέμνονται, η τομή τους όμως δεν αντιστοιχεί σε
κορυφή.
• Δύο οποιεσδήποτε κορυφές του δεν μπορούν να συνδέονται μεταξύ τους με περισσότερες
από μία ακμές.
• Δεν επιτρέπονται ανακυκλώσεις, δηλαδή να ξεκινά μία ακμή από μία κορυφή του γράφου
και να καταλήγει στην ίδια κορυφή (στην αντίθετη περίπτωση ορίζονται οι πολλαπλοί
γράφοι).
Παράλληλα ακολουθείται μία κοινή ονοματολογία όπως:
1. Όταν ο γράφος έχει μόνο μία κορυφή και καμία ακμή ονομάζεται τετριμμένος.
2. Οι κορυφές x και y ενός γράφου που ορίζουν μία ακμή του έστω e λέγονται άκρα της e.
Τότε λέμε ότι η ακμή e προσπίπτει στις κορυφές x και y του γράφου.
3. Αν δύο ακμές προσπίπτουν σε μία κοινή κορυφή τότε ονομάζονται όμορες.
4. Οι κορυφές x και y ενός γράφου που ορίζουν μία ακμή του καλούνται γειτονικές.
5. Ο αριθμός των ακμών που προσπίπτουν σε μία κορυφή, έστω v, ορίζει τον βαθμό της
κορυφής v και συμβολίζεται με d(v). Σε κάθε μη κατευθυνόμενο γράφο ισχύει:

∑ d(v) = 2|Ε|
v∈V
Η απόδειξη γι’ αυτό βασίζεται στην παρατήρηση ότι κάθε ακμή έχει δύο άκρα.
6. Ένας γράφος G’=(V’, E’) είναι υπογράφος (subgraph) του γράφου G=(V, E) όταν όλοι οι
κόμβοι και οι ακμές του γράφου G’ περιέχονται στον γράφο G δηλαδή ισχύουν: V’ ⊆ V
και E’ ⊆ E.

Παράδειγμα:
Ο γράφος:

έχει υπογράφο τον:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 145


Αλγόριθμοι και Δομές Δεδομένων

Για κάθε υποσύνολο κορυφών V’ του V μπορούμε να παράγουμε έναν υπογράφο ο οποίος
έχει σύνολο κορυφών το V’ και σύνολο ακμών όλες τις ακμές του G με άκρα στο V’. Επίσης,
μπορούμε να παράγουμε έναν υπογράφο G’ του γράφου G αν αφαιρέσουμε από τον G
(σύνολο Ε) μία ακμή. Αντιστοίχως, να παράγουμε έναν υπογράφο G’ του γράφου G αν
αφαιρέσουμε από τον G (σύνολο V) μία κορυφή έστω x και παράλληλα όλες τις ακμές που
έχουν ως άκρο την κορυφή x.

4.2.3 Αναπαράσταση γράφου στον υπολογιστή


Για την αναπαράσταση ενός γράφου στον υπολογιστή μπορεί να χρησιμοποιηθεί είτε ο
πίνακας γειτνίασης (adjacency matrix) είτε λίστες γειτνίασης (adjacency lists).
Έστω έχουμε έναν γράφο G = (V, E) με n κορυφές που αριθμούνται από το 1 έως το n.
Τότε ο πίνακας γειτνίασης είναι ένας πίνακας n x n, έστω A, με στοιχεία aij όπου κάθε aij
είναι ένα bit το οποίο αποθηκεύει την τιμή 1 στην περίπτωση που υπάρχει ακμή με άκρα τις
κορυφές i και j και 0 διαφορετικά.
Στις λίστες γειτνίασης έχουμε έναν πίνακα Adj που αποτελείται από n λίστες, μία για κάθε
κορυφή στο V. Για κάθε v ∈ V, το στοιχείο Αdj[v] είναι ένας δείκτης στο πρώτο στοιχείο μίας
απλά συνδεδεμένης λίστας που περιέχει όλες τις κορυφές u για τις οποίες (v, u) ∈ E. Δηλ. το
Adj[v] περιέχει όλες τις κορυφές του γράφου που είναι γειτονικές στη v.

Παράδειγμα
Παρακάτω φαίνονται οι δύο δομές δεδομένων για τον γράφο του σχήματος:

Σχήμα 56: Πίνακας γειτνίασης και λίστες γειτνίασης για την αποθήκευση ενός γράφου

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 146


Αλγόριθμοι και Δομές Δεδομένων

Παρατηρείστε ότι ο πίνακας γειτνίασης είναι συμμετρικός αφού τα ζεύγη (i, j) και (j, i)
παριστάνουν την ίδια ακμή (i, j) ∈ Ε και άρα aij = aji = 1.
Ο πίνακας γειτνίασης μπορεί να χρησιμοποιηθεί και για την αποθήκευση ενός βεβαρημένου
γράφου. Στην περίπτωση αυτή το στοιχείο aij του πίνακα γειτνίασης έχει ως τιμή το βάρος w(i,
j) αν (i, j) ∈ Ε. Αν (i, j) ∉ Ε τότε το aij παίρνει την ειδική τιμή NULL (αν και σε πολλά
γραφοθεωρητικά προβλήματα βολεύει να χρησιμοποιήσουμε την τιμή 0 ή ∞).
Οι λίστες γειτνίασης μπορούν επίσης να προσαρμοστούν για να αποθηκεύσουν έναν
βεβαρημένο γράφο, αν σε κάθε κόμβο της λίστας Adj[v] αποθηκεύουμε και το βάρος της
ακμής (v, u). Mπορούμε επιπλέον να διατηρούμε τη λίστα διατεταγμένη ως προς τα βάρη των
ακμών, κάτι που βολεύει σε αρκετά προβλήματα.
Αναφορικά τώρα με την αποδοτικότητα των δύο δομών, ισχύουν τα εξής.
Ο χώρος καταρχήν του πίνακα γειτνίασης είναι πάντα n2 ανεξάρτητα από το |Ε|. Σε
κάποιες εφαρμογές αρκεί να αποθηκεύσουμε μόνο τα στοιχεία που είναι πάνω ή κάτω από
την κύρια διαγώνιο και να μειώσουμε με τον τρόπο αυτό τον απαιτούμενο χώρο παραπάνω
από το μισό. Στις λίστς γειτνίασης, το συνολικό μέγεθος όλων των λιστών (συνολικό πλήθος
κόμβων) είναι μόνο 2|Ε| και επομένως ο συνολικός χώρος που καταλαμβάνει η δομή μαζί με
τον πίνακα Adj είναι Ο(n+|E|), δηλ. οιλίστες γειτνίασης είναι πιο αποδοτική σε χώρο για
αραιούς γράφους.
Σε σχέση με την υλοποίηση βασικών πράξεων σε ένα γράφο, κάθε δομή μπορεί να
υποστηρίξει αποδοτικά συγκεκριμένες μόνο πράξεις. Ο έλεγχος για παράδειγμα αν ο γράφος
περιέχει την ακμή (x, y) ή η εισαγωγή/διαγραφή μιας ακμής στον/από το γράφο εκτελούνται
όλες σε χρόνο Ο(1) αν χρησιμοποιήσουμε τον πίνακα γειτνίασης (ελέγχουμε ή αλλάζουμε
μόνο την τιμή aij). Αντιθέτως, με λίστες γειτνίασης και οι τρεις πράξεις απαιτούν χρόνο Ο(d(x))
αφού στη χειρότερη περίπτωση πρέπει να σαρώσουμε ολόκληρη τη λίστα Adj[x] για να
βρούμε αν η (x, y) υπάρχει, κι αν ναι να τη διαγράψουμε ή διαφορετικά να την εισάγουμε. Κι
επειδή o γράφος είναι μη κατευθυνόμενος, το ίδιο θα πρέπει να κάνουμε και στη λίστα Adj[y].
Οι λίστες γειτνίασης είναι σαφώς πιο αποδοτικές αν κανείς θέλει να υπολογίσει το βαθμό μιας
κορυφής x ή να βρει όλες τις ακμές του γράφου. Για την πρώτη πράξη απαιτείται σάρωση της
λίστας Adj[x] που γίνεται σε χρόνο Ο(d(x)) και για τη δεύτερη σάρωση όλων των λιστών σε
χρόνο Ο(|E|). Στην περίπτωση του πίνακα γειτνίασης απαιτείται χρόνος Ο(n) και Ο(n2)
αντίστοιχα.
Ο ακόλουθος πίνακας συνοψίζει τα αποτελέσματα της σύγκρισης μεταξύ των δύο δομών:

Γενικά η λίστα γειτνίασης είναι μία αρκετά εύρωστη και αποδοτική δομή η οποία μπορεί να
προσαρμόζεται κατάλληλα ανάλογα με την εφαρμογή.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 147


Αλγόριθμοι και Δομές Δεδομένων

4.2.4 Κατηγορίες γράφων


Στο κεφ. αυτό θα δούμε διαφορετικά είδη γράφων τα οποία απαντώνται σε μια ποικιλία
εφαρμογών.

Πολλαπλοί γράφοι
Πολλαπλοί (multiple) είναι οι γράφοι στους οποίους επιτρέπονται ανακυκλώσεις, δηλ.
ακμές με άκρα την ίδια κορυφή. Τυπικά, ένας πολλαπλός είναι ένας γράφος G=(V, E) όπου το
Ε περιέχει ακμές της μορφής (e, k) όπου:
• e ένα ζεύγος διαφορετικών κορυφών ή μόνο μία κορυφή ως άκρα
• k είναι ένας φυσικός αριθμός που χρησιμοποιείται για τον ορισμό του πλήθους των
ανακυκλώσεων σε μία κορυφή.

Παράδειγμα
Έστω ότι έχουμε τον γράφο G=(V, E), με V = {v1, v2, v3} και Ε={{(v1, v2), 0}, {( v1, v2), 1},
{(v1, v3), 0}, {(v3), 0},{(v2, v3), 0}}. Στην περίπτωση αυτή ο γράφος αποτελείται από 3 κορυφές
και 5 ακμές όπως φαίνεται και στο παρακάτω σχήμα:

Σχήμα 57: Παράδειγμα πολλαπλού γράφου

Ισομορφικοί γράφοι
Δύο γράφοι G’= (V’, E’) και G=(V, E) καλούνται ισομορφικοί (isomorphic) αν υπάρχει
αμφιμονοσύμαντη αντιστοιχία μεταξύ των συνόλων V’ και V τέτοια ώστε να διατηρούνται οι
σχέσεις γειτνίασης.
Στο παρακάτω σχήμα δίνονται δύο ισομορφικοί γράφοι.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 148


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 58: Παραδειγμα ισομορφικών γράφων

Επίπεδος γράφος
Ένας επίπεδος (planar) γράφος μπορεί να απεικονιστεί στο επίπεδο με τέτοιο τρόπο
ώστε οι ακμές του να τέμνονται μόνο στις κορυφές του. Η απεικόνιση αυτή φαίνεται στο δεξιό
γράφο του προηγούμενου σχήματος

Πλήρης γράφος
Ένας γράφος καλείται πλήρης (complete) όταν για κάθε ζεύγος κορυφών του περιέχει
μια ακμή. Ένας πλήρης γράφος με n κορυφές συμβολίζεται με Kn και έχει n(n-1)/2 ακμές. Οι
ισομορφικοί γράφοι του προηγούμενου σχήματος είναι πλήρεις (γράφοι K4)

Διμερής γράφος
Διμερής (bipartite) ονομάζεται ένας γράφος όταν οι κορυφές του μπορούν να χωριστούν
σε δύο ξένα μεταξύ τους σύνολα και όλες οι ακμές του έχουν το ένα άκρο τους στο πρώτο
σύνολο και το άλλο στο δεύτερο σύνολο.
Ένας διμερής γράφος είναι πλήρης όταν υπάρχουν ακμές που ενώνουν κάθε ζεύγος
κορυφών (από τα δύο ξένα σύνολα κορυφών). Ένας πλήρης διμερής γράφος των οποίων τα
δύο σύνολα κορυφών έχουν n και m κορυφές αντίστοιχα συμβολίζεται με Kn,m και έχει n*m
ακμές.
Παράδειγμα διμερή γράφου δίνεται στο επόμενο σχήμα:

Σχήμα 59: Παράδειγμα διμερή γράφου

Συμπληρωματικοί γράφοι
Δύο γράφοι G’= (V’, E’) και G= (V, E) καλούνται συμπληρωματικοί όταν έχουν το ίδιο
σύνολο κορυφών (V’ = V), E ∩ E’ = Ø και για οποιεσδήποτε δύο κορυφές v και u η ακμή {v,
u} ∈ E U E’.
Στο παρακάτω σχήμα δίνονται δύο συμπληρωματικοί γράφοι:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 149


Αλγόριθμοι και Δομές Δεδομένων

Σχήμα 60: Παράδειγμα συμπληρωματικών γράφων

Παρακάτω θα δούμε και άλλες σημαντικές κατηγορίες γράφων οι οποίες συνδέονται με


την διαπερασιμότητα ενός γράφου καθώς και με την συνεκτικότητα του, έννοιες που
περιγράφονται αμέσως μετά.

4.2.5 Διαπερασιμότητα
Έστω έχουμε έναν γράφο G= (V, E) με n κορυφές v1, v2,….,vn. Μονοπάτι (path) του G είναι
μία ακολουθία γειτονικών κορυφών. Το μονοπάτι τότε συνδέει την πρώτη και την τελευταία
κορυφή. Αν αυτές είναι ίδιες, το μονοπάτι καλείται κλειστό, διαφορετικά ανοικτό.
Το μήκος (length) ενός μονοπατιού είναι το σύνολο (πλήθος) των ακμών που περιέχει. Στην
περίπτωση που όλες οι κορυφές ενός μονοπατιού είναι μοναδικές (απαντώνται μόνο μία
φορά), τότε το μονοπάτι καλείται απλό (simple). Στην περίπτωση που όλες οι ακμές ενός
μονοπατιού είναι μοναδικές, τότε το μονοπάτι καλείται ίχνος (trace).
Ένα κλειστό απλό μονοπάτι με περισσότερες από δύο κορυφές καλείται κύκλος (cycle). Ένας
γράφος που δεν περιέχει κύκλους λέγεται άκυκλος (acyclic).
Η απόσταση (distance) μεταξύ δύο κορυφών είναι το μήκος του συντομότερου μονοπατιού
από τη μία στην άλλη.
Το επόμενο σχήμα διασαφηνίζει τις παραπάνω έννοιες:

Σχήμα 61: Επεξήγηση των βασικών εννοιών διαπερασιμότητας ενός γράφου

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 150


Αλγόριθμοι και Δομές Δεδομένων

Κύκλος Euler και γράφος Euler


Κύκλος Euler ενός γράφου είναι ένας κύκλος που περιλαμβάνει όλες τις ακμές του
γράφου ακριβώς μία φορά (‘μονοκονδυλιά’). Στην περίπτωση που ένας γράφος περιέχει έναν
κύκλο Εuler ονομάζεται γράφος Εuler.

Κύκλος Hamilton και γράφος Hamilton


Κύκλος Hamilton ενός γράφου είναι ένας κύκλος που περιλαμβάνει όλες τις κορυφές του
γράφου ακριβώς μία φορά. Στην περίπτωση που ένας γράφος περιέχει έναν κύκλο Hamilton
ονομάζεται γράφος Ηamilton.
Όπως φαίνεται παρακάτω, ο γράφος του προηγούμενου σχήματος είναι ταυτόχρονα
γράφος Euler και γράφος Hamilton:

Σχήμα 62: Κύκλος Euler και κύκλος Hamilton σε ένα γράφο

Για να είναι ένας γράφος γράφος Euler, θα πρέπει κάθε κορυφή του να έχει άρτιο βαθμό
(κάθε φορά που φτάνουμε μέσω κάποιας ακμής στην κορυφή v, θα πρέπει μέσω κάποιας
άλλης ακμής να μπορούμε να φύγουμε από την v). Είναι εύκολο επίσης να δείτε ότι κάθε
πλήρης γράφος είναι γράφος Hamilton (έχει πάντοτε κύκλο Hamilton). Eιδικότερα για τους
γράφους Hamilton ισχύουν τα εξής:
Έστω ένας γράφος G με n κορυφές. Ο G είναι γράφος Hamilton σε οποιαδήποτε από τις
ακόλουθες περιπτώσεις:
1. Αν όλες οι κορυφές του G έχουν βαθμό n-1.
2. Αν όλες οι κορυφές του G έχουν βαθμό ≥ n/2.
3. Αν το άθροισμα των βαθμών κάθε ζεύγους μη γειτονικών κορυφών είναι ≥ n (n ≥ 3).

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 151


Αλγόριθμοι και Δομές Δεδομένων

4.2.6 Συνεκτικότητα
Ένας γράφος λέγεται συνεκτικός (connected) όταν δύο οποιεσδήποτε κορυφές του
συνδέονται με ένα μονοπάτι.
Συνεκτική συνιστώσα (connected component) ενός γράφου G=(V, E) καλείται ένας
υπογράφος G’ που είναι συνεκτικός και για τον οποίο δεν υπάρχει άλλος συνεκτικός
υπογράφος του G που να έχει υπογράφο τον G’.
Στο παρακάτω σχήμα φαίνεται ένας συνεκτικός γράφος:

Σχήμα 63: Παράδειγμα συνεκτικού γράφου

Ένας συνεκτικός άκυκλος γράφος είναι ένα δένδρο.


Ένας γράφος είναι δισυνεκτικός ή διπλά συνεκτικός (dubly connected) όταν για κάθε
ζευγάρι κορυφών υπάρχουν δύο διαφορετικά μονοπάτια τα οποία τις συνδέουν.
Στο παρακάτω σχήμα φαίνεται ένας δισυνεκτικός γράφος.

Σχήμα 64: Παράδειγμα δισυνεκτικού γράφου

Μια παραπλήσια ιδιότητα των γράφων είναι η συνεκτικότητα κορυφών (vertex


connectivity) και η συνεκτικότητα ακμών (edge connectivity).
Έστω έχουμε έναν γράφο G. Τότε η συνεκτικότητα κορυφών κ(G) του γράφου
(συνεκτικότητα ως προς τις κορυφές του) είναι ο ελάχιστος αριθμός κορυφών που πρέπει να
αφαιρεθούν από τον γράφο έτσι ώστε ο υπογράφος που προκύπτει να είναι μη συνεκτικός ή
ένας τετριμμένος γράφος.
Η συνεκτικότητα ακμών λ(G) του γράφου είναι ο ελάχιστος αριθμός ακμών που πρέπει
να αναιρεθούν από τον γράφο έτσι ώστε ο υπογράφος του που προκύπτει να είναι μη
συνεκτικός ή ένας τετριμμένος γράφος.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 152


Αλγόριθμοι και Δομές Δεδομένων

Ισχύει το παρακάτω θεώρημα:


Αν d(G) είναι ο ελάχιστος βαθμός των κορυφών του G τότε ισχύει: κ(G)≤λ(G) ≤ d(G).

Παράδειγμα
Παρακάτω απεικονίζεται ένας γράφος στον οποίο κ(G)=2, λ(G)=3 και d(G)=3.

Επίσης, ένας γράφος G καλείται k-συνεκτικός ως προς τις κορυφές όταν κ(G) ≥k και
αντιστοίχως k-συνεκτικός ως προς τις ακμές όταν λ(G) ≥k.

4.2.7 Κατευθυνόμενοι γράφοι


Σε έναν κατευθυνόμενο (directed) γράφο οι ακμές του είναι προσανατολιζόμενες προς
μία κατεύθυνση. Ο προσανατολισμός αυτός ορίζεται με ένα βέλος. Εδώ επιτρέπονται
ανακυκλώσεις αλλά δεν επιτρέπονται πολλαπλές ακμές με τα ίδια άκρα και την ίδια
κατεύθυνση. Για παράδειγμα, ο γράφος του παρακάτω σχήματος είναι κατευθυνόμενος:

Σχήμα 65: Παράδειγμα κατευθυνόμενου γράφου

Ο ορισμός του βαθμού μιας κορυφής v επεκτείνεται και στους κατευθυνόμενους γράφους.
Έχουμε στην περίπτωση αυτή τον βαθμό εισόδου (in-degree) din(v) που αποτελεί το πλήθος
των ακμών που καταλήγουν σε μια κορυφή και τον βαθμό εξόδου (out-degree) dout(v) που
αποτελεί το πλήθος των ακμών που ξεκινούν από την κορυφή. Ο βαθμός της κορυφής είναι το
άθροισμα του βαθμού εισόδου και εξόδου της. Είναι εύκολο να δείτε ότι σε κάθε
κατευθυνόμενο γράφο ισχύει:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 153


Αλγόριθμοι και Δομές Δεδομένων

∑ d (v)= ∑ d
v∈V
in

v∈V
out(v) = |Ε|

H ιδιότητα της συνεκτικότητας απαντάται και στους κατευθυνόμενους γράφους. Ένας


κατευθυνόμενος γράφος είναι ασθενώς συνεκτικός (weakly connected) όταν για κάθε
ζεύγος κορυφών x και y υπάρχει ένα τουλάχιστον μονοπάτι είτε από τη x στην y είτε από την y
στη x. Ο γράφος είναι ισχυρά συνεκτικός (strongly connected) όταν για κάθε ζεύγος
κορυφών κάθε μία είναι προσεγγίσιμη από την άλλη.

Στο γράφο του επόμενου σχήματος υπάρχουν τρεις ισχυρά συνεκτικές συνιστώσες: {Α, Β, Γ,
Δ}, {Χ} και {Υ}.

Α B X

Δ Γ Y

Σχήμα 66: Ισχυρά συνεκτικές συνιστώσες ενός γράφου

4.2.6 Αναζήτηση κορυφών γράφου


Η λειτουργία της αναζήτησης ή διαπέρασης των κορυφών ενός γράφου χρησιμοποιείται
ως υπορουτίνα σε αρκετά γραφοθεωρητικά προβλήματα.
Έστω ένας μη κατευθυνόμενος γράφος G=(V, E) και μία ξεχωριστή κορυφή s του G.
Mπορούμε από την s να επισκεφτούμε όλες τις κορυφές που είναι προσεγγίσιμες από αυτήν
με τον παρακάτω γενικό αλγόριθμο:

Αλγόριθμος αναζήτησης κορυφών


1. Ξεκινούμε από την s και επισκεπτόμαστε μία γειτονική της
2. Αφού έχουμε επισκεφθεί την κορυφή v є V, συνεχίζουμε με μια γειτονική της
3. Όταν φτάσουμε σε κορυφή που έχουμε επισκεφτεί ήδη, επιστρέφουμε πίσω και συνεχί-
ζουμε με άλλη.
4. Ο αλγόριθμος τερματίζει όταν έχουμε επισκεφθεί όλες τις κορυφές που είναι
προσεγγίσιμες από την s

Ο παραπάνω αλγόριθμος παράγει ένα δένδρο αναζήτησης με ρίζα την κορυφή s και κόμβους
όλες τις προσεγγίσιμες από την s κορυφές του G. Αν ο G είναι συνεκτικός, τότε το δένδρο αυτό
ονομάζεται γεννητικό (spanning tree) επειδή καλύπτει ολόκληρο το γράφο (δηλ. μπορεί να
‘γεννήσει’ το γράφο).

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 154


Αλγόριθμοι και Δομές Δεδομένων

Η αναζήτηση των κορυφών ενός γράφου μπορεί να γίνει με δύο τρόπους ανάλογα με την
σειρά που επιλέγουμε να επισκεφθούμε τις κορυφές:
(α) Aναζήτηση πρώτα κατά πλάτος (Breadth-first search, ΒFS)
(β) Aναζήτηση πρώτα κατά βάθος (Depth-first search, DFS)

Aλγόριθμος BFS(G, s)
Στον αλγόριθμο BFS η σειρά επίσκεψης των κορυφών καθορίζεται από τις αποστάσεις τους
από την s: ο αλγόριθμος πρώτα επισκέπτεται όλες τις κορυφές σε απόσταση k από την s
πριν προχωρήσει στις κορυφές σε απόσταση k+1.

Στην υλοποίηση του αλγόριθμου χρησιμοποιείται η δομή της ουράς στην οποία
αποθηκεύονται οι γειτονικές κορυφές της τρέχουσας τις οποίες δεν έχουμε επισκεφθεί ακόμα.
Το δένδρο αναζήτησης του BFS περιέχει αρχικά μόνο τη ρίζα που είναι η κορυφή s. Στη
συνέχεια στο δένδρο προστίθενται μία – μία όλες οι κορυφές v που ο αλγόριθμος ανακαλύπτει
(επισκέπτεται για πρώτη φορά) πχ. από την κορυφή u μαζί με την αντίστοιχη ακμή (v, u). Στο
δένδρο αυτό η v είναι ο πατέρας (parent) της u. Δεδομένου ότι ο αλγόριθμος ανακαλύπτει
κάθε κορυφή του G μόνο μία φορά, ο πατέρας οποιασδήποτε κορυφής πλην της s είναι
μοναδικός. Στο δένδρο μπορούν να οριστούν επίσης και οι σχέσεις προγόνου – απογόνου
μιας κορυφής σε σχέση με τη ρίζα s: αν η x ανήκει στο μονοπάτι από τη ρίζα s προς την
κορυφή y, τότε η x καλείται πρόγονος της y και η y απόγονος της x.
Στο επόμενο σχήμα απεικονίζεται ένας γράφος και το δένδρο αναζήτησης που παράγει ο
αλγόριθμος BFS(G, s):

Σχήμα 67: BFS δένδρο αναζήτησης

Επειδή ακριβώς ο BFS(G, s) βρίσκει την απόσταση κάθε κορυφής από την κορυφή s,
μπορεί να χρησιμοποιηθεί για την εύρεση της ελάχιστης απόστασης δ(s, v) από την s στη v
η οποία είναι το ελάχιστο πλήθος ακμών στο μονοπάτι s → v.
Αν για την αναπαράσταση του γράφου χρησιμοποιήσουμε λίστες γειτνίασης, τότε
αποδεικνύεται ότι ο χρόνος εκτέλεσης του BFS είναι Ο(|V|+|E|).

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 155


Αλγόριθμοι και Δομές Δεδομένων

Aλγόριθμος DFS(G, s)
Στον αλγόριθμο αυτό ψάχνουμε όσο γίνεται ‘βαθύτερα’ στον G. Δηλ. η σειρά επίσκεψης των
κορυφών καθορίζεται από το ποια επισκέφτηκε ο αλγόριθμος τελευταία: αν αυτή ήταν η v, τότε
η επόμενη θα είναι κάποια γειτονική της v την οποία ακόμα δεν έχει επισκεφτεί.

Ο DFS αλγόριθμος είναι αναδρομικός και αρκετά πιο κομψός σε σχέση με τον BFS. Μας
δίνει δε πιο αναλυτική πληροφορία για τη δομή του γράφου όπως πχ. η παρουσία κύκλων.
Το αντίστοιχο δένδρο αναζήτησης ονομάζεται DFS δένδρο. Στο επόμενο σχήμα φαίνεται ένα
τέτοιο δένδρο για τον ίδιο γράφο που είχαμε και στο σχήμα 58:

Σχήμα 68: DFS δένδρο αναζήτησης

Και εδώ μπορεί να αποδειχθεί εύκολα ότι με τη χρήση λιστών γειτνίασης ο χρόνος
εκτέλεσης του DFS είναι επίσης Ο(|V|+|E|).

4.2.9 Βασικά γραφοθεωρητικά προβλήματα


Στο κεφάλαιο αυτό θα δούμε εν συντομία κάποια από τα βασικότερα προβλήματα γράφων
τα οποία απαντώνται σε πλήθος εφαρμογών. Διακρίνονται δε σε δύο μεγάλες κατηγορίες:
α) Αποδοτικά επιλύσιμα προβλήματα, δηλ. υπάρχει πολυωνυμικός αλγόριθμος για την
επίλυσή τους και
β) Υπολογιστικά δύσκολα προβλήματα, δηλ. προβλήματα για τα οποία δεν έχει βρεθεί ως
τώρα πολυωνυμικός αλγόριθμος επίλυσης όταν η είσοδος είναι ένας γενικός γράφος και
μόνο για απλές περιπτώσεις γράφων είναι εφικτή η αποδοτική τους επίλυση.

Προβλήματα που επιλύονται σε πολυωνυμικό χρόνο

Συντομότερα μονοπάτια

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 156


Αλγόριθμοι και Δομές Δεδομένων

Για το πρόβλημα αυτό υπάρχουν δύο βασικές παραλλαγές:

Α. Συντομότερα μονοπάτια από μια κορυφή-πηγή (Single source shortest paths)


Eίσοδος: Ένας βεβαρημένος κατευθυνόμενος ή μη γράφος G= (V, Ε) με μη αρνητικά βάρη
ακμών και μία κορυφή-πηγή s ∈ V.
Έξοδος: Συντομότερα μονοπάτια (δηλ. μονοπάτια όπου το άθροισμα των βαρών όλων των
ακμών τους είναι ελάχιστο) από την s στις υπόλοιπες κορυφές του G.

Στο επόμενο σχήμα φαίνονται η είσοδος και η έξοδος του αντίστοιχου αλγόριθμου επίλυσης:

Β. Συντομότερα μονοπάτια για όλα τα ζεύγη κορυφών (all pairs shortest paths)
Στην παραλλαγή αυτή το ζητούμενο είναι η εύρεση των συντομότερων μονοπατιών μεταξύ
όλων των ζευγών κορυφών (x, y) ∈ VxV.
Τα μη αρνητικά βάρη ακμών εξασφαλίζουν ότι ο γράφος εισόδου δεν έχει κύκλους με
αρνητικό βάρος (διαφορετικά το συντομότερο μονοπάτι μεταξύ δύο κορυφών του κύκλου θα
περιελάμβανε ένα άπειρο πλήθος επαναλήψεων του αρνητικού αυτού κύκλου). Για την
επίλυση του προβλήματος ο πλέον κλασικός αλγόριθμος είναι αυτός του Dijkstra, γνωστός
από το 1959!
Η φυσική σημασία των βαρών των ακμών εξαρτάται από τη πρόβλημα που καλούμαστε
να επιλύσουμε στην πράξη: κάθε βάρος μπορεί να αντιστοιχεί στην απόσταση μεταξύ δύο
κορυφών ή στο χρόνο μετάβασης από τη μία κορυφή στην άλλη για παράδειγμα. Γι΄ αυτόν
ακριβώς το λόγο το συγκεκριμένο πρόβλημα έχει ένα εντυπωσιακά μεγάλο πλήθος
εφαρμογών. Οι πιο προφανείς αφορούν σε μεταφορικά ή επικοινωνιακά δίκτυα, όπως πχ.
υπολογισμός των βέλτιστων διαδρομών για την διανομή ενός προϊόντος από μια αποθήκη σε
ένα δίκτυο πόλεων ή τη δρομολόγηση πακέτων σε έναν προορισμό σε ένα δίκτυο δεδομένων.
Μια άλλη ειδική εφαρμογή είναι η εξής: Υποθέστε ότι θέλουμε να ζωγραφίσουμε ένα
γράφο. Το κέντρο της σελίδας θα πρέπει να αντιστοιχεί στο ‘κέντρο’ του γράφου με ό,τι
σημαίνει αυτό. Ένας καλός ορισμός του κέντρου είναι η κορυφή του γράφου που
ελαχιστοποιεί τη μέγιστη απόσταση προς οποιαδήποτε άλλη κορυφή του γράφου. Η εύρεση
αυτού του κέντρου απαιτεί γνώση της απόστασης (συντομότερου μονοπατιού) μεταξύ όλων
των ζευγών κορυφών του γράφου.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 157


Αλγόριθμοι και Δομές Δεδομένων

Toπολογική διάταξη (Topological ordering)


Eίσοδος: Ένας κατευθυνόμενος άκυκλος γράφος G=(V, E).
Έξοδος: Μια γραμμική διάταξη των κορυφών του G έτσι ώστε για κάθε ακμή (x, y) ∈ E η
κορυφή x να προηγείται της κορυφής y.

Η τοπολογική διάταξη προκύπτει ως ένα φυσικό υποπρόβλημα στους περισσότερους


αλγόριθμους για κατευθυνόμενους άκυκλους γράφους (directed acyclic graphs, DAGs). Μια
κλασική εφαρμογή της τοπολογικής διάταξης είναι η δρομολόγηση εργασιών (πχ.
βιομηχανικών εργασιών σε μια μηχανή ή υπολογιστικών διεργασιών σε έναν εξυπηρετητή)
όπου ορισμένες εργασίες πρέπει να εκτελεστούν πριν από κάποιες άλλες. Οι περιορισμοί
αυτοί στη σειρά εκτέλεσης μπορούν να παρασταθούν με ένα DAG και οποιαδήποτε
τοπολογική διάταξη καθορίζει τη σωστή σειρά δρομολόγησης των εργασιών ώστε να
ικανοποιούνται όλοι οι περιορισμοί.

Ελάχιστο γεννητικό δένδρο (Minimum spanning tree)


Eίσοδος: Ένας βεβαρημένος μη κατευθυνόμενος γράφος G=(V, E).
Έξοδος: Ένας υπογράφος του G με το ελάχιστο βάρος που σχηματίζει ένα δένδρο (ή ένα
δάσος από δένδρα σε ένα μη συνεκτικό γράφο) στο V.

Το εν λόγω πρόβλημα έχει μεγάλη ιστορία: ο πρώτος αλγόριθμος για την επίλυσή του
δόθηκε το 1926!
Το ελάχιστο γεννητικό δένδρο έχει εφαρμογές σε όλα σχεδόν τα προβλήματα σχεδιασμού
δικτύων. Υποθέστε για παράδειγμα ότι μια τηλεφωνική εταιρία θέλει να συνδέσει όλα τα

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 158


Αλγόριθμοι και Δομές Δεδομένων

τηλεφωνικά της κέντρα σε μια πόλη. Τότε το ελάχιστο γεννητικό δένδρο καθορίζει το πιο
οικονομικό σχήμα διασύνδεσης, δηλ. αυτό με τα λιγότερα μέτρα καλωδίου.
Γενικά, τα ελάχιστα γεννητικά δένδρα είναι σημαντικά για πολλούς λόγους:
- Μπορούν να υπολογιστούν εύκολα και γρήγορα και παράγουν ένα αραιό υπογράφο
που δίνει αρκετή γνώση για την τοπολογία του αρχικού γράφου.
- Παρέχουν έναν τρόπο για τον εντοπισμό clusters σε ένα σύνολο σημείων. Η διαγραφή
των ακμών μεγάλου βάρους από το ελάχιστο γεννητικό δένδρο δίνει τις συνεκτικές
συνιστώσες οι οποίες ορίζουν φυσικά clusters μεταξύ των σημείων.
- Μπορούν να χρησιμοποιηθούν ώστε να παράγουν προσεγγιστικές λύσεις σε
υπολογιστικά δύσκολα προβλήματα όπως πχ. το πρόβλημα του πλανόδιου πωλητή το
οποίο παρουσιάζεται στα επόμενα.

Συνεκτικές συνιστώσες (Connected components)


Eίσοδος: Ένας μη κατευθυνόμενος γράφος G=(V, E).
Έξοδος: Οι συνεκτικοί υπογράφοι του G.
Για κατευθυνόμενους γράφους ενδιαφέρον έχει η εύρεση των ισχυρά συνεκτικών συνιστωσών
του γράφου, όπως απεικονίζεται στο ακόλουθο σχήμα:

Και αυτό το πρόβλημα είναι σημαντικό επειδή αποτελεί το αναγκαίο βήμα


προεπεξεργασίας για κάθε σχεδόν γραφοαλγόριθμο. Απαντάται δε σε πολλές εφαρμογές
όπως για παράδειγμα στον εντοπισμό clusters σε ένα σύνολο στοιχείων. Μπορούμε να
αναπαραστήσουμε κάθε στοιχείο με μία κορυφή και να προσθέσουμε μια μη κατευθυνόμενη
ακμή μεταξύ κάθε ζευγαριού στοιχείων τα οποία χαρακτηρίζονται ως ‘ομοειδή’. Τότε οι
συνεκτικές συνιστώσες του γράφου που προκύπτει αντιστοιχούν σε διαφορετικές κλάσεις
στοιχείων.

Συνεκτικότητα ακμών / κορυφών (Edge / vertex connectivity)


Eίσοδος: Ένας μη κατευθυνόμενος γράφος G=(V, E).
Έξοδος: To μικρότερο υποσύνολο ακμών (κορυφών αντίστοιχα) η απομάκρυνση των οποίων
κάνει τον G μη συνεκτικό.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 159


Αλγόριθμοι και Δομές Δεδομένων

Στο επόμενο σχήμα παρουσιάζεται το πρόβλημα υπολογισμού της συνεκτικότητας των ακμών
ενός γράφου.

Όπως είδαμε στην παρ. 4.2.6 συνεκτικότητα ακμών και κορυφών είναι δύο ποσότητες που
συνδέονται και ο ελάχιστος βαθμός των κορυφών του γράφου αποτελεί ένα άνω φράγμα και
για τις δύο.
Μια ενδιαφέρουσα παραλλαγή του γενικού προβλήματος είναι η εύρεση του μικρότερου
υποσυνόλου ακμών (κορυφών) η απομάκρυνση των οποίων για δεδομένα s, t ∈ V θα
διαχωρίσει την κορυφή s από την κορυφή t, πρόβλημα που συνδέεται άμεσα με την
αξιοπιστία δικτύων.

Κύκλος Euler
Eίσοδος: Ένας μη κατευθυνόμενος γράφος G=(V, E).
Έξοδος: Ένας κύκλος Euler.

Το πρόβλημα αυτό διατυπώθηκε από τον μεγάλο μαθηματικό Leonard Euler ο οποίος το
1736 έλυσε το πρόβλημα με τις επτά γέφυρες της πόλης Κönigsberg της Ρωσίας (σημερινό
Καλίνιγκραντ). Ο Euler παρατήρησε ότι μια αναγκαία συνθήκη για να έχει ένας γράφος G
κύκλο Euler είναι κάθε κορυφή του G να έχει άρτιο βαθμό.
Η σημασία του προβλήματος είναι προφανής σε προβλήματα διαδρομών όταν κανείς
καλείται να σχεδιάσει πάνω στο χάρτη μιας πόλης βέλτιστες διαδρομές απορριμματοφόρων,
εκχιονιστικών μηχανημάτων, ταχυδρομικών μέσων κλπ. Σε όλες τις εφαρμογές πρέπει να
επισκεφτούμε κάθε δρόμο της πόλης τουλάχιστον μία φορά. Για λόγους αποδοτικότητας
θέλουμε να ελαχιστοποιείται ο συνολικός χρόνος οδήγησης ή ισοδύναμα η συνολική

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 160


Αλγόριθμοι και Δομές Δεδομένων

απόσταση που διανύουμε, το οποίο επιτυγχάνεται αν περάσουμε από κάθε δρόμο μία φορά,
δηλ. κινηθούμε κατά μήκος ενός κύκλου Euler.

Μέγιστο ταίριασμα (Μaximum matching)


Eίσοδος: Ένας (βεβαρυμένος) διμερής μη κατευθυνόμενος γράφος G=(V, E).
Έξοδος: To μεγαλύτερο υποσύνολο Μ του Ε έτσι ώστε κάθε κορυφή του V να είναι άκρο μιας
το πολύ ακμής του Μ.

Παρατηρήστε ότι τα μέγιστα ταιριάσματα μπορεί να είναι περισσότερα από ένα. Όταν
έχουμε βεβαρημένο γράφο θέλουμε το άθροισμα των βαρών των ακμών του ταιριάσματος να
είναι μέγιστο.
Χαρακτηριστική εφαρμογή του προβλήματος έχουμε στο εξής παράδειγμα: Υποθέστε ότι
έχουμε ένα σύνολο υπαλλήλων καθένας εκ των οποίων μπορεί να διεκπεραιώσει κάποιες από
τις εργασίες ενός συνόλου εργασιών που πρέπει να εκτελεστούν. Αναζητούμε μια ανάθεση
εργασιών τέτοια ώστε κάθε εργασία να ανατεθεί σε ένα μοναδικό υπάλληλο. Κάθε
αντιστοίχηση μεταξύ ενός υπαλλήλου και μιας εργασίας που μπορεί να κάνει ορίζει μια ακμή
και αυτό που μας ενδιαφέρει είναι το σύνολο των ακμών οι οποίες δεν μοιράζονται τον ίδιο
υπάλληλο ή την ίδια εργασία με κάποιες άλλες, δηλ. ένα ταίριασμα.

Ροή δικτύου (Network flow)


Eίσοδος: Ένας βεβαρημένος μη κατευθυνόμενος γράφος G=(V, E) όπου κάθε ακμή (x, y) ∈ E
έχει χωρητικότητα c(x, y). Μία ορυφή-αρχή s και μία κορυφή-τέλος t.
Έξοδος: Η μέγιστη ποσότητα ροής που μπορεί να δρομολογηθεί από την s στην t χωρίς να
υπερβούμε τις χωρητικότητες των ακμών του G.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 161


Αλγόριθμοι και Δομές Δεδομένων

Η εύρεση του πιο αποδοτικού σε κόστος τρόπου για τη δρομολόγηση προϊόντων από ένα
σύνολο εργοστασίων σε ένα σύνολο αποθηκών ορίζει ένα πρόβλημα ροής. Το πρόβλημα
απαντάται ακόμη σε εφαρμογές ανάθεσης πόρων σε δίκτυα επικοινωνίας, δρομολόγησης
εργασιών κλπ.
Η πραγματική αξία του προβλήματος συνίσταται στο γεγονός ότι πολλές εφαρμογές
δυναμικού προγραμματισμού είναι δυνατόν να μοντελοποιηθούν ως προβλήματα ροής
δικτύου και μπορούν να χρησιμοποιηθούν ειδικοί γραφοαλγόριθμοι για την αποδοτική τους
επίλυση. Σε προβλήματα ροής δικτύου μπορούν να αναχθούν για παράδειγμα βασικά
γραφοθεωρητικά προβλήματα όπως διμερή ταιριάσματα, συντομότερα μονοπάτια και
συνεκτικότητα ακμών / κορυφών.

Έλεγχος επιπεδότητας και ένθεση (Planarity testing and embedding)


Eίσοδος: Ένας μη κατευθυνόμενος γράφος G=(V, E).
Έξοδος: Αν ο G είναι επίπεδος, απεικόνισή του με τέτοιο τρόπο ώστε να μην υπάρχουν
τεμνόμενες ακμές.

Η ένθεση ενός γράφου βοηθάει να κατανοήσουμε καλύτερη τη δομή του. Οι γράφοι που
χρησιμοποιούνται για να αναπαραστήσουν οδικά δίκτυα ή ολοκληρωμένα κυκλώματα είναι
επίπεδοι επειδή ορίζονται από επίπεδες δομές.
Η πιο σημαντική ιδιότητα ενός επίπεδου γράφου είναι ότι είναι αραιός. Συγκεκριμένα
ισχύει |Ε| ≤ 3|V|-6 (φόρμουλα του Euler), δηλ. κάθε επίπεδος γράφος έχει γραμμικό πλήθος
ακμών και επιπλέον πρέπει να περιέχει μία κορυφή βαθμού το πολύ 5. Τόσο το πρόβλημα του
ελέγχου αν ένας γράφος είναι επίπεδος όσο και αυτό της ένθεσης (εύρεσης μιας επίπεδης
απεικόνισης) μπορούν να επιλυθούν σε γραμμικό χρόνο.

Υπολογιστικά δύσκολα προβλήματα

Κύκλος Hamilton

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 162


Αλγόριθμοι και Δομές Δεδομένων

Eίσοδος: Ένας μη κατευθυνόμενος γράφος G= (V, Ε).


Έξοδος: Ένας κύκλος Hamilton στον G.

Πρόκειται για ένα από τα γνωστότερα δύσκολα προβλήματα. Δείτε για παράδειγμα ότι για
τον πλήρη γράφο Kn με n>2 έχουμε (n-1)!/2 δυνατές λύσεις.

Το πρόβλημα του πλανόδιου πωλητή (Traveling salesman problem)


Eίσοδος: Ένας βεβαρημένος μη κατευθυνόμενος γράφος G=(V, Ε).
Έξοδος: Ένας κύκλος Hamilton ελαχίστου κόστους στον G.

Το πρόβλημα του πλανόδιου πωλητή είναι ίσως το πιο διάσημο δύσκολο πρόβλημα το
οποίο όμως μπορεί να προσεγγιστεί με υποβέλτιστους ευριστικούς αλγόριθμους. Το
προηγούμενο πρόβλημα της εύρεσης ενός κύκλoυ Hamilton είναι ειδική περίπτωση αυτού του
προβλήματος όπου κάθε ακμή του γράφου έχει κόστος 1 ενώ για κάθε ζεύγος μη γειτονικών
κορυφών θέτουμε το κόστος ίσο με ∞.

Χρωματισμός ακμών (Edge coloring)


Eίσοδος: Ένας μη κατευθυνόμενος γράφος G= (V, Ε).
Έξοδος: Το μικρότερο σύνολο χρωμάτων με το οποίο μπορούμε να χρωματίσουμε τις ακμές
του G έτσι ώστε οι ακμές ίδιου χρώματος να μην μοιράζονται την ίδια κορυφή.
Για το χρωματισμό των ακμών του γράφου του παρακάτω σχήματος απαιτούνται τρία
χρώματα:

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 163


Αλγόριθμοι και Δομές Δεδομένων

Το ελάχιστο πλήθος χρωμάτων για το χρωματισμό των ακμών του G καλείται χρωματικός
δείκτης (chromatic index) και συμβολίζεται με χ(G). Παρατηρήστε ότι σε ένα κύκλο G άρτιου
μήκους έχουμε χ(G)=2, ενώ σε ένα κύκλο περιττού μήκους χ(G)=3. Σε ένα ταίριασμα G το
χ(G)=1.
Το πρόβλημα του χρωματισμού ακμών απαντάται σε εφαρμογές δρομολόγησης όπου
κανείς θέλει να ελαχιστοποιήσει το πλήθος των συγκρούσεων κατά την εκτέλεση ενός
συνόλου εργασιών. Για παράδειγμα, υποθέστε ότι θέλουμε να παράγουμε ένα πρόγραμμα
συναντήσεων μιας ομάδας ατόμων, όπου τα άτομα συναντώνται ανά δύο και κάθε συνάντηση
κρατά μία ώρα. Όλες οι συναντήσεις μπορούν να δρομολογηθούν σε διαφορετικές ώρες για ν’
αποφύγουμε τις συγκρούσεις αλλά θα κερδίσουμε χρόνο αν κάποιες ανεξάρτητες συναντήσεις
δρομολογηθούν ταυτόχρονα. Κατασκευάζουμε λοιπόν έναν γράφο G του οποίου κάθε κορυφή
αντιστοιχεί σε ένα άτομο και κάθε ακμή συνδέει δύο άτομα που θέλουν να συναντηθούν. Τότε
ο χρωματισμός των ακμών του G μας δίνει τη ζητούμενη δρομολόγηση. Τα διαφορετικά
χρώματα παριστάνουν διαφορετικές χρονικές περιόδους στη δρομολόγηση και όλες οι
συναντήσεις του ίδιου χρώματος λαμβάνουν χώρα ταυτόχρονα.

Κάλυψη κορυφών (Vertex cover)


Eίσοδος: Ένας μη κατευθυνόμενος γράφος G= (V, Ε).
Έξοδος: Το μικρότερο υποσύνολο S του V έτσι ώστε κάθε ακμή του E έχει τουλάχιστον ένα
άκρο από το S.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 164


Αλγόριθμοι και Δομές Δεδομένων

Η κάλυψη κορυφών είναι ειδική περίπτωση του γενικού προβλήματος κάλυψης συνόλων
(set cover problem) το οποίο παίρνει ως είσοδο μία αυθαίρετη συλλογή C από σύνολα C1, C2,
…, Cn που περιέχουν στοιχεία από ένα σύμπαν U = {1, 2, …, m} και παράγει στην έξοδο το
μικρότερο υποσύνολο από σύνολα της C των οποίων η ένωση δίνει το U. Η αναγωγή του
προβλήματος κάλυψης κορυφών στο πρόβλημα της κάλυψης συνόλων γίνεται ως εξής: Το U
θα περιέχει όλες τις ακμές του G και κάθε Ci θα περιέχει τις ακμές που έχουν άκρο την κορυφή
i. Tότε το υποσύνολο κορυφών S ορίζει μία κάλυψη κορυφών στο γράφο G αν και μόνον αν τα
αντίστοιχα σύνολα Ci που έχουν οριστεί για τις κορυφές του S ορίζουν μία κάλυψη συνόλων
για το U. Παρόλα αυτά, επειδή κάθε ακμή του G μπορεί να ανήκει μόνο σε δύο διαφορετικά
σύνολα Ci, κάθε στιγμιότυπο του προβλήματος της κάλυψης κορυφών είναι πιο απλό από το
γενικό πρόβλημα της κάλυψης συνόλων.
Για διμερείς γράφους το πρόβλημα της κάλυψης κορυφών είναι ισοδύναμο με το
πρόβλημα του μέγιστου ταιριάσματος και μπορεί να επιλυθεί σε πολυωνυμικό χρόνο.

Κλίκα (Clique)
Eίσοδος: Ένας μη κατευθυνόμενος γράφος G= (V, Ε).
Έξοδος: Το μεγαλύτερο υποσύνολο S του V έτσι για κάθε ζεύγος κορυφών x, y ∈ S η ακμή
(x, y) ∈ E.

Παρατηρήστε ότι μια κλίκα με m κορυφές είναι ο πλήρης γράφος Κm. Ο εντοπισμός
clusters από ομοειδή αντικείμενα ανάγεται συχνά στο πρόβλημα της εύρεσης μεγάλων κλικών
σε γράφους. Σκεφτείτε ένα γράφο του οποίου οι κορυφές αντιστοιχούν σε άτομα, πχ.
συμμαθητές μιας τάξης, και οι ακμές του συνδέουν ζευγάρια από άτομα που είναι φίλοι. Μια
κλίκα μεταξύ των μαθητών της τάξης με την πραγματική της σημασία αντιστοιχεί τώρα σε μια
κλίκα με γραφοθεωρητικούς όρους στο γράφο που αναπαριστά τις σχέσεις φιλίας μεταξύ των
μαθητών.

Ισομορφισμός γράφων (Graph isomorphism)


Eίσοδος: Δύο μη κατευθυνόμενοι γράφοι G1 και G2.
Έξοδος: Mία αντιστοίχηση f των κορυφών του G1 στις κορυφές του G2 έτσι ώστε για όλες τις
ακμές η ακμή (x, y) ανήκει στον G1 αν και μόνο αν η ακμή (f(x), f(y)) ανήκει στον G2.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 165


Αλγόριθμοι και Δομές Δεδομένων

Ο ισομορφισμός γράφων ελέγχει αν δύο γράφοι είναι πραγματικά ίδιοι. Υποθέστε ότι μας
δίνεται μία συλλογή από γράφους και πρέπει να εκτελέσουμε μια πράξη σε κάθε έναν από
αυτούς. Αν βρούμε τους γράφους που είναι ισομορφικοί, τότε μπορούμε να αγνοήσουμε τα
αντίγραφα για ν΄ αποφύγουμε περιττή εργασία.

4.3 Διαχείριση ξένων συνόλων και δομές UNION-FIND


Σε αρκετές εφαρμογές απαιτείται η ομαδοποίηση n διακεκριμένων στοιχείων σε μια
συλλογή από ξένα μεταξύ τους σύνολα (διαμέριση). Οι βασικές πράξεις που ορίζονται σε
τέτοιου είδους δομές είναι:
• Μakeset(x) η οποία δημιουργεί ένα καινούργιο μονοσύνολο με το στοιχείο x. Εφόσον τα
σύνολα είναι ξένα μεταξύ τους απαιτούμε το στοιχείο x να μην ανήκει ήδη σε κάποιο
σύνολο.
• Union(x, y) που ενώνει τα δυναμικά σύνολα που περιέχουν τα x και y σε ένα νέο σύνολο
καταστρέφοντας έτσι τα προηγούμενα. Συνήθως, κάθε σύνολο φέρει μια ετικέτα (label)
που είναι το όνομα κάποιου στοιχείου του συνόλου που καλείται αντιπρόσωπος. Σε μια
πράξη Union() το νέο σύνολο που προκύπτει θα μπορούσε να πάρει είτε το όνομα του
συνόλου του x ή του y ή ένα νέο όνομα. Παρακάτω υποθέτουμε ότι το νέο σύνολο παίρνει το
όνομα του y.
• Find(x) που επιστρέφει το (ένα δείκτη στο) όνομα του συνόλου που περιέχει το x.
Υποθέτουμε ότι οι θέσεις των στοιχείων που μετέχουν σε μια πράξη μέσα στη δομή είναι
γνωστές, δίνονται δηλ. οι δείκτες στα στοιχεία της δομής. Για λόγους ευκολότερης κατανόησης
υποθέτουμε επίσης για τη συνέχεια ότι τα στοιχεία μας είναι φυσικοί αριθμοί από το σύνολο U
= {1, 2, 3, …, n} το οποίο καλείται σύμπαν (universe).
Oι UNION-FIND δομές χρησιμοποιούνται στην επίλυση αρκετών γραφοθεωρητικών
προβλημάτων που περιγράφηκαν στο προηγούμενο κεφάλαιο όπως η εύρεση των συνεκτικών
συνιστωσών ενός μη κατευθυνόμενου γράφου, ο υπολογισμός της ροής μέγιστου ή
ελάχιστου κόστους σε ένα δίκτυο κλπ. Το γεγονός αυτό από μόνο του κάνει τις συγκεκριμένες
δομές σημαντικές.

Παράδειγμα
Στη συνέχεια παρουσιάζουμε έναν αλγόριθμο για την εύρεση των συνεκτικών
συνιστωσών του μη κατευθυνόμενου γράφου G = (V, E) o οποίος κάνει χρήση των τριών
πράξεων Makeset(), Union() και Find() oι οποίες υλοποιούνται πάνω σε κάποια δομή UNION-

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 166


Αλγόριθμοι και Δομές Δεδομένων

FIND. Υπενθυμίζουμε ότι σε μια συνεκτική συνιστώσα κάθε ζεύγος κορυφών συνδέεται μέσω
ενός μονοπατιού.
Έστω ότι το σύνολο των κορυφών του G είναι το V = {1, 2, 3, …, n} για κάποιο ορισμένο n.
Για την αποθήκευση του G στον υπολογιστή μπορούμε να χρησιμοποιήσουμε είτε έναν
πίνακα ή λίστες γειτνίασης αλλά στον αλγόριθμο που ακολουθεί δεν γίνεται οποιαδήποτε
υπόθεση για τη δομή αποθήκευσης.

Αλγόριθμος Συνεκτικές Συνιστώσες (G)


Δεδομένα // G = (V, E): μη κατευθυνόμενος γράφος //
Αρχή
Για v = 1 Μέχρι n με Βήμα 1
Μakeset(v)
Tέλος Επανάληψης
Για όλες τις ακμές (x, y) ∈ E
Αν Find(x) ≠ Find(y) Τότε
Union(x, y)
Tέλος Αν
Tέλος Επανάληψης
Αποτελέσματα // Διαμέριση του συνόλου V σε ξένα υποσύνολα που αντιστοιχούν
στις συνεκτικές συνιστώσες του G //
Tέλος Συνεκτικές Συνιστώσες

Ο αλγόριθμος αρχικά κατασκευάζει για κάθε κορυφή v του G το μονοσύνολο {v}. Στη
συνέχεια, για κάθε ακμή (x, y) του G για την οποία οι κορυφές x, y ανήκουν σε διαφορετικά
σύνολα της UNION-FIND δομής, ενώνει τα δύο αυτά σύνολα αφού οι x, y ανήκουν στην ίδια
συνεκτική συνιστώσα του G. Μετά την επεξεργασία όλων των ακμών του γράφου, δύο
οποιεσδήποτε κορυφές θα ανήκουν στην ίδια συνεκτική συνιστώσα αν και μόνο αν ανήκουν
στο ίδιο σύνολο της UNION-FIND δομής. Ο επόμενος αλγόριθμος ελέγχει αν οι κορυφές v, u
ανήκουν στην ίδια συνεκτική συνιστώσα του G:

Αλγόριθμος Ίδια συνεκτική συνιστώσα (v, u)


Δεδομένα // v, u: ακέραιοι //
Αρχή
Αν Find(u) = Find(u) Τότε επέστρεψε (ΤRUE)
Aλλιώς επέστρεψε (FALSE)
Tέλος Αν
Αποτελέσματα // ΤRUE αν οι v, u ανήκουν στην ίδια συνεκτική συνιστώσα και
FALSE διαφορετικά //
Tέλος Ίδια συνεκτική συνιστώσα

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 167


Αλγόριθμοι και Δομές Δεδομένων

4.3.1 Υλοποίηση UNION - FIND δομής με λίστες


Για την υλοποίηση μιας UNION-FIND δομής ένας εύκολος τρόπος είναι να
χρησιμοποιήσουμε διασυνδεδεμένες λίστες όπως φαίνεται στο επόμενο σχήμα:

Σχήμα 69: Αναπαράσταση συνόλων με τη χρήση συνδεδεμένων λιστών

Υποθέτοντας ότι το όνομα κάθε συνόλου αντιστοιχεί στο όνομα του πρώτου στοιχείου της
λίστας (αντιπρόσωπος του συνόλου), τότε κάθε στοιχείο της λίστας περιέχει δύο δείκτες: ο
ένας δείχνει στο επόμενό του και ο άλλος στον αντιπρόσωπο.
Στην παραπάνω δομή η πράξη Find(x) εκτελείται σε Ο(1) χρόνο ακολουθώντας το δείκτη
από τον κόμβο που περιέχει το x στον αντιπρόσωπο της λίστας. Για μια πράξη Union(x, y)
όμως θα πρέπει να προσθέσουμε τη λίστα που περιέχει το y στο τέλος αυτής με το x και να
ενημερώσουμε τους δείκτες της δεύτερης λίστας να δείχνουν το νέο αντιπρόσωπο που παίρνει
χρόνο O(μήκος της λίστας).
Έστω ότι έχουμε n στοιχεία x1, x2, …, xn στη δομή και εκτελούμε m συνολικά πράξεις.
Έστω επίσης ότι n = m/2+1 και q = m – n = m/2-1. Eκτελούμε τώρα n Μakeset(xi) πράξεις για
κάθε ένα από τα n στοιχεία και στη συνέχεια q Union(xi, xi+1) πράξεις για i = 1, 2, …, q-1. Ο
συνολικός χρόνος για τις n Μakeset() πράξεις είναι O(n). Η i-οστή Union() ενημερώνει τους
δείκτες i στοιχείων και επομένως για τις q Union() ξοδεύουμε συνολικό χρόνο O(1 + 2 + … + q-
1) = O(q2). Eπομένως, ο χρόνος για τις m πράξεις είναι O(n+q2) = O(m2) αφού n = O(m) και q
= O(m). Άρα, κάθε μία από τις m πράξεις έχει κατανεμημένο χρόνο Ο(m). Παρατηρείστε ότι
κάθε πράξη Union() μειώνει το πλήθος των συνόλων κατά 1 και άρα μπορούμε να έχουμε το
πολύ n-1 Union() από τις οποίες προκύπτει ένα μόνο σύνολο n στοιχείων.
Οι παραπάνω χρόνοι μπορούν να βελτιωθούν αν κατά την πράξη Union() προσθέτουμε
κάθε φορά τη μικρότερη λίστα στο τέλος της μεγαλύτερης (αυτό απαιτεί να αποθηκεύουμε
στον κόμβο του αντιπροσώπου και το μήκος της λίστας του συνόλου). Ο ευριστικός αυτός
κανόνας λέγεται weighted union rule και για μια ακολουθία από m Makeset(), Union() και
Find() πράξεις από τις οποίες οι n είναι Μakeset() απαιτεί χρόνο Ο(m + nlogn) αφού κάθε
Makeset() ή Find() κοστίζει Ο(1) χρόνο και η ενημέρωση των δεικτών για ένα στοιχείο σε όλες
τις ≤ n-1 Union() γίνεται logn το πολύ φορές (σε κάθε Union() το στοιχείο αυτό τοποθετείται σε
μια λίστα με τουλάχιστον διπλάσιο μέγεθος).

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 168


Αλγόριθμοι και Δομές Δεδομένων

4.3.2 Υλοποίηση UNION - FIND δομής με δένδρα


Μια πιο αποδοτική υλοποίηση της δομής UNION-FIND χρησιμοποιεί δένδρα (έχουμε ένα
δάσος από δένδρα) όπου κάθε κόμβος δείχνει στον πατέρα του. Η ρίζα του δένδρου περιέχει
τον αντιπρόσωπο του συνόλου και δείχνει στον εαυτό της.
Στο παρακάτω σχήμα η αριστερή εικόνα δείχνει δύο τέτοια δένδρα και η δεξιά το
αποτέλεσμα μιας Union():

Σχήμα 70: Αναπαράσταση συνόλων με τη χρήση δένδρων

Προφανώς και η δομή αυτή από μόνη της δεν είναι αποδοτική αφού τα μονοπάτια των
δένδρων μπορούν να γίνουν γραμμικά σε μήκος. Έτσι, μια Find(x) πράξη που εκτελείται
ανεβαίνοντας το μονοπάτι από τον κόμβο του x προς τη ρίζα του δένδρου κοστίζει χρόνο Ο(n).
H απόδοση της δομής μπορεί όμως να βελτιωθεί σημαντικά αν χρησιμοποιήσουμε δύο
ευριστικούς κανόνες, τον union by rank rule και το path compression.
O union by rank κανόνας είναι παρόμοιος με τον weighted union που είδαμε νωρίτερα
αλλά αντί για τα μεγέθη των δένδρων χρησιμοποιούμε ως κριτήριο τα ύψη τους. Η ιδέα είναι
στις πράξεις Union() να κάνουμε τη ρίζα του δένδρου με το μικρότερο ύψος να δείχνει στη
ρίζα αυτού με το μεγαλύτερο. Για το λόγο αυτό σε κάθε κόμβο με την τιμή x αποθηκεύουμε την
ακέραια ποσότητα rank που είναι μια προσεγγιστική εκτίμηση του ύψους του δένδρου με ρίζα
τον κόμβο αυτό και αποτελεί ένα άνω φράγμα στο ύψος του δένδρου. Αρχικά, όταν με τη
Makeset() δημιουργείται ένα μονοσύνολο, η τιμή του rank τίθεται 0. Κάθε Find() δεν αλλάζει
τις ποσότητες rank. Όταν τώρα εκτελείται μία πράξη Union() σε δύο δένδρα που οι ρίζες τους
έχουν το ίδιο rank, τότε επιλέγουμε αυθαίρετα μία ρίζα, έστω v, και την κάνουμε παιδί της
άλλης, έστω w, και αυξάνουμε κατά 1 το rank της w (μόνο τότε αυξάνει το rank του δένδρου με
ρίζα την τιμή w).
Στη συνέχεια δίνουμε τους αλγόριθμους για κάθε πράξη της UNION-FIND δομής στην
οποία εφαρμόζουμε τον κανόνα union by rank. Λόγω της αρχικής υπόθεσης ότι τα στοιχεία
μας είναι οι φυσικοί αριθμοί 1, 2, …, n, θα χρειαστούμε δύο πίνακες parent[1..n] και rank[1..n]
όπου θα αποθηκεύουμε τον πατέρα κάθε κόμβου και την ποσότητα rank αντίστοιχα. (Στην
περίπτωση που τα στοιχεία μας δεν ήταν φυσικοί αλλά μπορούσαν να έχουν οποιοδήποτε
τύπο όπως χαρακτήρες ή συμβολοσειρές, τότε η υλοποίηση απαιτεί τη χρήση εγγραφών και
δεικτών όπως είδαμε στα δένδρα αναζήτησης αλλά η λογική των αλγόριθμων δεν αλλάζει).

Αλγόριθμος Makeset(x)
Δεδομένα // parent [1..n], rank[1..n], x: ακέραιοι //

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 169


Αλγόριθμοι και Δομές Δεδομένων

Αρχή
parent[x] = x
rank[x] = 0
Aποτελέσματα // Μονοσύνολο {x} //
Tέλος Makeset

Αλγόριθμος Union(x, y)
Δεδομένα // parent [1..n], rank[1..n], x, y, v, w: ακέραιοι //
Αρχή
v = Find(x)
w = Find(y)
Αν (rank[v] > rank[w]) Τότε parent[w] = v
Aλλιώς
parent[v] = w
Αν (rank[v] = rank[w]) Τότε rank[w] = rank[w]+1 Τέλος Αν
Τέλος Αν
Aποτελέσματα // Νέο σύνολο που προκύπτει από την ένωση των ξένων
υποσυνόλων που περιέχουν τα x και y αντίστοιχα //
Tέλος Union

Ο αλγόριθμος της Union(x, y) καλεί δύο φορές την πράξη Find() που επιστρέφει τη ρίζα
του δένδρου στο οποίο ανήκει το x και y αντίστοιχα.

Αλγόριθμος Find(x)
Δεδομένα // parent [1..n], x, v: ακέραιοι //
Αρχή
v=x
Όσο (v ≠ parent[v]) /* Δεν έχουμε φτάσει ακόμα στη ρίζα του δένδρου που περιέχει
το x */
Τότε v = parent[v]
Τέλος Όσο
Επέστρεψε parent[v]
Aποτελέσματα // Όνομα του συνόλου που περιέχει το x //
Tέλος Find

H πράξη Find() θα μπορούσε να υλοποιηθεί και αναδρομικά, παρόλα αυτά επιλέξαμε την
επαναληπτική υλοποίηση που είναι και πιο εύληπτη.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 170


Αλγόριθμοι και Δομές Δεδομένων

Χρησιμοποιώντας τον κανόνα union by rank οι πολυπλοκότητες των πράξεων είναι


παρόμοιες με αυτές που είδαμε στην υλοποίηση της δομής με διασυνδεδεμένες λίστες.
Συγκεκριμένα, m Makeset(), Union() και Find() πράξεις από τις οποίες οι n είναι Μakeset()
παίρνουν συνολικό χρόνο Ο(n+mlogn), δηλ. έχουμε κατανεμημένο χρόνο ανά πράξη Ο(logn)
στη χειρότερη περίπτωση. H απόδειξη γι’ αυτό βασίζεται στο γεγονός ότι για κάθε δένδρο με
ρίζα τo στοιχείο x, το πλήθος των κόμβων του δένδρου είναι τουλάχιστον 2rank(x) (η σχέση αυτή
μπορεί να αποδειχθεί επαγωγικά) κι επειδή το πλήθος των στοιχείων οποιουδήποτε δένδρου
είναι ≤ n κάθε κόμβος έχει rank ≤ logn.
Σημειώνουμε εδώ ότι αντί του union by rank κανόνα μπορούμε να εφαρμόσουμε απευθείας
τον weighted union o oποίος απαιτεί να αποθηκεύουμε με κάθε στοιχείο x το μέγεθος του
υπόδενδρου με ρίζα το x. Kαι αυτός ο κανόνας μειώνει το μήκος του μακρύτερου μονοπατιού
οποιουδήποτε δένδρου σε Ο(logn) και επομένως δίνει την ίδια ασυμπτωτική πολυπλοκότητα.

Ο ευριστικός κανόνας του path compression είναι επίσης απλός και αρκετά αποδοτικός:
κάθε φορά που εκτελούμε μια πράξη Find(x) ενημερώνουμε τους δείκτες των κόμβων που
επισκεπτόμαστε να δείχνουν στη ρίζα του δένδρου όπου ανήκει το x. Για να γίνει αυτό
κάνουμε δύο περάσματα. Στο πρώτο ανεβαίνουμε το μονοπάτι από τον κόμβο με το στοιχείο x
προς τη ρίζα και στο δεύτερο κατεβαίνουμε το ίδιο μονοπάτι αλλάζοντας κατάλληλα τους
δείκτες των κόμβων. Η εφαρμογή του path compression απεικονίζεται παρακάτω:

Σχήμα 71: Εφαρμογή του κανόνα path compression στην εκτέλεση μιας Find()

Χρησιμοποιώντας μόνο τον κανόνα path compression, n Makeset(), n-1 το πολύ Union()
και f Find() πράξεις έχει αποδειχθεί ότι παίρνουν συνολικό χρόνο Ο(n + flogn) αν f < n και
Ο(flog(1+f/n)n) αν f ≥ n.
Αντί του πλήρους path compression μπορούν να χρησιμοποιηθούν και άλλα υβριδικά
σχήματα που μειώνουν το πλήθος των ενημερώσεων των δεικτών χωρίς να αλλάζουν τις
παραπάνω πολυπλοκότητες. Ένα τέτοιο σχήμα είναι το path splitting όπου κατά τη διάρκεια
της Find() βάζουμε κάθε κόμβο του μονοπατιού να δείχνει στον παππού του οπότε
προκύπτουν δύο μονοπάτια με μήκος περίπου το μισό του αρχικού.

Αν στη δομή UNION-FIND εφαρμόσουμε και τους δύο ευριστικούς κανόνες union by rank
και path compression, oι αλγόριθμοι των πράξεων Makeset() και Union() είναι ίδιοι με αυτούς
που είδαμε παραπάνω, αλλάζει όμως ο αλγόριθμος της Find():

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 171


Αλγόριθμοι και Δομές Δεδομένων

Αλγόριθμος Find(x)
Δεδομένα // parent [1..n], x: ακέραιοι //
Αρχή
Αν (x ≠ parent[x]) /* Δεν έχουμε φτάσει ακόμα στη ρίζα του δένδρου που περιέχει το
x */
Τότε parent[x] = Find(parent[x])
Τέλος Αν
Επέστρεψε parent[x]
Aποτελέσματα // Όνομα του συνόλου που περιέχει το x //
Tέλος Find

Η αναδρομή στον νέο αλγόριθμο της Find() βοηθά στην υλοποίηση των δύο περασμάτων
που απαιτούνται λόγω του κανόνα path compression. Στο πρώτο πέρασμα ανεβαίνουμε το
μονοπάτι από τον κόμβο με το x στη ρίζα του δένδρου και επιστρέφουμε την τιμή της ρίζας ως
αποτέλεσμα της Find(x) ενώ με κάθε επιστροφή από μια αναδρομική κλήση ενημερώνεται ο
αντίστοιχος κόμβος του μονοπατιού ώστε να δείχνει στη ρίζα. Επειδή η επιστροφή από κάθε
αναδρομική κλήση γίνεται με σειρά LIFO, οι κόμβοι ενημερώνονται με σειρά από πάνω προς
τα κάτω κατά μήκος του μονοπατιού. Σε μια επαναληπτική υλοποίηση θα έπρεπε να ανεβούμε
το μονοπάτι από τον κόμβο με το x δύο φορές: μία για να βρούμε τη ρίζα του δένδρου που
περιέχει το x και μία δεύτερη για να ενημερώσουμε την τιμή parent κάθε κόμβου του
μονοπατιού.

Η από κοινού εφαρμογή του union by rank και του path compression κανόνα, δίνει
συνολικό χρόνο σχεδόν γραμμικό στο πλήθος των πράξεων. Συγκεκριμένα, για m συνολικά
πράξεις ο χρόνος χειρότερης περίπτωσης είναι Ο(mα(m, n)), όπου α(m, n) είναι η αντίστροφη
συνάρτηση του Αckermann η οποία αυξάνει με πολύ αργό ρυθμό: ισχύει α(m, n) ≤ 3 για n <
216 = 65.536, δηλ. σε όλες τις πρακτικές εφαρμογές η α(m, n) είναι μια σταθερά όχι μεγαλύτερη
του 3. Έχει αποδειχθεί ότι ο χρόνος αυτός είναι ασυμπτωτικά βέλτιστος.

Ερωτήσεις Κεφαλαίου
1. Δώστε τους ορισμούς των παρακάτω εννοιών:
• Ρίζα και φύλλα ενός δένδρου.
• Πρόγονοι ενός κόμβου.
• Υπόδενδρο ενός κόμβου.
• Απόγονοι ενός κόμβου.
• Πατέρας ενός κόμβου.
• Παιδιά ενός κόμβου.
• Βαθμός ενός κόμβου.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 172


Αλγόριθμοι και Δομές Δεδομένων

• Βάθος ενός κόμβου.


• Ύψος ενός δένδρου.
• Επίπεδο ενός κόμβου.
2. Ποια δένδρα ονομάζονται φυλλοπροσανατολισμένα και ποια κομβοπροσανατολισμένα;
3. Πότε ένα δένδρο καλείται κ-δικό.
4. Δώστε κάποιες εφαρμογές των δυαδικών δένδρων.
5. Τι είναι τα νηματοειδή δένδρα.
6. Τι είναι το δυαδικό δένδρο αναζήτησης; Περιγράψτε τις βασικές πράξεις αναζήτησης,
εισαγωγής και διαγραφής σ’ αυτό.
7. Δώστε την χρονική πολυπλοκότητα μέσης περίπτωσης για ένα δυαδικό δένδρο
αναζήτησης που κατασκευάστηκε μέσω μιας τυχαίας ακολουθίας n πράξεων εισαγωγής.
8. Περιγράψτε αναλυτικά τις τρεις μεθόδους διαπέρασης δένδρου. Δώστε παραδείγματα.
Ποια η εφαρμογή τους στην μετατροπή παραστάσεων πολωνικού συμβολισμού;
9. Δώστε τους ορισμούς των AVL, κόκκινων-μαύρων, ΒΒ[α] και (a, b) δένδρων.
10. Δώστε το ελάχιστο και μέγιστο ύψος των AVL και κόκκινων-μαύρων δένδρων.
11. Τι επιτυγχάνει η πράξη της περιστροφής σε ένα ισοζυγισμένο δυαδικό δένδρο
αναζήτησης;
12. Υπολογίστε το μέγιστο πλήθος φύλλων σε ένα (a, b) δένδρο.
13. Ποιες επαναζυγιστικές πράξεις χρησιμοποιούμε για να επιτύχουμε ζύγιση από μια
δυναμική πράξη insert ή delete σε ένα (a, b) δένδρο;
14. Περιγράψτε το διδιάστατο δένδρο περιοχής και δώστε τις χρονικές πολυπλοκότητες των
βασικών πράξεων. Πώς γενικεύεται σε μεγαλύτερες διαστάσεις;
15. Περιγράψτε τη δομή trie και δώστε τις πολυπλοκότητες χρόνου και χώρου χειρότερης
περίπτωσης. Πώς μπορούμε να βελτιώσουμε τις πολυπλοκότητες αυτές;
16. Δώστε τον ορισμό ενός τετραδικού δένδρου και εξηγείστε πώς γενικεύεται στις d
διαστάσεις.
17. Τι είναι ένας γράφος; Πως ορίζεται;
18. Δώστε τους ορισμούς για τα παρακάτω:
• Τετριμμένος γράφος
• Άκρα ακμής
• Γειτονικές κορυφές
• Όμορες ακμές
• Βαθμός μιας κορυφής
• Υπογράφος
• Ισομορφικοί γράφοι
• Πλήρης γράφος
• Διμερής γράφος

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 173


Αλγόριθμοι και Δομές Δεδομένων

• Πλήρης διμερής γράφος


• Συμπληρωματικοί γράφοι
19. Να σχεδιαστεί ένας (ή περισσότεροι αν υπάρχουν) γράφος ο οποίος να έχει 5 κορυφές
και βαθμούς σε κάθε κορυφή 2, 2, 2, 3 και 3.
20. Δώστε το πλήθος των ακμών για τους γράφους Kn και Kn, m.
21. Να σχεδιαστούν όλοι οι γράφοι με 5 κορυφές και 3 ή 7 ακμές. Χρησιμοποιείστε την
παρατήρηση ότι δύο γράφοι είναι ισόμορφοι αν και μόνο αν οι συμπληρωματικοί τους
είναι ισόμορφοι.
22. Περιγράψτε τις βασικές δομές δεδομένων που χρησιμοποιούνται για την αναπαράσταση
ενός γράφου στον υπολογιστή. Ποια δομή είναι καλύτερη και γιατί;
23. Ορίστε τον γράφο Euler και τον γράφο Hamilton.
24. Τι είναι ένας συνεκτικός γράφος; Τι είναι η συνεκτική συνιστώσα; Ορίστε το δισυνεκτικό
γράφο.
25. Ορίστε τη συνεκτικότητα ως προς τις κορυφές και τις ακμές ενός γράφου. Ποια η σχέση
τους με τον ελάχιστο βαθμό των κορυφών ενός γράφου;
26. Ορίστε τον κατευθυνόμενο γράφο και το βαθμό μιας κορυφής.
27. Ορίστε τις ισχυρά και ασθενώς συνεκτικές συνιστώσες ενός κατευθυνόμενου γράφου.
28. Δώστε τις χρονικές πολυπλοκότητας των αλγορίθμων αναζήτησης κορυφών γράφου BFS
και DFS.
29. Περιγράψτε κάποιες από τις εφαρμογές του UNION-FIND προβλήματος.
30. Δώστε τις πολυπλοκότητας κάθε πράξης Makeset(), Union(), Find() στην υλοποίηση της
UNION-FIND δομής με λίστες. Εξηγείστε πώς αλλάζουν οι πολυπλοκότητες αυτές αν
εφαρμόσουμε τον κανόνα weighted union.
31. Περιγράψτε τους κανόνες union by rank και path compression στην υλοποίηση της
UNION-FIND δομής με δένδρα. Ποιες οι πολυπλοκότητες των πράξεων αν εφαρμόσουμε
μόνο τον πρώτο; Αν εφαρμόσουμε και τους δύο;

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 174


Αλγόριθμοι και Δομές Δεδομένων

ΒΙΒΛΙΟΓΡΑΦΙΑ

[1] Χ. Κοίλιας, “Δομές Δεδομένων και Οργανώσεις Αρχείων”, Εκδόσεις Νέων Τεχνολογιών,
1998.
[2] Λ. Κυρούσης, Χ. Μπούρας, Π. Σπυράκης, Γ. Σταματίου, “Εισαγωγή στους Γράφους:
Θεωρία, Προβλήματα και Λύσεις”, Eκδόσεις Gutenberg, 1999.
[3] Θ. Παπαθεοδώρου, “Αλγόριθμοι: Εισαγωγικά Θέματα και Παραδείγματα”, Εκδόσεις
Πανεπιστημίου Πατρών, 1999.
[4] Π.Δ. Μποζάνης, “Αλγόριθμοι, Σχεδιασμός και Ανάλυση”, Εκδόσεις Τζιόλα, 2003.
[5] Π.Δ. Μποζάνης, “Δομές Δεδομένων - Ταξινόμηση και Αναζήτηση με Java”, Εκδόσεις
Τζιόλα, 2003.
[6] Α. Τσακαλίδης, “Δομές Δεδομένων”, Εκδόσεις Πανεπιστημίου Πατρών, 2004.
[7] Δ. Φωτάκης, “Tαξινόμηση – Αναζήτηση – Επιλογή”, Σημειώσεις μαθήματος,
Πανεπιστήμιο Αιγαίου, Τμήμα Μηχανικών Πληροφοριακών και Επικοινωνιακών
Συστημάτων, Νοέμβριος 2006.
[8] T. Cormen, C. Leiserson, R. Rivest, C. Stein, “Introduction to Algorithms (2nd Edition)”,
MIT Press, 2001.
[9] S. Sahni, “Δομές Δεδομένων, Αλγόριθμοι και Εφαρμογές στην C++”, Εκδόσεις Τζιόλα,
2004.
[10] R. Sedgewick, “Αλγόριθμοι σε C” (3η αμερικάνικη έκδοση), Εκδόσεις Κλειδάριθμος,
2005.
[11] N. Wirth, “Αλγόριθμοι και Δομές Δεδομένων”, Εκδόσεις Κλειδάριθμος, 1990.

On-line πηγές στο διαδίκτυο


[1] http://www.softlab.ntua.gr/~fotakis/data_structures.html, Δ. Φωτάκης, Διαλέξεις και
σημειώσεις μαθήματος “Δομές Δεδομένων”, Πανεπιστήμιο Αιγαίου, Τμήμα Μηχανικών
Πληροφοριακών και Επικοινωνιακών Συστημάτων, Φθινόπωρο 2007.
[2] http://www.syros.aegean.gr/users/gaviotis/ADD/Default.htm, Γ. Γαβιώτης, Υλικό
μαθήματος “Αλγόριθμοι – Δομές Δεδομένων”, Πανεπιστήμιο Αιγαίου, Τμήμα
Μηχανικών Σχεδίασης Προϊόντων και Συστημάτων, 2008-09.
[3] http://www.corelab.ntua.gr/courses/algorithms/, E. Zάχος, Διαλέξεις μαθήματος
“Αλγόριθμοι και Πολυπλοκότητα”, Εθνικό Μετσόβιο Πολυτεχνείο, Σχολή Ηλεκτρολόγων
Μηχανικών, 2008-09.
[4] http://www.cs.sunysb.edu/~skiena/214/lectures/, S. Skiena, “Lecture Notes for
Computers, Algorithms - Data Structures”, Stony Brook University, Dept. of Computer
Science.
[5] http://www.cs.sunysb.edu/~algorith/, S. Skiena, “The Stony Brook Algorithm Repository”,
Stony Brook University, Dept. of Computer Science, 1997.
[6] http://www.cs.auckland.ac.nz/~jmor159/PLDS210/, Course σε αλγόριθμους & δομές
δεδομένων και animations από το Αuckland University.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 175


Αλγόριθμοι και Δομές Δεδομένων

[7] http://ocw.mit.edu/OcwWeb/Electrical-Engineering-and-Computer-Science/6-046JFall-
2005/Video Lectures/, Οpen courses σε αλγόριθμους από το Massachusetts Institute of
Technology.
[8] http://www.csse.monash.edu.au/~dwa/MELB/, Animation αλγόριθμων από το Melburne
University.
[9] http://www.cs.pitt.edu/~kirk/algorithmcourses/, Κατάλογος με ιστοσελίδες που
ασχολούνται με αλγόριθμους.

ΤΕΣΥΔ / Δ. Σοφοτάσιος - Γ. Τσακνάκης 176

You might also like