TEΧΝΟΛΟΓΙΚΟ ΕΚΠΑΙ ΕΥΤΙΚΟ Ι ΡΥΜΑ ΣΕΡΡΩΝ ΣΧΟΛΗ ΤΕΧΝΟΛΟΓΙΚΩΝ ΕΦΑΡΜΟΓΩΝ ΤΜΗΜΑ ΠΛΗΡΟΦΟΡΙΚΗΣ & ΕΠΙΚΟΙΝΩΝΙΩΝ ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ (Σηµειώσεις Εργαστηρίου) Ευάγγελος Γ. Ούτσιος Σέρρες 2004
2
ΠΕΡΙΕΧΟΜΕΝΑ Κεφάλαιο 1 ΠΙΝΑΚΕΣ 1.1 Εισαγωγή 6 1.2 Βασικές έννοιες πινάκων 7 Κεφάλαιο 2 ΑΝΑ ΡΟΜΗ 2.1 Εισαγωγή 14 2.2 Υπολογισµός παραγοντικού 14 2.3 Υπολογισµός δύναµης 16 2.4 Υπολογισµός όρων ακολουθίας αριθµών Fibonacci 16 2.5 Μέγιστος Κοινός ιαιρέτης (Αλγόριθµος Ευκλείδη) 17 2.6 Πύργοι του Hanoi 19 Κεφάλαιο 3 ΑΝΑΖΗΤΗΣΗ 3.1 Εισαγωγή 22 3.2 Σειριακή Αναζήτηση 22 3.3 υαδική Αναζήτηση 23 Κεφάλαιο 4 ΤΑΞΙΝΟΜΗΣΗ 4.1 Εισαγωγή 26 4.2 Ταξινόµηση µε απευθείας επιλογή 26 4.3 Ταξινόµηση µε απευθείας εισαγωγή 27 4.4 Ταξινόµηση φυσαλίδας 28 4.5 Γρήγορη ταξινόµηση 28 Κεφάλαιο 5 ΓΡΑΜΜΙΚΕΣ ΛΙΣΤΕΣ 5.1 Εισαγωγή 32 5.2 Σειριακές Λίστες 5.2.1 Στοίβα 32 5.2.2 Ουρά 41 5.3 Συνδεδεµένες Λίστες 5.3.1 Απλή συνδεδεµένη λίστα 49 5.3.2 Στοίβα ως συνδεδεµένη λίστα 52 5.3.3 Ουρά ως συνδεδεµένη λίστα 54 Κεφάλαιο 6 ΕΝ ΡΑ 6.1 Εισαγωγή 56 6.2 υαδικά δένδρα 58 3
Κεφάλαιο 7 ΓΡΑΦΟΙ 7.1 Εισαγωγή 66 7.2 Μέθοδοι Αναπαράστασης γράφων 68 7.3 Μέθοδοι ιάσχισης γράφων 7.3.1 Αναζήτηση µε προτεραιότητα Βάθους 70 7.3.2 Αναζήτηση µε προτεραιότητα Πλάτους 72 Κεφάλαιο 8 ΠΙΝΑΚΕΣ ΚΑΤΑΚΕΡΜΑΤΙΣΜΟΥ 8.1 Εισαγωγή 78 8.2 Συγκρούσεις 79 8.3 Ανοιχτή αναφορά διεύθυνσης 80 8.4 Ξεχωριστή σύνδεση 83 ΠΑΡΑΡΤΗΜΑ A ΕΡΓΑΣΤΗΡΙΑΚΕΣ ΑΣΚΗΣΕΙΣ 87 ΒΙΒΛΙΟΓΡΑΦΙΑ.101 4
Κεφάλαιο 1 ΠΙΝΑΚΕΣ 1.1 Εισαγωγή 1.2 Βασικές έννοιες πινάκων 5
1.1 ΕΙΣΑΓΩΓΗ Εκτός από τους αλγορίθµους, σηµαντική έννοια για την Πληροφορική είναι και η έννοια των δεδοµένων. Τα δεδοµένα αποθηκεύονται στον υπολογιστή µε τη βοήθεια των λεγόµενων δοµών δεδοµένων. Θεωρώντας τους αλγόριθµους και τις δοµές δεδοµένων µια αδιάσπαστη ενότητα µπορεί να λεχθεί, ότι η ενότητα αυτή τελικά αποτελεί τη βάση ενός προγράµµατος που επιλύει ένα πρόβληµα. Αλγόριθµοι + οµές εδοµένων = Προγράµµατα Τα δεδοµένα ενός προβλήµατος αποθηκεύονται στον Η/Υ, είτε στην κύρια µνήµη του ή στη δευτερεύουσα µνήµη του. Η αποθήκευση αυτή δε γίνεται κατά ένα τυχαίο τρόπο αλλά συστηµατικά, δηλαδή χρησιµοποιώντας µία δοµή. Η έννοια της δοµής δεδοµένων (data structure) είναι σηµαντική για την Πληροφορική και ορίζεται µε τον ακόλουθο τυπικό ορισµό. Ορισµός οµή εδοµένων είναι ένα σύνολο αποθηκευµένων δεδοµένων που υφίστανται επεξεργασία από ένα σύνολο λειτουργιών. Κάθε µορφή δοµής δεδοµένων αποτελείται από ένα σύνολο κόµβων (nodes). Οι βασικές λειτουργίες επί των δοµών δεδοµένων είναι οι ακόλουθες: Προσπέλαση (access) πρόσβαση σε ένα κόµβο µε σκοπό να εξετασθεί ή να τροποποιηθεί το περιεχόµενό του Εισαγωγή (insertion) η προσθήκη νέων κόµβων σε µία υπάρχουσα δοµή ιαγραφή (deletion) η αφαίρεση ενός κόµβου από µία υπάρχουσα δοµή Αναζήτηση (searching) γίνεται προσπέλαση των κόµβων µίας δοµής, προκειµένου να εντοπισθούν ένας ή περισσότεροι που έχουν µία δεδοµένη ιδιότητα Ταξινόµηση (sorting) οι κόµβοι µίας δοµής τοποθετούνται σε αύξουσα ή φθίνουσα σειρά Αντιγραφή (copying) 6
όλοι οι κόµβοι ή µερικοί από τους κόµβους µίας δοµής αντιγράφονται σε µία άλλη δοµή Συγχώνευση (merging) δύο ή περισσότερες δοµές συνενώνονται σε µία ενιαία δοµή ιαχωρισµός (separation) αποτελεί την αντίστροφη πράξη της συγχώνευσης Στην πράξη σπάνια χρησιµοποιούνται όλες οι λειτουργίες για κάποια δοµή. Παρατηρείται συχνά το φαινόµενο µία δοµή δεδοµένων να είναι αποδοτικότερη από µία άλλη δοµή µε κριτήριο κάποια λειτουργία, για παράδειγµα την αναζήτηση, αλλά λιγότερο αποδοτική για κάποια άλλη λειτουργία, για παράδειγµα την εισαγωγή. Αυτές οι παρατηρήσεις εξηγούν αφ ενός την ύπαρξη διαφορετικών δοµών, και αφ ετέρου τη σπουδαιότητα της επιλογής της κατάλληλης δοµής κάθε φορά. 1.2 ΒΑΣΙΚΕΣ ΕΝΝΟΙΕΣ ΠΙΝΑΚΩΝ Με τον όρο στατική δοµή δεδοµένων εννοείται ότι το ακριβές µέγεθος της απαιτούµενης κύριας µνήµης καθορίζεται κατά τη στιγµή του προγραµµατισµού τους, και κατά συνέπεια κατά τη στιγµή της µετάφρασης του προγράµµατος και όχι κατά τη στιγµή της εκτέλεσής του. Μία άλλη σηµαντική διαφορά σε σχέση µε τις δυναµικές δοµές που θα περιγραφούν παρακάτω είναι ότι τα στοιχεία των στατικών δοµών αποθηκεύονται σε συνεχόµενες θέσεις µνήµης. Στην πράξη, οι στατικές δοµές υλοποιούνται µε πίνακες που µας είναι γνωστοί από άλλα µαθήµατα και υποστηρίζονται από κάθε γλώσσα προγραµµατισµού. Μπορούµε να ορίσουµε τον πίνακα ως µια δοµή που περιέχει στοιχεία του ίδιου τύπου (δηλαδή ακέραιους, πραγµατικούς κλπ.). Η δήλωση των στοιχείων ενός πίνακα και η µέθοδος αναφοράς τους εξαρτάται από τη συγκεκριµένη γλώσσα υψηλού επιπέδου που χρησιµοποιείται. Όµως γενικά η αναφορά στα στοιχεία ενός πίνακα γίνεται µε τη χρήση του συµβολικού ονόµατος του πίνακα ακολουθούµενου από την τιµή ενός ή περισσοτέρων δεικτών (indexes) σε παρένθεση ή αγκύλη. Ένας πίνακας µπορεί να είναι µονοδιάστατος, αλλά γενικά µπορεί να είναι δισδιάστατος, τρισδιάστατος και γενικά ν-διάστατος πίνακας. 7
Παράδειγµα 1 Εύρεση του ελάχιστου στοιχείου ενός µονοδιάστατου πίνακα 1..n Αλγόριθµος Ελάχιστο_πίνακα εδοµένα table[n], i, min min = table[1] Για i από 2 µέχρι n Aν table[i] < min τότε min = table[i] Τέλος επανάληψης Εµφάνισε min Τέλος Ελάχιστο_Πίνακα ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ Υλοποίηση στην C: #define N 10 main() int table[n] = 11, 23, 2, 34, 56, 65, 7, 9, 1, 25; int i, min; min = table[0]; for (i=1; i<n; i++) if (table[i]<min) min = table[i]; printf( Min = %d,min); Παράδειγµα 2 Εύρεση αθροίσµατος στοιχείων δισδιάστατου πίνακα 1..m, 1..n Αλγόριθµος Άθροισµα_στοιχείων_Πίνακα εδοµένα table[m][n], i, j, sum, row[m], col[n] Για i από 1 µέχρι m Για j από 1 µέχρι n ιάβασε table[i][j] Τέλος επανάληψης Τέλος επανάληψης sum = 0; Για i από 1 µέχρι m row[i] = 0 Τέλος επανάληψης Για j από 1 µέχρι n col[j] = 0 Τέλος επανάληψης Για i από 1 µέχρι m Για j από 1 µέχρι n sum = sum + table[i][j] row[i] = row[i] + table[i][j] 8
colo[j] = col[j] + table[i][j] Τέλος επανάληψης Τέλος επανάληψης Εµφάνισε sum Για i από 1 µέχρι m Εµφάνισε row[i] Tέλος επανάληψης Για j από 1 µέχρι n Εµφάνισε col[i] Tέλος επανάληψης Τέλος Άθροισµα_στοιχείων_Πίνακα ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ Υλοποίηση στην C: #define M 5 #define N 5 main() int table[m][n], row[m], col[n]; int i, j, sum; for (i=0; i<m; i++) for (j=0; j<n; j++) scanf( %d,&table[i][j]); sum = 0; for (i=0; i<m; i++) row[i] = 0; for (j=0; j<n; j++) col[j] = 0; for (i=0; i<m; i++) for (j=0; j<n; j++) sum = sum + table[i][j]; row[i] = row[i] + table[i][j]; col[j] = col[j] + table[i][j]; printf( sum = %d, sum); for (i=0; i<m; i++) printf( row[%d] = %d\n, i, row[i]); for (j=0; j<n; j++) printf( col[%j] = %d\n, j, col[j]); 9
table row 4 16 5 21 7 53 28 9 38 13 51 139 17 67 22 40 30 176 20 40 10 3 13 86 21 34 48 29 26 158 col 90 166 123 106 127 612 sum Παράδειγµα 3 Απόδοση τιµών σε πίνακα µε τη συνάρτηση random #include <stdlib.h> #include <time.h> #define N 30000 int getuniquenumber(int p[n], int i); main() int i, p[n]; long t0, t1, dt; randomize(); printf("getting %i random numbers...",n); time(&t0); for (i=0; i<n; i++) p[i] = getuniquenumber(p,i); time(&t1); printf("complete.\n"); dt = t1 - t0; printf("calc time = %i\n",dt); getch(); return 0; int getuniquenumber(int p[n], int i) int x,j, found; do x = random(32768); found = 0; j = 0; 10
while (j<=i && found = = 0) if (p[j] = = x) found = 1; j++; while (found = = 1); ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ return x; Σηµ.: Το προηγούµενο παράδειγµα χρησιµοποιεί την συνάρτηση random για να παράγει ένα πλήθος από τυχαίους αριθµούς στο διάστηµα 0 32767 και τους αποδίδει ύστερα σε ένα πίνακα αναλόγου προκαθορισµένου µεγέθους. Η συνάρτηση randomize χρησιµοποιείται για να παράγεται µία νέα σειρά τυχαίων αριθµών κάθε φορά. Η συνάρτηση getuniquenumber ελέγχει, µε µία επαναλαµβανόµενη διαδικασία, αν κάποιος τυχαίος αριθµός έχει παραχθεί από πριν, έτσι ώστε οι τελικοί αριθµοί που θα καταχωρηθούν στον πίνακα να είναι µοναδικοί. Τέλος, η συνάρτηση time χρησιµοποιείται για να καταµετρήσει το συνολικό χρόνο (σε δευτερόλεπτα) που διαρκεί η παραγωγή των ζητούµενων τυχαίων αριθµών. Το πρόγραµµα αυτό µπορεί να αποτελέσει τη βάση για την ανάπτυξη άλλων προγραµµάτων όπου είναι απαραίτητο να αποδοθεί ένα µεγάλο πλήθος αριθµών σε κάποιο αντίστοιχο πίνακα ή να καταµετρηθεί ο συνολικός χρόνος εκτέλεσης ενός συγκεκριµένου αλγορίθµου ή ακόµα να συγκριθούν οι χρόνοι εκτέλεσης διαφόρων αλγορίθµων αναζήτησης, ταξινόµησης, κλπ. 11
12
Κεφάλαιο 2 ΑΝΑ ΡΟΜΗ 2.1 Εισαγωγή 2.2 Υπολογισµός παραγοντικού 2.3 Υπολογισµός δύναµης 2.4 Υπολογισµός όρων ακολουθίας αριθµών Fibonacci 2.5 Μέγιστος Κοινός ιαιρέτης (Αλγόριθµος Ευκλείδη) 2.6 Πύργοι του Hanoi 13
2.1 ΕΙΣΑΓΩΓΗ Αναδροµή είναι η µέθοδος κατά την οποία, σε µία γλώσσα προγραµµατισµού, µία διαδικασία ή συνάρτηση έχει την δυνατότητα να καλεί τον εαυτό της. Η υλοποίηση της αναδροµής βασίζεται στη έννοια της στοίβας. Σε κάθε κλήση µίας υπορουτίνας πρέπει να φυλάγονται οι διευθύνσεις επιστροφής. Όταν µία υπορουτίνα καλεί τον εαυτό της θα πρέπει επίσης να φυλάγονται οι προηγούµενες τιµές των µεταβλητών και να χρησιµοποιούνται όταν τελειώσει η αναδροµική κλήση. Η χρήση της αναδροµής διευκολύνει πολύ τον προγραµµατιστή στην ανάπτυξη και τον έλεγχο ενός προγράµµατος. Θα πρέπει όµως να χρησιµοποιείται µε µέτρο, γιατί η εκτέλεση ενός αναδροµικού προγράµµατος έχει χρονικό κόστος. Γενικά, ανάµεσα σε ένα επαναληπτικό και ένα αναδροµικό πρόγραµµα θα πρέπει να προτιµάµε το πρώτο, εκτός και αν η ανάπτυξή του µας δυσκολεύει ιδιαίτερα. Η αναδροµή ενδείκνυται σε προβλήµατα κάποιας σχετικής πολυπλοκότητας, που εξ ορισµού τα εκφράζουµε αναδροµικά. 2.2 ΥΠΟΛΟΓΙΣΜΟΣ ΠΑΡΑΓΟΝΤΙΚΟΥ Επαναληπτικός ορισµός: n! = 1*2*3*..(n-1)*n Συνάρτηση µε επανάληψη: int factorial(int n) int i,f; f = 1; for (i=2; i<=n; i++) f = f*i; return f; Αναδροµικός ορισµός: n! = 1 αν n = 0, = n*(n-1)! αν n > 0 14
Συνάρτηση µε αναδροµή: ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ int factorial(int n) int f; if (n = = 0) f = 1; f = n*factorial(n-1); return f; Ας παρακολουθήσουµε τις τιµές των µεταβλητών κατά την κλήση των δύο συναρτήσεων, π.χ. για n = 4: Επαναληπτική µέθοδος: f = 1 i = 2 f = 1*2 = 2 i = 3 f = 2*3 = 6 i = 4 f = 6*4 = 24 Αναδροµική µέθοδος: factorial(4) = 4*factorial(3) factorial(3) = 3*factorial(2) factorial(2) = 2*factorial(1) factorial(1) = 1*factorial(0) factorial(0) = 1 Ακολούθως, η τελευταία τιµή 1 µεταβιβάζεται στην προηγούµενη κλήση και έτσι υπολογίζεται το factorial(1) = 1. Κατά τον ίδιο τρόπο έχουµε factorial(2) = 2*1 = 2 factorial(3) = 3*2 = 6 factorial(4) = 4*6 = 24 15
2.3 ΥΠΟΛΟΓΙΣΜΟΣ ΥΝΑΜΗΣ Επαναληπτικός ορισµός: x n = x*x*x*..*x, n φορές ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ Συνάρτηση µε επανάληψη: int power(int x, int n) int i,p; p = 1; for (i=1; i<=n; i++) p = p*x; return p; Αναδροµικός ορισµός: x n = 1 αν n = 0, = x* x n-1 αν n > 0 Συνάρτηση µε αναδροµή: int power(int x, int n) int p; if (n = = 0) p = 1; p = x*power(x,n-1); return p; 2.4 ΥΠΟΛΟΓΙΣΜΟΣ ΟΡΩΝ ΑΚΟΛΟΥΘΙΑΣ ΑΡΙΘΜΩΝ FIBONACCI Αναδροµικός ορισµός: f i = 0 αν i = 0 1 αν i = 1 f i-1 +f i-2 αν i>1 int fibonacci(int n) int fib ; if (n <= 1) fib = n ; fib = fibonacci(n-1)+fibonacci(n-2); 16
2.5 ΜΕΓΙΣΤΟΣ ΚΟΙΝΟΣ ΙΑΙΡΕΤΗΣ (Αλγόριθµος Ευκλείδη) Αν οι x και y είναι δύο ακέραιοι, όχι και οι δύο µηδέν, ορίζουµε ότι ο µέγιστος κοινός διαιρέτης (greatest common divisor) είναι ο µέγιστος ακέραιος που διαιρεί ακριβώς και τον x και τον y. Για παράδειγµα, gcd(1000, 600) = 200 gcd(-70, 90) = 10 gcd(0, 12) = 12 Σύµφωνα µε τον Ευκλείδη ισχύει όµως gcd(x, y) = gcd(y, x%y) Αν τον κανόνα αυτό τον εφαρµόσουµε επαναλαµβανόµενα µέχρι το δεύτερο όρισµα να γίνει 0, τότε καταλήγουµε σε έναν ιδιαίτερα αποτεσµατικό αλγόριθµο υπολογισµού του µέγιστου κοινού διαιρέτη. Π.χ. gcd(1900, 700) = gcd(700, 500) Επαναληπτικός αλγόριθµος int gcd(int x, int y) int r; r = y; while (r!= 0) r = x % y; x = y ; y = r ; return x ; Αναδροµικός αλγόριθµος int gcd(int x, int y) int r; if (y = = 0) r = x; r = gcd(y, x % y); return r; = gcd(500, 200) = gcd(200, 100) = gcd(100, 0) = 100 17
2.6 ΠΥΡΓΟΙ ΤΟΥ HANOI Η αναδροµή µπορεί να αποτελέσει ένα πολύ ισχυρό εργαλείο για τον προγραµµατιστή. Μερικές φορές, κάποιο πρόβληµα που φαίνεται δύσκολο όταν το αντιµετωπίζουµε µε άλλες προγραµµατιστικές τεχνικές, αποδεικνύεται ιδιαίτερα εύκολο όταν σκεφτόµαστε αναδροµικά. Ένα τέτοιο χαρακτηριστικό παράδειγµα είναι ένα παιδικό παιχνίδι που ονοµάζεται οι Πύργοι του Hanoi (Towers of Hanoi). Το παιχνίδι αποτελείται από τρεις στύλους και ένα πλήθος κυκλικών δίσκων που στοιβάζονται ο ένας πάνω στον άλλο. Οι δίσκοι είναι διαφορετικού µεγέθους και στοιβάζονται κατά φθίνουσα σειρά µεγέθους. Ο σκοπός του παιχνιδιού είναι να µετακινηθούν όλοι οι δίσκοι από τον 1 ο στον 2 ο στύλο, µε βάση όµως τους παρακάτω κανόνες: Μόνο ένας δίσκος µπορεί να µετακινηθεί κάθε φορά Ένας δίσκος δεν µπορεί να τοποθετηθεί πάνω σε έναν άλλο µικρότερου µεγέθους Ο 3 ος στύλος µπορεί να χρησιµοποιηθεί σαν βοηθητικός 1 2 3 start goal spare Το παιχνίδι αυτό έχει µία εντυπωσιακή και µακρά ιστορία. Ο θρύλος λέει ότι ανακαλύφθηκε από το Θεό κατά το χάραµα του χρόνου και δόθηκε στον άνθρωπο σαν ένα από τα µεγάλα καθήκοντα της ανθρωπότητας. Τελικά η λύση του ανατέθηκε στους µοναχούς ενός συγκεκριµένου µοναστηριού κάποιου ανατολίτικου χωριού µε το όνοµα Hanoi. Ο θρύλος επίσης λέει ότι, όταν το παιχνίδι ολοκληρωθεί, το τελευταίο καθήκον που ανατέθηκε στην ανθρωπότητα θα έχει ολοκληρωθεί και αυτό θα σηµάνει το τέλος του κόσµου. Η έκδοση που υποτίθεται ότι ο Θεός παρουσίασε στο ανθρώπινο είδος είχε 64 δίσκους. Μπορούµε να θεωρήσουµε ότι ένας πύργος που αποτελείται από Ν δίσκους µπορεί να αποσυντεθεί σε δύο πύργους, ένα πύργο που αποτελείται από ένα µόνο δίσκο (το 18
µεγαλύτερο) και ένα πύργο που αποτελείται από τους υπόλοιπους Ν-1 δίσκους. Με βάση τη θεώρηση αυτή, το πρόβληµα της µετακίνησης Ν δίσκων µπορεί να αναχθεί αναδροµικά στη µετακίνηση Ν-1 δίσκων, ως εξής: 1. Μετακίνησε τους επάνω Ν-1 δίσκους από τον 1 ο στύλο (start) στον 3 ο στύλο (spare), χρησιµοποιώντας σαν βοηθητικό τον 2 ο στύλο (goal) 2. Μετακίνησε τον ένα δίσκο που έµεινε στον 1 ο στύλο, από τον 1 ο στύλο (start) στον 2 ο στύλο (goal) 3. Μετακίνησε τους Ν-1 δίσκους από τον 3 ο στύλο (spare) στον 2 ο στύλο (goal), χρησιµοποιώντας σαν βοηθητικό τον 1 ο στύλο (start) Η µετακίνηση των Ν-1 δίσκων στα βήµατα 1 και 3 µπορεί να αναχθεί στη µετακίνηση Ν-2 δίσκων κλπ. Η κατανόηση του αλγορίθµου γίνεται ευκολότερη αν χρησιµοποιήσουµε 3 δίσκους. Τότε τα βήµατα έχουν ως εξής: 1. Μετακίνησε ένα δίσκο από τον 1 ο στύλο στον 2 ο στύλο. 2. Μετακίνησε ένα δίσκο από τον 1 ο στύλο στον 3 ο στύλο. 3. Μετακίνησε ένα δίσκο από τον 2 ο στύλο στον 3 ο στύλο. 4. Μετακίνησε ένα δίσκο από τον 1 ο στύλο στον 2 ο στύλο. 5. Μετακίνησε ένα δίσκο από τον 3 ο στύλο στον 1 ο στύλο. 6. Μετακίνησε ένα δίσκο από τον 3 ο στύλο στον 2 ο στύλο. 7. Μετακίνησε ένα δίσκο από τον 1 ο στύλο στον 2 ο στύλο. Υλοποίηση στην C: // towers of Hanoi #include <stdio.h> #include <conio.h> void hanoitowers(int rings, int start, int goal, int spare); main() int n; printf("enter number of rings:"); scanf("%d",&n); printf("to move %d rings from peg 1 to peg 2, do the following:\n",n); hanoitowers(n, 1, 2, 3); getch(); return 0; 19
void hanoitowers(int rings, int start, int goal, int spare) if (rings = = 1) printf("move from %d to %d \n", start, goal); hanoitowers(rings-1, start, spare, goal); printf("move from %d to %d \n", start, goal); hanoitowers(rings-1, spare, goal, start); ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ 20
Κεφάλαιο 3 ΑΝΑΖΗΤΗΣΗ 3.1 Εισαγωγή 3.2 Σειριακή Αναζήτηση 3.3 υαδική Αναζήτηση 21
3.1 ΕΙΣΑΓΩΓΗ Αναζήτηση (searching) είναι η διεργασία της εύρεσης κάποιας συγκεκριµένης τιµής ανάµεσα από ένα σύνολο τιµών. Το πρόβληµα της αναζήτησης είναι ένα από τα πιο ενδιαφέροντα προβλήµατα της Επιστήµης των Υπολογιστών λόγω της µεγάλης πρακτικότητάς του. Το πρόβληµα γίνεται ακόµα πιο ενδιαφέρον αν ληφθεί υπόψη η µεγάλη ποικιλία των χαρακτηριστικών της δοµής όπου αποθηκεύονται τα δεδοµένα (π.χ. στατική ή δυναµική, τρόπος οργάνωσης, µέσο αποθήκευσης, κλπ.). Στη συνέχεια θα εξετασθούν δύο από τις πιο γνωστές µέθοδοι αναζήτησης πινάκων: Σειριακή αναζήτηση (Sequential search) υαδική αναζήτηση (Binary search) 3.2 ΣΕΙΡΙΑΚΗ ΑΝΑΖΗΤΗΣΗ Η πιο απλή µέθοδος αναζήτησης ενός στοιχείου σε ένα πίνακα είναι η σειριακή αναζήτηση. Γίνεται προσπέλαση των στοιχείων του πίνακα από την πρώτη θέση µέχρι την τελευταία. Κάθε φορά ελέγχεται το στοιχείο της τρέχουσας θέσης αν είναι το ζητούµενο, οπότε και τελειώνει η αναζήτηση. Αν προσπελασθεί όλος ο πίνακας και δεν βρεθεί το ζητούµενο στοιχείο, τότε η αναζήτηση θεωρείται ανεπιτυχής. Παρακάτω δίνεται ο αλγόριθµος (σε ψευδοκώδικα) για τη σειριακή αναζήτηση, µε δύο διαφορετικούς τρόπους, έναν επαναληπτικό και ένα αναδροµικό. 1) Επαναληπτική έκδοση Αλγόριθµος Σειριακή_αναζήτηση_1 εδοµένα p[n], i, key, found, position found = false position = -1 i = 0 ΟΣΟ (found = = false ) ΚΑΙ (i < N) AN p[i] = = key TOTE found = true position = i ΑΛΛΙΩΣ i = i+1 ΤΕΛΟΣ ΑΝ ΤΕΛΟΣ ΕΠΑΝΑΛΗΨΗΣ AN found = = true ΤΟΤΕ ΕΜΦΑΝΙΣΕ position ΑΛΛΙΩΣ ΕΜΦΑΝΙΣΕ εν υπάρχει ΤΕΛΟΣ Σειριακή_αναζήτηση_1 22
2) Αναδροµική έκδοση Αλγόριθµος Σειριακή_αναζήτηση_2 εδοµένα p[n], i, key, found, position ΑΝ i > N ΤΟΤΕ found = false ΑΛΛΙΩΣ ΑN p[i] = = key TOTE found = true position = i ΑΛΛΙΩΣ i = i+1 Σειριακή_αναζήτηση_2(p,key,found,position) ΤΕΛΟΣ ΑΝ AN found = = true ΤΟΤΕ ΕΜΦΑΝΙΣΕ position ΑΛΛΙΩΣ ΕΜΦΑΝΙΣΕ εν υπάρχει ΤΕΛΟΣ Σειριακή_αναζήτηση_1 3.3 ΥΑ ΙΚΗ ΑΝΑΖΗΤΗΣΗ Όταν τα στοιχεία του πίνακα είναι ταξινοµηµένα, τότε είναι δυνατόν να επιταχυνθεί η διαδικασία αναζήτησης, εφαρµόζοντας επιλεκτικά και όχι σειριακά την αναζήτηση. Η µέθοδος που περιγράφεται παρακάτω είναι η πιο γνωστή σε αυτή την κατηγορία. Πιο συγκεκριµένα, συγκρίνεται η τιµή του ζητούµενου στοιχείου µε το µεσαίο στοιχείο του πίνακα. Αν είναι ίσα, τότε η αναζήτηση έχει τελειώσει. Αν όχι, τότε µπορεί ο µισός πίνακας να εξαιρεθεί από τις επόµενες αναζητήσεις γιατί αποκλείεται να περιέχει το ζητούµενο στοιχείο, εφόσον αυτός είναι ταξινοµηµένος. Η διαδικασία αυτή επαναλαµβάνεται µέχρις ότου βρεθεί το στοιχείο ή διαπιστωθεί ότι δεν υπάρχει. Η µέθοδος αυτή είναι η δυαδική. Είναι φανερό ότι µε τη µέθοδο αυτή µετά από κάθε σύγκριση το µέγεθος του πίνακα που θα ανιχνευθεί στη συνέχεια είναι το ½, ¼,... του αρχικού µεγέθους. Η µέγιστη τιµή των απαιτούµενων συγκρίσεων δίνεται από τη σχέση Μέγιστη τιµή απαιτούµενων συγκρίσεων = log 2 N Το πλεονέκτηµα της µεθόδου αυτής είναι ότι είναι πολύ σταθερή, δηλαδή συνήθως η επίδοση είναι πολύ κοντά στη µέση τιµή των συγκρίσεων. Το µειονέκτηµα είναι ότι ο πίνακας πρέπει να είναι ταξινοµηµένος. 23
Αριθµός συγκρίσεων µε υαδική Αναζήτηση: Στοιχεία Ν Συγκρίσεις 10 4 100 7 1000 10 10000 14 100000 17 1000000 20 10000000 24 100000000 27 1000000000 30 υαδική αναζήτηση µε αναδροµή int binarysearch(int left, int right, int p[], int x) int mid, pos; if (left > right) pos = -1; mid = (left + right) / 2; if (p[mid] == x) pos = mid; if (x < p[mid]) pos = binarysearch(left,mid-1,p,x); pos = binarysearch(mid+1,right,p,x); return pos; 24
Κεφάλαιο 4 ΤΑΞΙΝΟΜΗΣΗ 4.1 Εισαγωγή 4.2 Ταξινόµηση µε απευθείας επιλογή 4.3 Ταξινόµηση µε απευθείας εισαγωγή 4.4 Ταξινόµηση φυσαλίδας 4.5 Γρήγορη ταξινόµηση 25
4.1 ΕΙΣΑΓΩΓΗ Ταξινόµηση είναι η διαδικασία της τοποθέτησης ενός συνόλου στοιχείων σε µία ιδιαίτερη σειρά. Η σειρά αυτή είναι συνήθως αύξουσα (ascending) ή φθίνουσα (descending). Σκοπός της ταξινόµησης είναι η διευκόλυνση της αναζήτησης στοιχείων του ταξινοµηµένου συνόλου. Η χρησιµότητα της ταξινόµησης φαίνεται στην πράξη σε περιπτώσεις αναζήτησης σε τηλεφωνικούς καταλόγους, σε βιβλιοθήκες, σε λεξικά, σε διάφορες δηµόσιες υπηρεσίες και οργανισµούς και γενικά παντού όπου υπάρχουν αποθηκευµένα δεδοµένα και πρέπει να αναζητηθούν και να βρεθούν. Το θέµα της ταξινόµησης παρουσιάζει µεγάλο ενδιαφέρον και έχουν αναπτυχθεί αρκετοί αλγόριθµοι ταξινόµησης. Στο κεφάλαιο αυτό θα περιγραφούν κάποιοι απ αυτούς. Επίσης, θα πρέπει να λαµβάνεται υπόψη: Η επιλογή µίας συγκεκριµένης δοµής δεδοµένων επηρεάζει τους αλγορίθµους που εκτελούν ένα έργο. Η επιλογή ενός αλγορίθµου είναι µία δύσκολη διαδικασία που µπορεί να διευκολυνθεί µε την ανάλυση της επίδοσης τους (performance analysis). Υπάρχει µία θεωρητικά βέλτιστη επίδοση που κανείς αλγόριθµος ταξινόµησης δεν µπορεί να ξεπεράσει. 4.2 ΤΑΞΙΝΟΜΗΣΗ ΜΕ ΑΠΕΥΘΕΙΑΣ ΕΠΙΛΟΓΗ (Straight Selection) Η µέθοδος έχει ως εξής: Επιλέγουµε το µικρότερο στοιχείο Το ανταλλάσσουµε µε το πρώτο στοιχείο Επαναλαµβάνουµε για τα υπόλοιπα στοιχεία, µέχρι να µείνει ένα Π.χ. 44 55 12 42 94 18 06 67 06 55 12 42 94 18 44 67 06 12 55 42 94 18 44 67 06 12 18 42 94 55 44 67 06 12 18 42 94 55 44 67 06 12 18 42 44 55 94 67 06 12 18 42 44 55 94 67 06 12 18 42 44 55 67 94 26
Ο αλγόριθµος σε C: ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ for (i=0; i<n-1; i++) k = i; min = p[i]; for (j = i+1; j<n; j++) if (p[j] < min) k = j; min = p[j]; p[k] = p[i] ; p[i] = min; 4.3 ΤΑΞΙΝΟΜΗΣΗ ΜΕ ΑΠΕΥΘΕΙΑΣ ΕΙΣΑΓΩΓΗ (Straight Insertion) Η µέθοδος είναι πολύ δηµοφιλής στους χαρτοπαίχτες και έχει ως εξής: Π.χ. Κάθε στοιχείο, ξεκινώντας από το δεύτερο, τοποθετείται στη σωστή του θέση µετακινώντας, αν χρειαστεί, τα στοιχεία δεξιά του κατά µία θέση. 44 55 12 42 94 18 06 67 44 55 12 42 94 18 06 67 12 44 55 42 94 18 06 67 12 42 44 55 94 48 06 67 12 42 44 55 94 18 06 67 12 18 42 44 55 94 06 67 06 12 18 42 44 55 94 67 06 12 18 42 44 55 67 94 Ο αλγόριθµος σε C: for (i=2; i<=n; i++) x = p[i]; p[0] = x; j = i-1; while (x < p[j]) p[j+1] = p[j] ; j = j-1 ; p[j+1] = x ; 27
4.4 ΤΑΞΙΝΟΜΗΣΗ ΦΥΣΑΛΙ ΑΣ (Bubble Sort) Ο αλγόριθµος βασίζεται στην αρχή της σύγκρισης και ανταλλαγής ζευγών από γειτονικά στοιχεία, µέχρις ότου ταξινοµηθούν όλα τα στοιχεία. Κάθε φορά µετακινείται το µικρότερο στοιχείο της ακολουθίας προς το αριστερό άκρο. Π.χ. 44 55 12 42 94 18 06 67 06 44 55 12 42 94 18 67 06 12 44 55 18 42 94 67 06 12 18 44 55 42 67 94 06 12 18 42 44 55 67 94 06 12 18 42 44 55 67 94 06 12 18 42 44 55 67 94 06 12 18 42 44 55 67 94 Ο αλγόριθµος σε C: for (i=1; i<n; i++) for (j=n-1; j>=i; j--) if (p[j-1] > p[j]) temp = p[j-1]; p[j-1] = p[j] ; p[j] = temp ; 4.5 ΓΡΗΓΟΡΗ ΤΑΞΙΝΟΜΗΣΗ (Quick Sort) Ο αλγόριθµος βασίζεται στην αρχή της αντιµετάθεσης και είναι η καλύτερη γνωστή µέθοδος ταξινόµησης για τυχαία στοιχεία. Η γρήγορη ταξινόµηση στηρίζεται στην παρατήρηση ότι είναι προτιµότερο οι αντιµεταθέσεις να γίνονται µεταξύ αποµακρυσµένων στοιχείων. Στην αρχή λαµβάνεται το πρώτο στοιχείο του πίνακα και µετακινείται στη θέση όπου τελικά θα αποθηκευτεί στο ταξινοµηµένο διάνυσµα. Μετά τον προσδιορισµό της τελικής αυτής θέσης, γίνεται αναδιάταξη των υπόλοιπων στοιχείων έτσι ώστε να µην υπάρχει κανένα µικρότερο στοιχείο προς τα αριστερά του και κανένα µεγαλύτερο στοιχείο προς τα δεξιά του. Έτσι, το πρώτο αυτό στοιχείο παίζει το ρόλο του άξονα (pivot) και ο πίνακας έχει διαµεριστεί κατά τέτοιο τρόπο ώστε το αρχικό πρόβληµα έχει αναχθεί σε δύο απλούστερα προβλήµατα, στην ανεξάρτητη δηλαδή ταξινόµηση 28
των δύο υποπινάκων. Μετά τον διαµερισµό του πίνακα, η ίδια διαδικασία εφαρµόζεται στους δύο υποπίνακες, ύστερα στους υποπίνακες των υποπινάκων, κοκ., µέχρις ότου ο κάθε υποπίνακας να αποτελείται από ένα µόνο στοιχείο. Στο σηµείο αυτό ο αρχικός πίνακας έχει ταξινοµηθεί. Π.χ. 36 47 18 59 32 73 41 27 51 i x j j 27 47 18 59 32 73 41 36 51 i j j j 27 32 18 59 47 73 41 36 51 i j j j 27 18 32 59 47 73 41 36 51 i j i [27 18] 32 [59 47 73 41 36 51] i j i j i i 18 27 59 47 51 41 36 73 i j i j i i i i 18 27 [59 47 51 41 36] 73 i j 36 47 51 41 59 i j i j i 36 47 41 51 59 i j i [36 47 41] 51 i j i 36 41 47 j i [36 41] 47 i j j 36 41 29
Ο αλγόριθµος σε C: void quicksort(int left, int right, int p[]) int i, j, mid, x, temp; ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ if (left < right) i = left; j = right; mid = (left+right)/2; x = p[mid]; while (i < j) while (p[i] < x) i++; while (p[j] > x) j--; if (i < j) if (p[i] == p[j]) if (i<mid) i++; if (j>mid) j--; temp = p[i]; p[i] = p[j]; p[j] = temp; quicksort(left,j-1,p); quicksort(j+1,right,p); 30
Κεφάλαιο 5 ΓΡΑΜΜΙΚΕΣ ΛΙΣΤΕΣ 5.1 Εισαγωγή 5.2 Σειριακές Λίστες 5.2.1 Στοίβα 5.2.2 Ουρά 5.3 Συνδεδεµένες Λίστες 5.3.1 Απλή συνδεδεµένη λίστα 5.3.2 Στοίβα ως συνδεδεµένη λίστα 5.3.3 Ουρά ως συνδεδεµένη λίστα 31
5.1 ΕΙΣΑΓΩΓΗ Γραµµική λίστα (linear list) είναι ένα πεπερασµένο σύνολο από κόµβους x 1, x 2,..., x n όπου το στοιχείο x k προηγείται του στοιχείου x k+1 και έπεται του x k-1. Κατατάσσονται συνήθως σε δύο κατηγορίες: Σειριακές γραµµικές λίστες (sequential linear lists) Συνδεδεµένες γραµµικές λίστες (linked linear lists) Στην πρώτη κατηγορία καταλαµβάνονται συνεχόµενες θέσεις µνήµης του Η/Υ για την αποθήκευση των κόµβων. Στην δεύτερη κατηγορία οι κόµβοι των λιστών βρίσκονται σε αποµακρυσµένες θέσεις που είναι µεταξύ τους συνδεδεµένες. Επίσης, οι γραµµικές λίστες χαρακτηρίζονται σαν: Στατικές δοµές δεδοµένων (static data structures) υναµικές δοµές δεδοµένων (dynamic data structures) Στην πρώτη κατηγορία, κατά τον προγραµµατισµό των λειτουργιών των λιστών, έχει προκαθορισθεί το µέγεθος της µνήµης που απαιτείται για την αποθήκευση των λιστών. Στην δεύτερη κατηγορία, µία λίστα µπορεί να αυξοµειωθεί κατά την διάρκεια εκτέλεσης του προγράµµατος. 5.2 ΣΕΙΡΙΑΚΕΣ ΛΙΣΤΕΣ 5.2.1 Στοίβα (stack) Μπορούµε να την παραλληλίσουµε σαν µία στοίβα από πιάτα. Κάθε νέο στοιχείο τοποθετείται στην κορυφή (top). Το στοιχείο που βρίσκεται στην κορυφή της στοίβας εξέρχεται πρώτο. Αυτή η µέθοδος επεξεργασίας ονοµάζεται LIFO (Last In First Out) 4 3 top 2 15 1 8 0 12 32
Μία στατική στοίβα υλοποιείται µε τη χρήση ενός µονοδιάστατου πίνακα και ενός δείκτη. ύο είναι οι κύριες λειτουργίες στη στοίβα: Ώθηση (push) στοιχείου στην κορυφή της στοίβας Απώθηση (pop) στοιχείου από τη στοίβα Η διαδικασία της ώθησης πρέπει οπωσδήποτε να ελέγχει µήπως η στοίβα είναι γεµάτη, οπότε έχουµε υπερχείλιση (overflow). Αντίστοιχα, η διαδικασία της απώθησης πρέπει να ελέγχει αν η στοίβα έχει αδειάσει, οπότε έχουµε υποχείλιση (underflow). Υλοποίηση στοίβας σε C: #define N 100 int stack[n], top = -1; void push(int stack[],int *t,int obj) if (*t = = N-1) printf("stack overflow...\n"); getch(); abort(); stack[++(*t)] = obj; int pop(int stack[],int *t) int r ; if (*t < 0) printf("stack empty...\n"); getch(); abort(); r = stack[(*t)--]; return r; 33
ΕΦΑΡΜΟΓΕΣ ΣΤΟΙΒΩΝ Εφαρµογή 1 Μία εφαρµογή που µπορεί να χρησιµοποιήσει τη δοµή της στοίβας για την υλοποίησή της, είναι ο έλεγχος σωστής χρήσης των παρενθέσεων στις αριθµητικές εκφράσεις. Για παράδειγµα η παράσταση (a*(b+c)+d) χρησιµοποιεί σωστά τις παρενθέσεις, ενώ η παράσταση (a*b+c)+d) δεν χρησιµοποιεί σωστά τις παρενθέσεις, γιατί πλεονάζει µία δεξιά παρένθεση. Το ίδιο και η παράσταση (a-b))*b+c). Επίσης η παράσταση (a*(b+c+d) δεν τις χρησιµοποιεί σωστά, γιατί πλεονάζει µία αριστερή παρένθεση. Υλοποίηση στη C: // Έλεγχος για σωστή χρήση παρενθέσεων #include <stdio.h> #include <conio.h> #include <stdlib> #define N 10 void push(char stack[],int *t, char obj); void pop(char stack[],int *t); main() char stack[n], top = -1; char expr[30]; int i, error; clrscr(); printf("input expression, ENTER to quit:\n"); gets(expr); error = 0; i = 0; while (expr[i]!='\0' && error = = 0) switch (expr[i]) case '(': push(stack,&top,expr[i]); break; case ')': if (top = = -1) error = 1; 34
pop(stack,&top); break; i++; if (top = = -1) if (error = = 0) printf("correct expression."); printf("error. Too many right parentheses."); printf("error. Too many left parentheses."); getch(); return 0; void push(char stack[], int *t, char obj) if ((*t) = = N-1) printf("stack overflow...\n"); getch(); abort(); stack[++(*t)] = obj; void pop(char stack[],int *t) char c; if ((*t) < 0) printf("stack empty...\n"); printf("error in expression.\n"); getch(); abort(); c = stack[(*t)--]; ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ Εφαρµογή 2 Μία κλασσική εφαρµογή που χρησιµοποιεί τη δοµή της στοίβας, είναι η επεξεργασία αριθµητικών εκφράσεων, όπως 2+3 ή 2*(3+4) ή ακόµα ((2+4)*7)+3*(9-5), από τις γλώσσες προγραµµατισµού. Αυτή η µορφή παράστασης ονοµάζεται 35
ένθετη (infix), και έχει το χαρακτηριστικό ότι οι τελεστές (operators) τοποθετούνται µεταξύ των τελεσταίων (operands). Κάθε ένθετη παράσταση χρησιµοποιεί προτεραιότητες (precedences) στη σειρά εκτέλεσης των πράξεων. Π.χ. στην παράσταση 3+4*5 έχει προτεραιότητα η πράξη του πολλαπλασιασµού. Φυσικά οι προτεραιότητες τροποποιούνται µε τη χρήση παρενθέσεων, π.χ. 2*(3+4) προτεραιότητα έχει η πράξη µέσα στις παρενθέσεις, δηλαδή η πρόσθεση. Τελεστής ή τελεσταίος Προτεραιότητα Μεταβλητή ή σταθερά 6 ^, ** 5 *, / 4 +, - 3 <, >, <=, >=, = =,!= 2 (, = 1 ) 0 Αν όµως ο µεταφραστής προσπαθήσει να εκτελέσει µία παράσταση στη ένθετη µορφή της, όπως π.χ. την 3+4*5, όταν φθάσει στον τελεστή της πρόσθεσης δεν γνωρίζει αν θα πρέπει να εκτελέσει την πρόσθεση ή να την αναβάλει για αργότερα, γιατί πιθανόν να προηγείται κάποια άλλη πράξη. Για το λόγο αυτό ο µεταφραστής ακολουθεί τις ακόλουθες δύο διαδικασίες για το σωστό υπολογισµό των εκφράσεων: Η ένθετη µορφή της αριθµητικής έκφρασης µεταφράζεται σε επιθεµατική (postfix) µορφή, όπου οι τελεστές εµφανίζονται µετά τους τελεσταίους. Π.χ η παράσταση 2+3 γίνεται 23+ και η παράσταση 3+4*5 µετατρέπεται σε 345*+ Η επιθεµατική µορφή χρησιµοποιείται κατόπιν για τον υπολογισµό της έκφρασης. Σηµ.: Η επιθεµατική µορφή ονοµάζεται και Αντίστροφη Πολωνική Μορφή (Reverse Polish Notation RPN) γιατί ανακαλύφθηκε από έναν Πολωνό µαθηµατικό. 36
Μετάφραση από ένθετη σε επιθεµατική µορφή Για τη µετατροπή µιας ένθετης µορφής σε επιθεµατική ακολουθείται µία διαδικασία σάρωσης της αριθµητικής έκφρασης και εφαρµογής των παρακάτω κανόνων: Αν το στοιχείο είναι τελεσταίος, τότε τοποθετείται στα δεξιά της επιθεµατικής µορφής. Αν το στοιχείο είναι τελεστής, τότε κατευθύνεται σε µία στοίβα. Εκεί συγκρίνεται µε τον τελεστή της κορυφής της στοίβας. Αν ο εισερχόµενος τελεστής έχει µεγαλύτερη προτεραιότητα από τον τελεστή της κορυφής της στοίβας, τότε ο τελεστής τοποθετείται στη στοίβα (push). Αν η προτεραιότητα του εισερχόµενου τελεστή είναι µικρότερη ή ίση από την προτεραιότητα του τελεστή της κορυφής της στοίβας, τότε: o Εξάγονται όλοι οι τελεστές της στοίβας µε προτεραιότητα µεγαλύτερη ή ίση από την προτεραιότητα του νέου τελεστή (pop) και τοποθετούνται στα δεξιά της επιθεµατικής µορφής. o Ο νέος τελεστής τοποθετείται στη στοίβα (push). Όταν η αριθµητική έκφραση έχει παρενθέσεις, τότε ακολουθούνται οι παρακάτω κανόνες: Η αριστερή παρένθεση τοποθετείται αµέσως στη στοίβα (push). Η δεξιά παρένθεση προκαλεί την εξαγωγή (pop) όλων των τελεστών µέχρι να συναντηθεί η αριστερή παρένθεση στη στοίβα. Ύστερα, οι δύο παρενθέσεις αγνοούνται. Π.χ. η παράσταση (2*(3/2+4)-3)/2 µετατρέπεται σε 232/4+*3-2/. Σάρωση ενθεµατικής Επιθεµατική παράστασης Στοίβα µορφή ( ( 2 ( 2 * (* 2 ( (*( 2 37
3 (*( 23 / (*(/ 23 2 (*(/ 232 + (*(+ 232/ 4 (*(+ 232/4 ) (* 232/4+ - (- 232/4+* 3 (- 232/4+*3 ) 232/4+*3- / / 232/4+*3-2 / 232/4+*3-2 ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ end 232/4+*3-2/ Υλοποίηση στη C: // Μετατροπή infix σε postfix #include <stdio.h> #include <conio.h> #include <stdlib> #define N 100 void push(char stack[],int *t, char obj); char pop(char stack[],int *t); void gotoper(char stack[], int *t, char opnew, int precnew, char postfix[], int *k); void gotparen(char stack[], int *t, char postfix[], int *k); main() char stack[n], top = -1; int i, j; char infix[100], postfix[100]; clrscr(); printf("input expression, ENTER to quit:\n"); gets(infix); i = 0; j = 0; while (infix[i]!='\0') switch (infix[i]) case '+': case '-': gotoper(stack,&top,infix[i],3,postfix,&j); break; 38
case '*': case '/': gotoper(stack,&top,infix[i],4,postfix,&j); break; case '(': push(stack,&top,infix[i]); break; case ')': gotparen(stack,&top,postfix,&j); break; default: postfix[j++] = infix[i]; break; i++; postfix[j] = '\0'; j = 0; while (postfix[j]!= '\0') printf("%c",postfix[j]); j++; getch(); return 0; void push(char stack[], int *t, char obj) if ((*t) == N-1) printf("stack overflow...\n"); getch(); abort(); stack[++(*t)] = obj; char pop(char stack[],int *t) char c; if ((*t) < 0) printf("stack empty...\n"); printf("error in expression.\n"); getch(); abort(); ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ 39
c = stack[(*t)--]; return c; ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ void gotoper(char stack[], int *t, char opnew, int precnew, char postfix[], int *k) int top, prectop, done = 0; char optop; top = *t; while(top!= -1 && done == 0) if (stack[top] =='(') prectop = 1; done = 1; if (stack[top] =='+' stack[top] == '-') prectop = 3; prectop = 4; if (prectop < precnew) done = 1; postfix[(*k)++] = pop(stack,&top); push(stack,&top,opnew); *t = top; void gotparen(char stack[], int *t, char postfix[], int *k) int top, done = 0; char cs; top = *t; while (top!= -1 && done ==0) cs = pop(stack,&top); if (cs =='(') done = 1; postfix[(*k)++] = cs; *t = top; 40
Υπολογισµός της επιθεµατικής µορφής Η διαδικασία υπολογισµού της αριθµητικής έκφρασης συνίσταται στη σάρωση της επιθεµατικής µορφής, µε την εφαρµογή των ακόλουθων δύο κανόνων: Αν το στοιχείο είναι τελεσταίος, τότε τοποθετείται σε µία στοίβα (push). Αν το στοιχείο είναι τελεστής, τότε: o Εξάγονται διαδοχικά δύο τελεσταίοι από την κορυφή της στοίβας (pop) o Εκτελείται η πράξη που δηλώνει ο τελεστής o Το αποτέλεσµα τοποθετείται στη στοίβα (push) Όταν τελειώσει η σάρωση της επιθεµατικής µορφής, στη στοίβα έχει αποµείνει το τελικό αποτέλεσµα, που είναι η τιµή της αριθµητικής παράστασης. π.χ. Σάρωση επιθεµατικής µορφής Στοίβα 2 2 3 2, 3 2 2, 3, 2 / (3/2) 2, 1.5 4 2, 1.5, 4 + (1.5+4) 2, 5.5 * (5.5*2) 11 3 11, 3 - (11-3) 8 2 8, 2 / (8/2) 4 5.2.2 Ουρά (queue) Την έννοια της ουράς την συναντάµε συχνά στην καθηµερινή µας ζωή, π.χ. ουρά αναµονής µε ανθρώπους. Το άτοµο που είναι πρώτο στην ουρά, εξυπηρετείται και εξέρχεται. Το άτοµο που µόλις καταφθάνει, τοποθετείται στο τέλος της ουράς. Η µέθοδος αυτή επεξεργασίας ονοµάζεται FIFO (First In First Out) 41
ύο βασικές λειτουργίες: Εισαγωγή (enqueue) στοιχείου στο πίσω άκρο της ουράς Εξαγωγή (dequeue) στοιχείου από το εµπρός άκρο της ουράς Εποµένως, για την υλοποίηση της ουράς χρειάζονται ένας πίνακας και δύο δείκτες, ο εµπρός (front) και ο πίσω (rear). Επειδή βολεύει προγραµµατιστικά ο δείκτης rear δείχνει πάντα στο τελευταίο στοιχείο, ενώ ο δείκτης front δείχνει µία θέση πριν το πρώτο στοιχείο και, κατά συνέπεια, η ισότητα των δύο δεικτών αποδεικνύει ότι η ουρά είναι άδεια. 0 1 2 3 4 5 15 8 12 front rear Υλοποίηση ουράς σε C: #define N 100 int q[n], front = -1, rear = -1; void enqueue(int q[], int *r, int obj) if (*r = = N-1) printf("queue is full..."); getch(); q[++(*r)] = obj; void dequeue(int q[], int *f, int r) int x; if (*f = = r) printf("queue is empty...\n"); 42
x = q[++(*f)]; printf("%d has been deleted...",x); ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ Η υλοποίηση της ουράς µε πίνακα έχει ένα µειονέκτηµα. Υπάρχει περίπτωση ο rear να φθάσει στο πάνω όριο του πίνακα, αλλά στην ουσία να µην υπάρχει υπερχείλιση, επειδή ο front θα έχει αυξηθεί (εικονική υπερχείλιση). π.χ. 0 1 2 3 4 5 15 8 12 16 front rear Στην πραγµατικότητα υπάρχει ελεύθερος χώρος για την εισαγωγή και νέων στοιχείων. Μία λύση θα ήταν να µεταφερθούν τα στοιχεία στο αριστερό άκρο του πίνακα. π.χ. 0 1 2 3 4 5 15 8 12 16 front rear elements = front rear; first = front + 1; for (i=0; i<elements; i++) q[i] = q[first++]; front = -1; rear = elements 1; Μία πιο αποτελεσµατική υλοποίηση θα ήταν η ουρά να αναδιπλώνεται, δηλαδή όταν ο rear = N-1, να επανατοποθετείται στο 0. Η δοµή αυτή ονοµάζεται κυκλική ουρά (circular queue). 0 1 2 3 4 5 21 15 8 12 16 rear front 43
Εφαρµογές Ουράς Η δοµή της ουράς ενδείκνυται για προβλήµατα όπου σχηµατίζονται ουρές από ανθρώπους. Ένα τέτοιο παράδειγµα είναι µία ουρά από πελάτες που περιµένουν σε µία τράπεζα για να εξυπηρετηθούν. Η εφαρµογή που περιγράφεται παρακάτω υλοποιεί µία τέτοια κατάσταση. Θεωρούµε ότι η τράπεζα διατηρεί ένα αρχείο µε εγγραφές πελατών µε την εξής δοµή: Αριθµός λογαριασµού Ονοµατεπώνυµο πελάτη Ποσό λογαριασµού Το πρόγραµµα είναι καθοδηγούµενο από ένα µενού, µε τις εξής βασικές επιλογές: 1. Άφιξη πελάτη στην τράπεζα 2. Αναχώρηση πελάτη (εφόσον έχει εξυπηρετηθεί) Κάθε φορά που καταφθάνει ένας πελάτης, επιλογή 1, τοποθετείται στο τέλος µιας ουράς µε τη λειτουργία enqueue (στην ουσία τοποθετείται ο αριθµός λογαριασµού του πελάτη, αν και θα µπορούσε να είναι κάποιος αριθµός προτεραιότητας). Όταν ο χρήστης επιλέξει τη λειτουργία 2, τότε αυτοµάτως ο πελάτης που βρίσκεται στην αρχή της ουράς, προωθείται για να εξυπηρετηθεί, αφού πρώτα γίνει εξαγωγή του αριθµού λογαριασµού από την ουρά µε την λειτουργία dequeue. Βάσει του, καταχωρηµένου στην ουρά, αριθµού λογαριασµού επιχειρείται µία αναζήτηση στο αρχείο των πελατών. Αν η εγγραφή βρεθεί τότε, αφού εµφανιστούν τα περιεχόµενα της εγγραφής στην οθόνη, ο πελάτης ζητείται να επιλέξει µια εκ των δύο συναλλαγών 1.Ανάληψη 2.Κατάθεση Αφού δοθεί το είδος της συναλλαγής, κατόπιν ζητείται το ποσόν της συναλλαγής το οποίο και προστίθεται ή αφαιρείται από το τρέχον ποσό του λογαριασµού. Στο τέλος η ενηµερωµένη εγγραφή καταχωρείται πίσω στο αρχείο. Αν η αναζήτηση στο αρχείο είναι ανεπιτυχής, τότε ο αριθµός λογαριασµού αγνοείται και η ροή του προγράµµατος επανέρχεται στο αρχικό µενού. 44
Τέλος, αν ο χρήστης επιλέξει την έξοδο από το µενού, τότε το πρόγραµµα τερµατίζει, αφού πρώτα εµφανίσει το πλήθος των πελατών που περιµένουν στην ουρά για να εξυπηρετηθούν. Υλοποίηση στην C: // Εφαρµογή Ουράς // Εξυπηρέτηση πελατών σε τράπεζα #include <stdio.h> #include <conio.h> #include <stdlib> #include <string.h> #define N 100 struct account int numb; char name[20]; float ammount; ; typedef struct account bankaccount; int getselection(bankaccount acc); void updateammount(bankaccount *p, int sel); int searchfile(file *f, int x, bankaccount *p); void fillblanks(char *s, int x); void displayfile(file *f); void enqueue(int q[n],int *r,int obj); int dequeue(int q[n],int *f,int rear); main() int front = -1, rear = -1, choice, telos = 0; int q[n]; FILE *bfp; bankaccount acc; int found, xcode, sel, reclen, ncust = 0, i; clrscr(); bfp = fopen("accounts.dat","rb+"); displayfile(bfp); rewind(bfp); do clrscr(); printf(" Menu\n"); printf(" ====\n"); 45
printf("1.customer arrival\n"); printf("2.customer departure\n"); printf("3.exit\n"); printf("\nchoice?"); scanf("%d",&choice); switch (choice) case 1: printf("\ngive account number:"); scanf("%i",&xcode); enqueue(q,&rear,xcode); break; case 2: xcode = dequeue(q,&front,rear); if (xcode!= 0) found = 0; found = searchfile(bfp,xcode,&acc); if (found = = 1) sel = getselection(acc); updateammount(&acc,sel); reclen = sizeof(bankaccount); fseek(bfp,-reclen,1); fwrite(&acc, sizeof(bankaccount), 1, bfp); printf("\naccount number invalid!\n"); getch(); rewind(bfp); break; case 3: telos = 1; break; while (telos = = 0); if (front = = rear) printf("no customers waiting in queue..."); ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ for (i=front+1; i<=rear; i++) ncust++; printf("number of customers waiting in queue: %d ",ncust); getch(); 46
fclose(bfp); return 0; ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ void enqueue(int q[n],int *r,int obj) if ((*r) == N-1) printf("queue is full..."); getch(); q[++(*r)] = obj; int dequeue(int q[n],int *f,int rear) int pel; if ((*f) == rear) printf("queue is empty...\n"); pel = 0; pel = q[++(*f)]; printf("customer %d is served...",pel); getch(); return pel; int getselection(bankaccount acc) int sel; printf("\naccount no.: %i\n", acc.numb); printf("name : %s\n", acc.name); printf("ammount : %6.2f\n", acc.ammount); printf("\n1. Deposit\n"); printf("2. Withdraw\n"); printf("\n\nselect transaction:"); do scanf("%d",&sel); while (sel<1 sel>2); return sel; 47
void updateammount(bankaccount *p, int sel) float poso; printf("give ammount:"); scanf("%f",&poso); if (sel ==1) p->ammount += poso; if (poso <= p->ammount) p->ammount -= poso; printf("ammount too large to withdraw!"); getch(); int searchfile(file *f, int x, bankaccount *p) bankaccount acc; int found = 0; fread(&acc, sizeof(bankaccount), 1, f); while (!feof(f) && found == 0) if (x == acc.numb) found = 1; fread(&acc, sizeof(bankaccount), 1, f); *p = acc; return found; void fillblanks(char *s, int x) int n, i; n = x - strlen(s); for (i=1; i<=n; i++) strcat(s," "); void displayfile(file *f) bankaccount acc; printf("\nthe contents of the file are:\n\n"); printf("acc.no. NAME AMMOUNT\n"); ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ 48
printf("======= ==================== =======\n"); fread(&acc, sizeof(bankaccount), 1, f); while (!feof(f)) fillblanks(acc.name,20); printf("%7d %s %8.2f\n",acc.numb, acc.name, acc.ammount); fread(&acc, sizeof(bankaccount), 1, f); getch(); 5.3 ΣΥΝ Ε ΕΜΕΝΕΣ ΛΙΣΤΕΣ Οι στατικές δοµές που µελετήθηκαν, παρουσιάζουν προβλήµατα στην εισαγωγή και διαγραφή κόµβων και στην αποδοτική εκµετάλλευση της διαθέσιµης µνήµης. Κύριο χαρακτηριστικό των συνδεδεµένων λιστών είναι ότι οι κόµβοι τους βρίσκονται σε αποµακρυσµένες θέσεις µνήµης και η σύνδεσή τους γίνεται µε δείκτες. Κατ αυτό τον τρόπο, η εισαγωγή και διαγραφή κόµβων γίνεται πολύ πιο απλά. Ένα άλλο θετικό χαρακτηριστικό είναι ότι, δεν απαιτείται εκ των προτέρων καθορισµός του µέγιστου αριθµού κόµβων της λίστας και µπορεί η λίστα να επεκταθεί ή να συρρικνωθεί κατά την εκτέλεση του προγράµµατος (δυναµικές δοµές). head 32 41 48 51 54 NULL Κάθε κόµβος της λίστας υλοποιείται µε µία δοµή (structure) µε δύο στοιχεία. Το ένα µέλος (data) περιέχει τα δεδοµένα (οποιουδήποτε τύπου) του κόµβου και το άλλο µέλος (next) είναι δείκτης προς τον επόµενο κόµβο. Τοποθετείται ένας δείκτης (head) στον πρώτο κόµβο για να προσπελάζεται η λίστα, ενώ ο δείκτης του τελευταίου κόµβου δείχνει στο NULL για να εντοπίζεται το τέλος της λίστας. 5.3.1 Απλή Συνδεδεµένη Λίστα Παράδειγµα υλοποίησης λίστας ακεραίων στην C: struct node int data; struct node *next; ; typedef struct node * PTR; 49
α) ηµιουργία λίστας PTR list_create(ptr head) PTR current; int x; ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ printf( Give an integer, 0 to stop: ); scanf( %i,&x); if (x = = 0) return NULL; head = malloc(sizeof(struct node)); head->data = x; current = head; printf( Give an integer, 0 to stop: ); scanf( %i,&x); while (x!=0) current->next = malloc(sizeof(struct node)); current = current->next; current->data = x; printf( Give an integer, 0 to stop: ); scanf( %i,&x); current->next = NULL; return head; β) Εισαγωγή στοιχείου στη σωστή θέση (υποτίθεται η λίστα είναι ταξινοµηµένη ) PTR insert_to_list(ptr head, int x) PTR current, previous, newnode; int found; newnode = malloc(sizeof(struct node)); newnode->data = x; newnode->next = NULL; if (head = = NULL) head = newnode; if (newnode->data < head->data) newnode->next = head; head = newnode; 50
previous = head; current = head->next; found = 0; while (current!= NULL && found = = 0) if (newnode->data < current->data) found = 1; previous = current; current = current->next; previous->next = newnode; newnode->next = current; return head; γ) ιαγραφή στοιχείου από λίστα PTR delete_from_list(ptr head, int x) PTR current, previous; int found; current = head; if (current = = NULL) printf("empty list...nothing to delete.\n"); getch(); if (x = = head->data) head = head->next; free(current); previous = current; current = head->next; found = 0; while (current!= NULL && found = = 0) if (x = = current->data) found = 1; ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ 51
previous = current; current = current->next; if (found = = 1) previous->next = current->next; free(current); printf("\nthe character is not in the list."); getch(); return head; δ) Εκτύπωση λίστας void print_list(ptr head) PTR current; current = head; if (current = = NULL) printf("the list is empty.\n"); while (current!= NULL) printf("%i ", current->data); current = current->next; ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ 5.3.2 Στοίβα ως Συνδεδεµένη Λίστα Η στοίβα υλοποιείται µε έναν δείκτη top που αρχικοποιείται στο NULL, και δείχνει ότι η στοίβα είναι άδεια. Η λειτουργία push δεν ελέγχει για υπερχείλιση γιατί θεωρητικά η στοίβα µπορεί να έχει όσους κόµβους θέλουµε (αφού δηµιουργείται δυναµικά). Επίσης, η λειτουργία pop, µε τη βοήθεια της εντολής free επιστρέφει στη διαθέσιµη µνήµη του Η/Υ το χώρο που καταλαµβάνονταν από τον κόµβο που διαγράφηκε. 52
top 32 11 18 NULL PTR top = NULL; PTR push(int obj, PTR t) PTR newnode; newnode = malloc(sizeof(struct node)); newnode->data = obj; newnode->next = t; t = newnode; return t; PTR pop(ptr t, int *obj) PTR p; if (t = = NULL) printf("stack empty.\n"); getch(); p = t; t = t->next; *obj = p->data; free(p); return t; 53
5.3.3 Ουρά ως Συνδεδεµένη Λίστα Η ουρά υλοποιείται µε τη χρήση δύο δεικτών front και rear που αρχικοποιούνται στην τιµή NULL. Η άδεια ουρά εκφράζεται µε front = NULL. Όπως και στην περίπτωση της στοίβας, δεν χρειάζεται να γίνεται έλεγχος υπερχείλισης από τη στιγµή που η ουρά αυξάνεται δυναµικά. 25 34 41 11 NULL front rear PTR front = NULL, rear = NULL; void enqueue(int obj, PTR *pf, PTR *pr) PTR newnode; newnode = malloc(sizeof(struct node)); newnode->data = obj; newnode->next = NULL; if ((*pf) = = NULL) *pf = newnode; *pr = newnode; (*pr)->next = newnode; *pr = newnode; void dequeue(ptr *pf, PTR *pr) PTR p; if ((*pf) = = NULL) printf("\nqueue empty. No elements to delete.\n"); p = *pf; *pf = (*pf)->next; if ((*pf) = = NULL) *pr = *pf; printf("\n%d has been deleted...\n",p->data); free(p); getch(); 54
Κεφάλαιο 6 ΕΝ ΡΑ 6.1 Εισαγωγή 6.2 υαδικά δένδρα 55
6.1 ΕΙΣΑΓΩΓΗ Οι δοµές που εξετάστηκαν στα προηγούµενα κεφάλαια, είχαν σαν κοινό χαρακτηριστικό τη γραµµικότητα µεταξύ των κόµβων τους. Στο κεφάλαιο αυτό θα εξετάσουµε την πιο ενδιαφέρουσα µή γραµµική δοµή, το δένδρο (tree). Ορισµός (αναδροµικός) Ένα δένδρο Τ είναι ένα πεπερασµένο σύνολο από έναν ή περισσότερους κόµβους. Ο πρώτος κόµβος του δένδρου ονοµάζεται ρίζα (root), ενώ οι υπόλοιποι κόµβοι απαρτίζουν άλλα υποσύνολα που µε τη σειρά τους είναι δένδρα και ονοµάζονται υποδένδρα (subtrees). Ο ορισµός είναι αναδροµικός γιατί µπορεί να θεωρηθεί ότι κάθε κόµβος του δένδρου είναι ρίζα σε κάποιο υποδένδρο. A B C D E F G T = (A, B, C, D, E, F, G) ρίζα Α. T1 = (B) T2 = (C, D, E, F, G) Βαθµός του κόµβου (node degree) Είναι ο αριθµός των υποδένδρων που αρχίζουν από ένα κόµβο. π.χ. Ο κόµβος Α έχει βαθµό 2. Ο κόµβος C έχει βαθµό 3. Βαθµός του δένδρου (tree degree) Είναι ο µέγιστος βαθµός από όλους τους βαθµούς κόµβων. 56
Τα δένδρα που χρησιµοποιούνται περισσότερο στην πράξη είναι τα δυαδικά δένδρα (binary trees), µε βαθµό 2. Οι κόµβοι ενός δένδρου από τους οποίους δεν αρχίζει κάποιο υποδένδρο λέγονται φύλλα (leaves) ή τερµατικοί κόµβοι (terminal nodes). Όλοι οι άλλοι κόµβοι λέγονται κλαδιά(branches). Η ρίζα ενός δένδρου ονοµάζεται πατέρας (father) των ριζών των υποδένδρων και οι ρίζες των υποδένδρων ονοµάζονται παιδιά (children) της ρίζας. Οι κόµβοι που έχουν τον ίδιο πατέρα ονοµάζονται αδελφοί (brothers). Όταν έχει σηµασία η διάταξη των κλαδιών ενός δένδρου, τότε αυτό λέγεται διατεταγµένο(ordered). Μία ενδιαφέρουσα µορφή διατεταγµένου δένδρου είναι το δυαδικό δένδρο αναζήτησης (binary search tree). Ένα δυαδικό δένδρο αναζήτησης είναι οργανωµένο έτσι ώστε για κάθε κόµβο t, όλα τα κλειδιά του αριστερού υποδένδρου να έχουν τιµή µικρότερη από την τιµή του κόµβου t και όλα τα κλειδιά του δεξιού υποδένδρου να έχουν τιµή µεγαλύτερη από την αντίστοιχη του κόµβου t. π.χ. AD B E A C F Ένας κόµβος y που βρίσκεται σε κατώτερο επίπεδο από ένα κόµβο x λέγεται απόγονος (descendant) του κόµβου x. Αντίστροφα, ο κόµβος x ονοµάζεται πρόγονος (ancestor) του κόµβου y. Η ρίζα του δένδρου βρίσκεται πάντοτε στο επίπεδο 1. Το µέγιστο επίπεδο όλων των κόµβων ενός δένδρου λέγεται βάθος(depth) ή ύψος (height) του δένδρου. άσος (forest) είναι ένα σύνολο από περισσότερα του ενός δένδρα που είναι ξένα µεταξύ τους. 57
6.2 ΥΑ ΙΚΑ ΕΝ ΡΑ (Binary trees) Ορισµός ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ Ένα δυαδικό δένδρο αποτελείται από ένα πεπερασµένο σύνολο κόµβων. Το δένδρο είναι είτε άδειο, είτε αποτελείται από δύο άλλα δυαδικά δένδρα που ονοµάζονται αριστερό και δεξιό υποδένδρο. Ορισµός στην C: struct treenode char data; struct treenode * left; struct treenode * right; ; typedef struct treenode * PTR; Μία συνηθισµένη εφαρµογή των δυαδικών δένδρων είναι η παράσταση αριθµητικών εκφράσεων στον Η/Υ. π.χ. Η αλγεβρική έκφραση A*B+C µπορεί να παρασταθεί ως + * C A B NULL NULL NULL NULL NULL NULL Μία σπουδαία λειτουργία σε ένα δένδρο είναι η διάσχισή του (traversal) ή διέλευση, η επίσκεψη δηλ. όλων των κόµβων του µία φορά. Υπάρχουν τρεις διαφορετικές µέθοδοι διάσχισης: α) Προδιατεταγµένη µέθοδος (preorder traversal) β) Ενδοδιατεταγµένη µέθοδος (inorder traversal) γ) Μεταδιατεταγµένη µέθοδος (postorder traversal) 58
π.χ. ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ AD B E A C F preorder 1. Επίσκεψη της ρίζας 2. Επίσκεψη του αριστερού υποδένδρου 3. Επίσκεψη του δεξιού υποδένδρου (Ο αλγόριθµος είναι αναδροµικός) Αποτελέσµατα: D, B, A, C, E, F Λεπτοµερής ανάλυση: 1. Ρίζα (D) 1 2. Αριστερά (B, A, C) => 1. Ρίζα (B) 2 3. εξιά (E, F) 2. Αριστερά (A) => 1. Ρίζα (A) 3 3. εξιά (C) 2. Αριστερά (άδειο) 1. Ρίζα (E) 5 3. εξιά (άδειο) 2. Αριστερά (άδειο) 1. Ρίζα (C) 4 3. εξιά (F) 2. Αριστερά (άδειο) 3. εξιά (άδειο) 1. Ρίζα (F) 6 2. Αριστερά (άδειο) 3. εξιά (άδειο) inorder 1. Επίσκεψη του αριστερού υποδένδρου 2. Επίσκεψη της ρίζας 3. Επίσκεψη του δεξιού υποδένδρου (Ο αλγόριθµος είναι αναδροµικός) Αποτελέσµατα: A, B, C, D, E, F 59
postorder 1. Επίσκεψη του αριστερού υποδένδρου 2. Επίσκεψη του δεξιού υποδένδρου 3. Επίσκεψη της ρίζας (Ο αλγόριθµος είναι αναδροµικός) Αποτελέσµατα: A, C, B, F, E, D Υλοποίηση στην C: void preorder_traversal(ptr t) if (t!=null) printf("%c ",t->data); preorder_traversal(t->left); preorder_traversal(t->right); void inorder_traversal(ptr t) if (t!=null) inorder_traversal(t->left); printf("%c ",t->data); inorder_traversal(t->right); void postorder_traversal(ptr t) if (t!=null) postorder_traversal(t->left); postorder_traversal(t->right); printf("%c,t->data); Εισαγωγή κόµβου σε υαδικό ένδρο Αναζήτησης: void insert_node(ptr *pt, char x) PTR t; t = *pt; 60
if (t= =NULL) t = malloc(sizeof (struct treenode)); t->data = x; t->left = NULL; t->right = NULL; if (x<t->data) insert_node(&(t->left),x); insert_node(&(t->right),x); *pt = t; ΑΛΓΟΡΙΘΜΟΙ & ΟΜΕΣ Ε ΟΜΕΝΩΝ ιαγραφή κόµβου από υαδικό ένδρο Αναζήτησης: Η διαδικασία της διαγραφής είναι πιο σύνθετη από τη διαδικασία της εισαγωγής. Αν ο κόµβος είναι τερµατικός, τότε είναι εύκολο. Επίσης εύκολη είναι η περίπτωση που ο διαγραφόµενος κόµβος έχει µόνον ένα απόγονο. ύσκολη είναι η περίπτωση που ο κόµβος έχει δύο απογόνους. Σ αυτή την περίπτωση ο κόµβος πρέπει να αντικατασταθεί είτε από τον πιο δεξιό κόµβο του αριστερού υποδένδρου, ή από τον πιο αριστερό κόµβο του δεξιού υποδένδρου. Προφανώς οι δύο αυτοί υποψήφιοι έχουν το πολύ έναν απόγονο. π.χ. 10 5 15 2 8 12 18 1 3 ιαγραφή του κόµβου 5. 61