Χαρακτηριστικά αναδροµής base case : συνθήκη τερµατισµού της αναδροµής Όταν το πρόβληµα είναι αρκετά µικρό ή απλό ώστε η λύση να είναι άµεση αναδροµικό βήµα : κλήση της ίδιας συνάρτησης για µικρότερη ή πιο απλή µορφή του ίδιου προβλήµατος κατασκευή της τελικής λύσης χρησιµοποιώντας τη λύση της πιο απλής µορφής
Παράδειγµα int factorial (int num) { if (num == 0) { return 0; base case return n * factorial(num-1); κατασκευή λύσης αναδροµικό βήµα
Συµβουλή Εµπιστευόµαστε ότι η αναδροµική κλήση θα λύσει σωστά το "µικρότερο" πρόβληµα Αποφεύγουµε να ακολουθούµε τις αναδροµικές κλήσεις σε βάθος γιατί θα χαθούµε! Αν η εκτέλεση τερµατίσει µε segmentation fault, ελέγχουµε τη συνθήκη τερµατισµού (base case) και τις παραµέτρους στην αναδροµική κλήση Προσέχουµε η συνθήκη τερµατισµού να ελέγχεται πάντα πριν γίνει η αναδροµική κλήση
Άσκηση 1 Υλοποιήστε την αναδροµική συνάρτηση int min (int table[], int start, int end) η οποία επιστέφει το µικρότερο ακέραιο που υπάρχει στον πίνακα table ανάµεσα στις θέσεις start και end (συµπεριλαµβανοµένων). Υποθέστε πώς το start δεν είναι µεγαλύτερο του end, και κανένα από τα δύο δεν είναι εκτός ορίων του πίνακα.
Λύση άσκησης 1 Ποιο είναι ένα πιο απλό/µικρό πρόβληµα? Η εύρεση του µικρότερου στοιχείο ανάµεσα στις θέσεις start+1 και end. Το πρόβληµα είναι µικρότερο γιατί δουλεύουµε σε µικρότερο εύρος. Πώς µπορούµε να βρούµε τη λύση του "µεγάλου" προβλήµατος τώρα? Συγκρίνοντας το στοιχείο στη θέση start µε το αποτέλεσµα του µικρότερου προβλήµατος Πότε σταµατάµε? Πότε είναι το πρόβληµα πολύ µικρό? Όταν το εύρος είναι 1! Με άλλα λόγια όταν start == end
Λύση άσκησης 1 int min (int table[], int start, int end) { int current_min; if (start == end) { return table[start]; current_min = min(table, start+1, end) if (table[start] < current_min) { return table[start]; else { return current_min;
Άσκηση 2 Ο Spiderman θέλει να διανύσει Ν οικοδοµικά τετράγωνα µε τη γνωστή µέθοδο. Γνωρίζοντας ότι µε κάθε εκτόξευση ιστού µπορεί να προχωρήσει είτε κατά ένα είτε κατά δύο τετράγωνα, υπολογίστε µε πόσους διαφορετικούς τρόπους µπορεί να διανύσει τα Ν τετράγωνα.
Άσκηση 2 - τρόπος σκέψης Θέλουµε µια συνάρτηση που να υπολογίζει όλους τους δυνατούς τρόπους να διανύσει ο Spiderman Ν τετράγωνα. Ξεκινώντας, έχει δύο επιλογές: Εκτοξεύει ιστό που διανύει ένα τετράγωνο. Σε αυτή την περίπτωση, η απάντηση του προβλήµατος είναι όσο όλοι οι δυνατοί τρόποι να διασχίσει Ν-1 τετράγωνα Εκτοξεύει ιστό που διανύει δύο τετράγωνα. Σε αυτή την περίπτωση, η απάντηση του προβλήµατος είναι όσο όλοι οι δυνατοί τρόποι να διασχίσει Ν-2 τετράγωνα Άρα, συνολικά, όλοι οι δυνατοί τρόποι είναι το άθροισµα των δύο παραπάνω λύσεων!
Άσκηση 2 - τρόπος σκέψης Προηγουµένως βρήκαµε το αναδροµικό βήµα. Τώρα πρέπει να σκεφτούµε το base case Έχουµε δύο "απλούστερες" περιπτώσεις: Υπάρχει µόνο ένα τετράγωνο. Σε αυτή την περίπτωση υπάρχει µόνο ένας τρόπος να το διανύσει ο Spiderman Υπάρχουν δύο τετράγωνα Σε αυτή την περίπτωση υπάρχουν δύο τρόποι να τα διανύσει. Είτε µε µια εκτόξευση που "πιάνει" δύο τετράγωνα,, είτε µε δύο εκτοξεύσεις που "πιάνουν" από ένα τετράγωνο η κάθε µια
Άσκηση 2 - µαθηµατική περιγραφή Αν θέλουµε να εκφράσουµε τη λύση µε µαθηµατικό τρόπο: f(ν) είναι η συνάρτηση που υπολογίζει το πλήθος των τρόπων να διανύσει ο Spiderman Ν τετράγωνα. f (N) = f(n-1) + f(n-2) f(1) = 1 f(2) = 2
Άσκηση 2 - σε C int travel (int blocks) { if (blocks == 1) return 1; if (blocks == 2) return 2; return ( travel(blocks-1) + travel(blocks-2));
Άσκηση 2 - εναλλακτικά Άραγε λύνεται και µε επανάληψη το πρόβληµα? ΝΑΙ! ΟΛΑ τα προβλήµατα που λύνονται αναδροµικά, µπορούν να λυθούν και επαναληπτικά. Για παράδειγµα, δείτε αυτή την επαναληπτική λύση για το Towers of Hanoi: for (x=1; x < (1 << disks); x++) { printf( "move from tower %d to tower %d.\n", (x&x-1)%3, ((x x-1)+1)%3 );
Άσκηση 2 - λύση µε επανάληψη int travel (int blocks) { int way1, way2, i, steps; way1 = 1; way2 = 2; for (i=3; i<=blocks; i++) { steps = way1+way2; way1 = way2; way2 = steps; if (blocks == 1) return way1; else return way2;
Άσκηση 3 Μια εναλλακτική υλοποίηση της συνάρτησης υπολογισµού παραγοντικού: int factorial (int num, int result) { if (num == 0) { return result; return factorial(num-1, num*result); με αρχική κλήση: factorial(n, 1);
Άσκηση 3 - πώς δουλεύει? factorial(4, 1) factorial(3, 4*1) factorial(2, 3*4*1) factorial(1, 2*3*4*1) factorial(0, 1*2*3*4*1) επιστρέφει 1*2*3*4*1 Είναι αυτό γνήσια αναδροµή? Έχει αναδροµικό βήµα? ΝΑΙ Έχει συνθήκη τερµατισµού? ΝΑΙ Κατασκευάζει το αποτέλεσµα από την πιο απλή λύση? ΟΧΙ!
Άσκηση 3 - τι συµβαίνει? Αυτό είναι ένα παράδειγµα tail recursion (αναδροµή ουράς?) Το αποτέλεσµα της συνάρτησης είναι ακριβώς ίδιο µε το αποτέλεσµα που µας επιστρέφει η αναδροµική κλήση. Επειδή η αναδροµική κλήση είναι η τελευταία εντολή στη συνάρτηση (η ουρά), ονοµάζουµε αυτή την αναδροµή tail recursion Η tail recursion ΔΕΝ είναι γνήσια αναδροµή - είναι ισοδύναµη µε επανάληψη και πολλοί compilers την αντικαθιστούν µε επανάληψη όταν κατασκευάζουν κώδικα assembly.
Από tail recursion σε επανάληψη - βήµα1 int factorial (int num, int result) { if (num == 0) return result; return factorial(num-1, num*result); int factorial (int num, int result) { start: if (num == 0) return result; result = num*result; num = num-1; goto start;
Από tail recursion σε επανάληψη - βήµα2 int factorial (int num, int result) { start: if (num == 0) return result; result = num*result; num = num-1; goto start; int factorial (int num, int result) { while (num!= 0) { result = num*result; num = num-1; return result;
Tail Recursion Αν η λύση που σκεφτήκατε για ένα πρόβληµα χρησιµοποιεί tail recursion, τότε αντικαταστήστε τη µε επανάληψη - είναι πιο αποτελεσµατική Αν σας ζητούν να λύσετε ένα πρόβληµα αναδροµικά, τότε ΜΗΝ το λύσετε µε tail recursion. Δε θεωρείται αναδροµική µια τέτοια λύση.