Μετάβαση στο περιεχόμενο
Topos Uranos

Περίληψη

Μια συνάρτηση μπορεί να καλεί τον εαυτό της: η αναδρομή λύνει ένα πρόβλημα ανάγοντάς το σε ένα μικρότερο στιγμιότυπο του ίδιου προβλήματος. Αυτό το μάθημα μεταφέρει στη C++ αναδρομικούς ορισμούς των μαθηματικών (το παραγοντικό, τη δύναμη, το άθροισμα των ψηφίων), τον καθένα με τη βασική και την αναδρομική του περίπτωση· δείχνει ότι η επαγωγή εγγυάται την ορθότητά τους και ότι ένα μέτρο που φθίνει εγγυάται τον τερματισμό τους· παρακολουθεί στη στοίβα κλήσεων τα πλαίσια που στοιβάζονται και αναιρούνται, και εξηγεί πότε η στοίβα εξαντλείται. Εξετάζει το όριο του τύπου του αποτελέσματος, που υπερχειλίζει πολύ πριν από τη στοίβα· συγκρίνει την αναδρομική και την επαναληπτική εκδοχή του Φιμπονάτσι μετρώντας κλήσεις, και κλείνει με ένα κριτήριο για την επιλογή ανάμεσα στην αναδρομή και σε έναν βρόχο.

Μαθησιακοί στόχοι

  1. Να γράφετε μια αναδρομική συνάρτηση, με τη βασική και την αναδρομική της περίπτωση, ξεκινώντας από έναν αναδρομικό ορισμό.
  2. Να αιτιολογείτε την ορθότητα και τον τερματισμό μιας αναδρομικής συνάρτησης με επαγωγή στο μέγεθος του προβλήματος.
  3. Να παρακολουθείτε στη στοίβα κλήσεων την εκτέλεση μιας αναδρομικής συνάρτησης και να εξηγείτε την υπερχείλιση της στοίβας.
  4. Να συγκρίνετε το κόστος της αναδρομικής και της επαναληπτικής εκδοχής του Φιμπονάτσι μετρώντας τις κλήσεις και τις πράξεις τους.

Αναδρομικοί ορισμοί

Το παραγοντικό

Το παραγοντικό ενός φυσικού nn είναι το γινόμενο των φυσικών από το 1 έως το nn: n!=1⋅2⋯nn! = 1 \cdot 2 \cdots n, με τη σύμβαση 0!=10! = 1. Οι τελείες αυτού του τύπου κρύβουν έναν βρόχο, και το μάθημα Βρόχοι δίδαξε πώς γράφεται. Ωστόσο, ο ίδιος ορισμός δέχεται και μια άλλη μορφή, που δεν αναφέρει κανέναν βρόχο: το παραγοντικό του nn είναι nn φορές το παραγοντικό του n−1n - 1, και το παραγοντικό του 0 είναι 1.

0!=1,n!=n⋅(n−1)!(n≥1).0! = 1, \qquad n! = n \cdot (n - 1)! \quad (n \geq 1).

Αυτός είναι ένας αναδρομικός ορισμός: ορίζει το παραγοντικό του nn μέσω του παραγοντικού ενός μικρότερου αριθμού. Δεν είναι κυκλικός, γιατί κάθε εφαρμογή του δεύτερου κανόνα κατεβαίνει ένα σκαλί και η σκάλα τελειώνει στον πρώτο: 3!=3⋅2!=3⋅2⋅1!=3⋅2⋅1⋅0!=63! = 3 \cdot 2! = 3 \cdot 2 \cdot 1! = 3 \cdot 2 \cdot 1 \cdot 0! = 6. Οι δύο κανόνες έχουν όνομα. Ο πρώτος είναι η βασική περίπτωση, που δίνει το αποτέλεσμα χωρίς να αναφέρεται σε τίποτε άλλο· ο δεύτερος είναι η αναδρομική περίπτωση, που ανάγει το πρόβλημα σε ένα μικρότερο στιγμιότυπο του ίδιου προβλήματος.

Η C++ επιτρέπει σε μια συνάρτηση να καλεί τον εαυτό της, και έτσι ο ορισμός μεταφέρεται σχεδόν λέξη προς λέξη.

ΠαράδειγμαΤο αναδρομικό παραγοντικό
Πρόγραμμα σε C++
#include <print>​// Παραγοντικό του n.// Προσυνθήκη: 0 <= n <= 12 (το φράγμα αιτιολογείται παρακάτω).// Μετασυνθήκη: επιστρέφει n! = 1 · 2 · ... · n, με 0! = 1.int factorial(int n){    if (n == 0) {        return 1;                   // βασική περίπτωση    }    return n * factorial(n - 1);    // αναδρομική περίπτωση}​int main(){    for (int n{0}; n <= 6; ++n) {        std::println("{}! = {}", n, factorial(n));    }}
Έξοδος
0! = 11! = 12! = 23! = 64! = 245! = 1206! = 720

Η συνάρτηση έχει ακριβώς τη μορφή του ορισμού: ένα if που χειρίζεται τη βασική περίπτωση και ένα return που εφαρμόζει την αναδρομική. Η κλήση factorial(n - 1) δεν διαφέρει από οποιαδήποτε άλλη κλήση του μαθήματος Συναρτήσεις: υπολογίζεται το όρισμά της, δημιουργείται ένα νέο πλαίσιο με το δικό του n και, όταν τελειώσει, η επιστρεφόμενη τιμή πολλαπλασιάζεται με το n εκείνου που κάλεσε.

Η σύμβαση δεν είναι διακοσμητική: με n=−1n = -1 η αναδρομική περίπτωση θα καλούσε με −2-2, έπειτα με −3-3, και δεν θα έφτανε ποτέ στο 0· και το 13!13! δεν χωρά σε έναν int. Πολλά εισαγωγικά κείμενα γράφουν αυτό το παραγοντικό με τη βασική περίπτωση if (n == 0 || n == 1), περιττή αλλά αβλαβή, και χωρίς να λένε τίποτα για τους αρνητικούς ούτε για το μέγεθος του αποτελέσματος: ο κώδικάς τους δεν τερματίζει με n<0n < 0 και δίνει ένα αποτέλεσμα χωρίς νόημα από το 13!13! και πέρα. Και τα δύο ελαττώματα θα εξεταστούν στην παράγραφό τους.

Βασική περίπτωση και αναδρομική περίπτωση

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

Η δύναμη xnx^n, με nn φυσικό, ορίζεται από τις x0=1x^0 = 1 και xn=x⋅xn−1x^n = x \cdot x^{n - 1}: το μέγεθος είναι ο εκθέτης. Το άθροισμα των ψηφίων ενός φυσικού nn (του 2026 είναι 2+0+2+6=102 + 0 + 2 + 6 = 10) ορίζεται ως εξής: αν n<10n < 10, το άθροισμα είναι nn, γιατί έχει ένα μόνο ψηφίο· αλλιώς, είναι το άθροισμα των ψηφίων του ⌊n/10⌋\lfloor n / 10 \rfloor, που είναι ο nn χωρίς το τελευταίο του ψηφίο, συν αυτό το τελευταίο ψηφίο, n mod 10n \bmod 10. Εδώ το μέγεθος είναι το πλήθος των ψηφίων.

ΠαράδειγμαΗ δύναμη και το άθροισμα των ψηφίων
Πρόγραμμα σε C++
#include <print>​// x υψωμένο στη n.// Προσυνθήκη: n >= 0.// Μετασυνθήκη: επιστρέφει x^n, με x^0 = 1.double power(double x, int n){    if (n == 0) {        return 1.0;    }    return x * power(x, n - 1);}​// Άθροισμα των δεκαδικών ψηφίων του n.// Προσυνθήκη: n >= 0.// Μετασυνθήκη: επιστρέφει το άθροισμα των ψηφίων του n.int digitSum(int n){    if (n < 10) {        return n;    }    return digitSum(n / 10) + n % 10;}​int main(){    std::println("2^10 = {}", power(2.0, 10));    std::println("1.5^3 = {}", power(1.5, 3));    std::println("άθροισμα των ψηφίων του 2026: {}", digitSum(2026));}
Έξοδος
2^10 = 10241.5^3 = 3.375άθροισμα των ψηφίων του 2026: 10

Στη digitSum, η ακέραια διαίρεση n / 10 αφαιρεί το τελευταίο ψηφίο και η n % 10 το εξάγει, όπως στην αλλαγή βάσης του μαθήματος Αριθμητικοί αλγόριθμοι. Για n=2026n = 2026, η συνάρτηση ζητά το άθροισμα των ψηφίων του 202, που ζητά εκείνο του 20, που ζητά εκείνο του 2· αυτό είναι βασική περίπτωση και επιστρέφει 2, και τα αποτελέσματα συντίθενται κατά την επιστροφή: 2+0=22 + 0 = 2, 2+2=42 + 2 = 4, 4+6=104 + 6 = 10.

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