Εισαγωγή στον Προγ/μό Η/Υ Ενότητα 13: Αναδρομή Διδάσκων: Μιχάλης Τίτσιας
Αναδρομή Η αναδρομή είναι η διαδικασία επίλυσης ενός προβλήματος με το να το χωρίζουμε σε μικρότερα υπο-προβλήματα της ίδιας μορφής. Η πλάγιο-γραφημένη φράση είναι η ουσία της αναδρομής. Το γεγονός ότι η αναδρομική αποδόμηση δημιουργεί υπο-προβλήματα τα οποία έχουν την ίδια μορφή με το αρχικό πρόβλημα σημαίνει ότι τα αναδρομικά προγράμματα θα χρησιμοποιήσουν την ίδια μέθοδο για να λύσουν τα υπο-προβλήματα σε διαφορετικά επίπεδα της λύσης. Όσον αφορά τη δομή του κώδικα, το καθοριστικό χαρακτηριστικό της αναδρομής είναι οι μέθοδοι που καλούν τον εαυτό τους, άμεσα ή έμμεσα, όσο προχωράει η αποδόμηση.
Μία απλή απεικόνιση της Αναδρομής Υποθέστε ότι είσαστε ο εθνικός διευθυντής κεφαλαίου για μία φιλανθρωπική οργάνωση και υπάρχει η ανάγκη να συγκεντρώσετε $1.000.000. Μία πιθανή προσέγγιση είναι η εύρεση ενός επιφανούς φιλάνθρωπου και να του ζητήσετε να δωρίσει το ποσό του $1.000.000. Το πρόβλημα αυτής της στρατηγικής είναι ότι η εύρεση ξεχωριστών ανθρώπων με το συνδυασμό μέσων και διάθεσης να βοηθήσουν είναι δύσκολος να βρεθεί. Είναι πολύ πιο πιθανό να γίνουν πιο μικρές δωρεές των $100. Μία άλλη στρατηγική είναι να ζητήσουμε από 10.000 φίλους μας από $100. Δυστυχώς οι περισσότεροι από μας δεν έχουμε 10.000 φίλους. Παρόλα αυτά υπάρχουν υποσχόμενες στρατηγικές. Μπορείτε για παράδειγμα να βρείτε 10 περιφερειακούς συντονιστές, και ο καθένας τους να συγκεντρώσει από $100.000. Αυτοί με τη σειρά τους θα έβρισκαν 10 τοπικούς συντονιστές, με στόχο για τον καθένα τα $10.000. Οι τοπικοί διαχειριστές θα συνέχιζαν τη διαδικασία ώς κάποιο διαχειρίσιμο σημείο.
Μία απλή απεικόνιση της Αναδρομής Το ακόλουθο διάγραμμα απεικονίζει την αναδρομική στρατηγική συγκέντρωσης $1.000.000, που περιγράφηκε στη προηγούμενη διαφάνεια. Goal: $1,000,000 Goal: $100,000 Goal: $10,000 Goal: $1000 Goal: $100
Ψευδοκώδικας Στρατηγικής Συλλογής Κεφαλαίων Αν υλοποιούσαμε τη στρατηγική συλλογής κεφαλαίων σε κώδικα Java, θα έμοιαζε κάπως έτσι: private void collectContributions(int n) { if (n <= 100) { Collect the money from a single donor. } else { Find 10 volunteers. Get each volunteer to collect n/10 dollars. Combine the money raised by the volunteers. } Αυτό που κάνει αναδρομική τη συνάρτηση είναι η γραμμή: Get each volunteer to collect n/10 dollars. Και θα υλοποιηθεί από την ακόλουθη αναδρομική κλήση: collectContributions(n / 10);
Αναδρομικές Μέθοδοι n! = n x (n - 1) x (n - 2) x . . . x 3 x 2 x 1 1 Τα ευκολότερα παραδείγματα κατανόησης της αναδρομής είναι μέθοδοι οι οποίες είναι κατανοητές από τη δήλωσή τους. Για παράδειγμα, θεωρείστε το παραγοντικό που μπορεί να οριστεί με κάθε έναν από τους ακόλουθους τρόπους: n! = n x (n - 1) x (n - 2) x . . . x 3 x 2 x 1 1 if n is 0 n! = n x (n - 1)! otherwise Ο δεύτερος ορισμός οδηγεί κατευθείαν στον ακόλουθο κώδικα, του οποίου η εκτέλεση εξομοιώνεται στην επόμενη διαφάνεια: private int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); }
Εξομοιώνοντας τη factorial μέθοδο public void run() { int n = readInt("Enter n: "); println(n + "! = " + factorial(n) ); } private int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); } n 5 private int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); } n 4 120 n private int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); } n 3 5 private int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); } n 2 private int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); } n 1 24 private int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); } n private int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); } n 6 2 1 1 Factorial Enter n: 5 5! = 120 skip simulation
Το αναδρομικό “Leap of Faith” Ο σκοπός της ανασκόπησης ολόκληρης της αποδόμησης του υπολογισμού του factorial(5) είναι για να πειστείτε ότι η διαδικασία λειτουργεί και ότι οι αναδρομικές κλήσεις, δεν διαφέρουν πραγματικά από άλλες κλήσεις μεθόδων, ή τουλάχιστον στην εσωτερική τους λειτουργία. Ο κίνδυνος ερευνώντας αυτές τις λεπτομέρειες είναι ότι σας ενθαρρύνει να κάνετε το ίδιο όταν γράφετε τα δικά σας αναδρομικά προγράμματα. Όπως συμβαίνει, κοιτάζοντας τις λεπτομέρειες ενός αναδρομικού προγράμματος σχεδόν πάντα κάνει πιο δύσκολο το γράψιμο αυτών των προγραμμάτων. Η υλοποίηση αναδρομικών προγραμμάτων γίνεται «φύση» μας μόνο αφού αποκτήσουμε αρκετή εμπιστοσύνη στη διαδικασία και δεν χρειάζεται να τα ελέγχουμε σε κάθε βήμα εκτέλεσης τους. Καθώς γράφετε ένα αναδρομικό πρόγραμμα, είναι σημαντικό να πιστεύετε ότι κάθε αναδρομική κλήση θα επιστρέψει το σωστό αποτέλεσμα εφόσον οι παράμετροι προσδιορίζουν ένα απλούστερο υπο-πρόβλημα. Έχοντας αυτό ως δεδομένο-ακόμα και πριν την ολοκλήρωση του κώδικα-ονομάζεται αναδρομικό leap of faith.
Το Αναδρομικό Πρότυπο Οι περισσότερες αναδρομικές μέθοδοι που συναντούμε σε εισαγωγικό μάθημα έχουν τη μορφή: if (test for a simple case) { Compute and return the simple solution without using recursion. } else { Divide the problem into one or more subproblems that have the same form. Solve each of the problems by calling this method recursively. Return the solution from the results of the various subproblems. } Για να βρούμε την αναδρομική λύση είναι κυρίως θέμα να καταλάβουμε πως θα τη χωρίσουμε σε κομμάτια για να ταιριάζει στο πρότυπο. Όταν το κάνετε αυτό, πρέπει να ακολουθείτε τους 2 αυτούς κανόνες: Αναγνωρίζουμε απλές περιπτώσεις που μπορούν να λυθούν χωρίς αναδρομή. 1. Βρίσκουμε την αναδρομική αποδόμηση που χωρίζει τη κάθε περίπτωση του προβλήματος σε απλούστερα υπο-προβλήματα του ίδιου τύπου, τα οποία μπορούμε να επιλύσουμε εφαρμόζοντας την αναδρομική μέθοδο. 2.
Άσκηση: Η Αναδρομική gcd Μέθοδος Στη συζήτηση για αλγοριθμικές μεθόδους (ενότητα 6), ένα από τα πρωτεύοντα παραδείγματα είναι ο αλγόριθμος του Ευκλείδη για τον υπολογισμό του μέγιστου κοινού διαιρέτη 2 ακεραίων, x και y. Ο αλγόριθμος του Ευκλείδη υλοποιείται με τον παρακάτω κώδικα: public int gcd(int x, int y) { int r = x % y; while (r != 0) { x = y; y = r; r = x % y; } return y; public int gcd(int x, int y) { if (y == 0) { return x; } else { return gcd(y, x % y); } Ξαναγράψτε αυτή τη μέθοδο ώστε αντί για επανάληψη να χρησιμοποιεί αναδρομή, εκμεταλλευόμενη τη διορατικότητα του Ευκλείδη ότι ο μέγιστος κοινός διαιρέτης του x και y είναι και ο μέγιστος κοινός διαιρέτης του y και του υπολοίπου του x διαιρούμενου από το y. Ως συνήθως, το κλειδί για να λύσουμε αυτό το πρόβλημα βρίσκεται στο να αναγνωρίσουμε την αναδρομική αποσύνθεση και να ορίσουμε κατάλληλες απλές περιπτώσεις.
Δυαδική Αναζήτηση (υλοποίηση με εντολή επανάληψης) Δυαδική Αναζήτηση (υλοποίηση με εντολή επανάληψης) private int binarySearch(int key, int[] array) { int lh = 0; int rh = array.length - 1; while (lh <= rh) { int mid = (lh + rh) / 2; if (key == array[mid]) return mid; if (key < array[mid]) { rh = mid - 1; } else { lh = mid + 1; } return -1;
Δυαδική Αναζήτηση (υλοποίηση με αναδρομή) Δυαδική Αναζήτηση (υλοποίηση με αναδρομή) private int binarySearch(int key,int[] array,int lh,int rh) { if (lh > rh) return -1; int mid = (lh + rh) / 2; if (key == array[mid]) return mid; if (key < array[mid]) return binarySearch(key, array, lh, mid - 1); else return binarySearch(key, array, mid + 1, rh); }
Ο Αλγόριθμος Ταξινόμησης Επιλογής (με εντολή επανάληψης) private void sort(int[] array) { for (int lh = 0; lh < array.length; lh++) { int rh = findSmallest(array, lh, array.length); swapElements(array, lh, rh); } Η μέθοδος findSmallest(array, p1, p2) επιστρέφει την θέση της μικρότερης τιμής του πίνακα από την θέση p1 μέχρι και πριν το p2. Η μέθοδος swapElements(array, p1, p2) ανταλλάσει τα στοιχεία των θέσεων που δόθηκαν ως ορίσματα.
Ο Αλγόριθμος Ταξινόμησης Επιλογής (με αναδρομή) private void sort(int[] array, int lh) { if (lh == array.length - 1) { return; } else { int rh = findSmallest(array, lh, array.length); swapElements(array, lh, rh); sort(int[] array, lh + 1);
Γραφική Αναδρομή Η αναδρομή εμφανίζεται σε μερικές γραφικές εφαρμογές, κυρίως στη δημιουργία fractals (μορφοκλασμάτων), τα οποία είναι μαθηματικές κατασκευές που αποτελούνται από παρεμφερή σχήματα επαναλαμβανόμενα σε διάφορες κλίμακες. Τα Fractals έγιναν πολύ δημοφιλή μέσω του βιβλίου The Fractal Geometry of Nature, τουBenoit Mandelbrot, το 1982 entitled. Ένα από τα απλούστερα fractal μοτίβα είναι το Koch fractal, ονομαζόμενο από τον εφευρέτη του, τον Σουηδό μαθηματικό Helge von Koch (1870-1924). Το Koch fractal αποκαλείται συνήθως fractal χιονονιφάδα εξαιτίας των όμορφων, εξάπλευρων συμμετριών που απεικονίζει όσο το σχέδιο γίνεται πιο λεπτομερές. Όπως απεικονίζεται στο επόμενο διάγραμμα:
Σχεδιάζοντας Koch Fractals order 1 Από την αρχική θέση (το οποίο ονομάζεται fractal of τάξης 0), κάθε ανώτερη fractal τάξη δημιουργείται αντικαθιστώντας κάθε τμήμα γραμμής του σχεδίου με 4 τμήματα που συνδέουν τα ίδια άκρα αλλά περιέχουν και μία ισόπλευρη σφήνα στο μέσο. order 2 Το σχήμα της προηγούμενης διαφάνειας είναι Koch fractal τάξεως 4. order 0
Εξομοιώνοντας το Snowflake Πρόγραμμα public void run() { SnowflakeFractal fractal = new SnowflakeFractal(100, 2) ; fractal.setFilled(true); fractal.setFillColor(Color.MAGENTA); add(fractal, getWidth() / 2, getHeight() / 2); } public SnowflakeFractal(double edge, int order) { addVertex(-edge / 2, -edge / (2 * Math.sqrt(3))); addFractalLine(edge, 0, order); addFractalLine(edge, -120, order); addFractalLine(edge, +120, order); this edge order 100.0 2 private void addFractalLine(double r, int theta, int order) { if (order == 0) { addPolarEdge(r, theta); } else { addFractalLine(r / 3, theta, order - 1); addFractalLine(r / 3, theta + 60, order - 1); addFractalLine(r / 3, theta - 60, order - 1); public void run() { SnowflakeFractal fractal = new SnowflakeFractal(100, 2) ; fractal.setFilled(true); fractal.setFillColor(Color.MAGENTA); add(fractal, getWidth() / 2, getHeight() / 2); } public SnowflakeFractal(double edge, int order) { addVertex(-edge / 2, -edge / (2 * Math.sqrt(3))); addFractalLine(edge, 0, order); addFractalLine(edge, -120, order); addFractalLine(edge, +120, order); } this edge order 100.0 2 private void addFractalLine(double r, int theta, int order) { if (order == 0) { addPolarEdge(r, theta); } else { addFractalLine(r / 3, theta, order - 1); addFractalLine(r / 3, theta + 60, order - 1); addFractalLine(r / 3, theta - 60, order - 1); } this theta order 2 r 100.0 private void addFractalLine(double r, int theta, int order) { if (order == 0) { addPolarEdge(r, theta); } else { addFractalLine(r / 3, theta, order - 1); addFractalLine(r / 3, theta + 60, order - 1); addFractalLine(r / 3, theta - 60, order - 1); } this theta order 1 r 33.33 fractal private void addFractalLine(double r, int theta, int order) { if (order == 0) { addPolarEdge(r, theta); } else { addFractalLine(r / 3, theta, order - 1); addFractalLine(r / 3, theta + 60, order - 1); addFractalLine(r / 3, theta - 60, order - 1); } this theta order r 11.11 private void addFractalLine(double r, int theta, int order) { if (order == 0) { addPolarEdge(r, theta); } else { addFractalLine(r / 3, theta, order - 1); addFractalLine(r / 3, theta + 60, order - 1); addFractalLine(r / 3, theta - 60, order - 1); } this theta order –60 r 11.11 private void addFractalLine(double r, int theta, int order) { if (order == 0) { addPolarEdge(r, theta); } else { addFractalLine(r / 3, theta, order - 1); addFractalLine(r / 3, theta + 60, order - 1); addFractalLine(r / 3, theta - 60, order - 1); } this theta order r 11.11 private void addFractalLine(double r, int theta, int order) { if (order == 0) { addPolarEdge(r, theta); } else { addFractalLine(r / 3, theta, order - 1); addFractalLine(r / 3, theta + 60, order - 1); addFractalLine(r / 3, theta - 60, order - 1); } this theta order 60 r 11.11 Snowflake skip simulation
Διάβασμα για το σπίτι Ενότητα 14.1 από «Η Τέχνη και Επιστήμη της JAVA: Μια εισαγωγή στην Επιστήμη των Υπολογιστών», E. Roberts