Αναδρομή (Recursion) Πώς να λύσουμε ένα πρόβλημα κάνοντας λίγη δουλειά και ανάγοντας το υπόλοιπο να λυθεί με τον ίδιο τρόπο. Πού χρειάζεται; Πολλές μαθηματικές συναρτήσεις ορίζονται αναδρομικά. Δεν είναι προφανές πώς μπορούν να οριστούν αλλιώς. Πολλά προβλήματα λύνονται εύκολα αναδρομικά και δύσκολα μη αναδρομικά
Τρίγωνο Sierpinski Μη αναδρομικός ορισμός;
Δύο παρεξηγήσεις «Είναι δύσκολο να κατανοηθεί» Όχι, απλά απαιτείται εξάσκηση «Δεν είναι αποδοτική (χάσιμο χρόνου, χώρου)» Κριτήριο η ευκολία για τον άνθρωπο, όχι την μηχανή. Η αποδοτικότητα εξαρτάται από την επιλογή αλγορίθμου (καλή και κακή χρήση αναδρομικότητας). Θα δούμε δύο παραδείγματα.
Σκέψου Αναδρομικά Χωρίζουμε τη λύση σε τρία μέρη i. Κάνε κάποια δουλειά προς την λύση ii. Χρησιμοποίησε την μέθοδο για ένα (ή περισσότερα) μικρότερα υπο-προβλήματα σε ένα (ή περισσότερα) υποσύνολο δεδομένων iii. Ένωσε το i) και ii) Πρέπει να περιλαμβάνει μια τετριμμένη περίπτωση για το i), που να μην απαιτείται η ii) ώστε να σταματάει η αναδρομικότητα
Παράδειγμα: Παραγοντικό Αναδρομικός Ορισμός του n!, n>=0 0! = 1 n! = n * (n-1)! Αναδρομικός Ορισμός Υποπρογράμματος «Λίγη δουλειά»: ένας πολλαπλασιασμός και μια αφαίρεση Βήμα Διακοπής (stopping case) το 0! -«Υποπρόβλημα»: Αναδρομικό Βήμα (recursive step) (n-1)! -Ενώνουμε τα δύο
Αναδρομικός Υπολογισμός του 3! 3! = 3 * 2! 2! = 2 * 1! 1! = 1* 0! 0! = 1 Χρησιμοποιώντας την τιμή 1 του 0! (βήμα διακοπής) είναι δυνατός ο υπολογισμός του 3! επιστρέφοντας στον υπολογισμό του 1!, του 2! και τέλος του 3! όπως φαίνεται στο παρακάτω σχήμα 3! = 3 * 2! = 3 * 2 = 6 2! = 2 * 1! = 2 * 1 = 2 1! = 1 * 0! = 1 * 1 = 1 0! = 1 Βήμα Διακοπής
Ο αναδρομικός αυτός ορισμός μπορεί να υλοποιηθεί εύκολα όπως φαίνεται στο παρακάτω υποπρόγραμμα: long par(int n) { long timi; if (n == 0) /* βήμα διακοπής */ timi = 1; else /* αναδρομικό βήμα n! = n * (n-1)!*/ timi = n * par(n - 1); // επιστροφή (Α) return (timi); }
paragontiko = par(3); 6 To n είναι 3 timi = 3* par(2) επιστροφή Αποτέλεσμα 2 To n είναι 2 timi = 2* par(1) επιστροφή Αποτέλεσμα 1 Παρουσιάζεται ο τρόπος λειτουργίας του αναδρομικού υποπρογράμματος, όπου κάθε πλαίσιο σχετίζεται με την εγγραφή ενεργοποίησης (activation record). To n είναι 1 timi = 1* par(0) επιστροφή Αποτέλεσμα 1 To n είναι 0 timi = 1 επιστροφή
Υλοποίηση Αναδρομής { /*κύριο πρόγραμμα*/ x = par(3); /* program address B */ } παράμετροι 3 B Διεύθυνση επιστροφής συνάρτησης 1 η κλήση της par(3) στο main (B) 2 Α 3 B 2 η κλήση της par(2) στην par (A)
3 η και 4 η κλήσεις της par(1) και par(0) στην par (A) 0 Α 1 Α 2 Α 3 B Τερματισμός 4 ης κλήσης της par(0) στην par (A) Επιστροφή τιμής 0!==1 Επιστροφή ροής προγράμματος στην διεύθυνση Α 1 Α 2 Α 3 B 1 Α
Τερματισμός 3 ης κλήσης της par(1) στην par (A) Επιστροφή τιμής 1!==1*0!==1 Επιστροφή ροής προγράμματος στην διεύθυνση Α 2 Α 3 B 1 Α Τερματισμός 2 ης κλήσης της par(2) στην par (A) Επιστροφή τιμής 1!==2*1!==2 Επιστροφή ροής προγράμματος στην διεύθυνση Α 3 Β 2 Α 6 Β Τερματισμός 1 ης κλήσης της par(3) στην main (B) Επιστροφή τιμής 3!==3*2!=6 Επιστροφή ροής προγράμματος στην διεύθυνση B
Οπτικοποίηση Χρόνου - Χώρου Συναρτήσεων Χρόνος Κλήσεων, Διάρκεια Εκτέλεσης και Απαιτούμενη μνήμη) Space E B Proc A C Main Proc D Proc D Proc D Proc D Time
Πολυπλοκότητα Ο(?) Παραγοντικού Αναδρομή-Επανάληψη Χρήση αναδρομικών συναρτήσεων Α. Με αναδρομή timi = n * par (n - 1) : Δυο κύριες πράξεις *, - T Α (n) = 2 + T(n - 1) = 2 + 2 + T(n - 2) = 2 + 2 +... + 2 + T(0) = 2*n+1 T Α (n) = O(n) Β. Με επανάληψη n! = 1*1 * 2 * 3 *... * n, μια κύρια πράξη * T Ε (n) = 1+1+...+1=(n+1)*1=n+1 T Ε (n) = O(n) T Α (n) = T Ε (n) = O(n)
Αριθμοί Fibonacci 0 1 1 2 3 5 8 13 21...Fibn = Fibn-1 + Fibn-2 Αναδρομική Υλοποίηση long Fib (long n) { long Fibnum; if (n <= 0) Fibnum = 0; else if (n == 1) Fibnum = 1; else Fibnum = Fib (n - 1) + Fib (n - 2); } return (Fibnum);
Πολυπλοκότητα Αναδρομικής Υλοποίησης Fibnum = Fib (n - 1) + Fib (n - 2); 3 βασικές πράξεις -, +, - Τ(n) = 3 + T(n - 1) + T(n - 2) = 3 + [3 + T(n - 2) + T(n - 3)] + T(n - 2) = 6 + 2Τ(n-2) + Τ(n - 3), για n>=3 T(n) >= 2T(n - 2) >= 2 2 T(n - 4) >=... >= 2 n/2 T(0), αν n άρτιο ή T(n) >= 2T(n - 2) >= 2 2 T(n - 4) >=... >= 2 (n-1)/2 T(1), αν n περιττό Αλλά Τ(0) = Τ(1) = 1, συνεπώς T Α (n) = O(2 n/2 ) για όλα τα n >=2 Αναδρομικές Συναρτήσεις Αναλυτικές Λύσεις (μάθημα Πολυπλοκότητα)
long efib (long n) /* Επαναληπτικό υποπρόγραμμα για τον υπολογισμό του n-ιοστού αριθμού Fibonacci*/ { long Fib1, Fib2, Fib3, i; } Fib1 = 0; Fib2 = 1; for (i = 3;i<=n;i++) { Fib3 = Fib1 + Fib2; Fib1 = Fib2; Fib2 = Fib3; } return(fib2); T E =1+1+1+ +1 = 1*(n+1)= n+ 1= O(n)
The Ackermann function Η συνάρτηση Ackermann ορίζεται αναδρομικά για μη-αρνητικούς ακέραιους m, n ως εξής
Η υλοποίηση απλή long ack(int m, int n) { if (m == 0) return (n+1) ; else if (n = =0) return ack(m-1, 1) ; else return ack(m-1, ack(m, n-1)) ; }
... Αλλά η συμπεριφορά της A(1, 2) = A(0, A(1,1)) = A(0, A(0, A(1,0))) = A(0, A(0, A(0,1))) = A(0, A(0, 2)) = A(0, 3) = 4
A(4, 3) = A(3, A(4, 2)) = A(3, A(3, A(4, 1))) = A(3, A(3, A(3, A(4, 0)))) = A(3, A(3, A(3, A(3, 1)))) = A(3, A(3, A(3, A(2, A(3, 0))))) = A(3, A(3, A(3, A(2, A(2, 1))))) = A(3, A(3, A(3, A(2, A(1, A(2, 0)))))) = A(3, A(3, A(3, A(2, A(1, A(1, 1)))))) = A(3, A(3, A(3, A(2, A(1, A(0, A(1, 0))))))) = A(3, A(3, A(3, A(2, A(1, A(0, A(0, 1))))))) = A(3, A(3, A(3, A(2, A(1, A(0, 2)))))) = A(3, A(3, A(3, A(2, A(1, 3))))) = A(3, A(3, A(3, A(2, A(0, A(1, 2)))))) = A(3, A(3, A(3, A(2, A(0, A(0, A(1, 1))))))) = A(3, A(3, A(3, A(2, A(0, A(0, A(0, A(1, 0)))))))) = A(3, A(3, A(3, A(2, A(0, A(0, A(0, A(0, 1)))))))) = A(3, A(3, A(3, A(2, A(0, A(0, A(0, 2)))))) = A(3, A(3, A(3, A(2, A(0, A(0, 3))))) = A(3, A(3, A(3, A(2, A(0, 4))))) = A(3, A(3, A(3, A(2, 5)))) =... = A(3, A(3, A(3, 13))) =... = A(3, A(3, 65533)) =... A(3, 65533) επιστρέφει 2 65536 3, αριθμός μεγαλύτερος από τον αριθμό των ατόμων σε όλο τον ορατό κόσμο. Κατόπιν, αυτός ο αριθμός χρησιμοποιείται ως δύναμη του 2 για το τελικό αποτέλεσμα.
Πιθανές Παγίδες Λάθος συνθήκη τερματισμού (do while) Ατέρμων Βρόχος Συνεχής Αναδρομή Δεν γίνεται Έλεγχος τερματισμού Χρησιμοποιούμε αναδρομή με μη αποδεκτές παραμέτρους
Μη τερματίζουσα Αναδρομή Δεν ικανοποιείται η συνθήκη τερματισμού Κλήση par(-1) ενώ ελέγχουμε (n==0) Συμπτώματα: συνεχείς κλήσεις έως ότου εξαντληθεί η μνήμη Δεν αλλάζουν οι παράμετροι της αναδρομής, N=f(N). Δεν υπάρχει πρόοδος.
Αφαίρεση της Γραμμικής Αναδρομής (αναδρομή με μια μόνο αναδρομική κλήση) Με χρήση Στοίβας void grammiki_anadromi (long n) { if (συνθήκη (n)) μη αναδρομική περίπτωση (n); else { προηγούμενες πράξεις (n); grammiki_anadromi (F(n)); μετέπειτα πράξεις (n); } }
void epanaliptiki (long n) { typos_stoiva stoiva; dimiourgia(stoiva); while (!συνθήκη (n)) { προηγούμενες πράξεις (n); othisi (stoiva, n); n = F(n); } } μη αναδρομική περίπτωση (n); while (!keni(stoiva)) { exagogi(stoiva, &n); μετέπειτα πράξεις (n); }