Zusammenfassung
Eine Funktion kann sich selbst aufrufen: Die Rekursion löst ein Problem, indem sie es auf eine kleinere Instanz desselben Problems zurückführt. Diese Lektion überträgt rekursive Definitionen der Mathematik nach C++ (die Fakultät, die Potenz, die Ziffernsumme), jede mit ihrem Basisfall und ihrem Rekursionsfall; sie zeigt, dass die Induktion ihre Korrektheit und ein abnehmendes Maß ihre Terminierung garantiert; sie verfolgt mit dem Aufrufstapel die Rahmen, die sich aufstapeln und wieder abbauen, und erklärt, wann der Stapel erschöpft ist. Sie untersucht die Grenze des Ergebnistyps, der lange vor dem Stapel überläuft; sie vergleicht die rekursive und die iterative Fassung von Fibonacci, indem sie Aufrufe zählt, und schließt mit einem Kriterium für die Wahl zwischen Rekursion und Schleife.
Lernziele
- Eine rekursive Funktion mit ihrem Basisfall und ihrem Rekursionsfall aus einer rekursiven Definition schreiben.
- Die Korrektheit und die Terminierung einer rekursiven Funktion durch Induktion über die Größe des Problems begründen.
- Die Ausführung einer rekursiven Funktion mit dem Aufrufstapel verfolgen und den Stapelüberlauf erklären.
- Die Kosten der rekursiven und der iterativen Fassung von Fibonacci vergleichen, indem man ihre Aufrufe und ihre Operationen zählt.
Rekursive Definitionen
Die Fakultät
Die Fakultät einer natürlichen Zahl ist das Produkt der natürlichen Zahlen von 1 bis : , mit der Konvention . Die Auslassungspunkte dieser Formel verbergen eine Schleife, und die Lektion Schleifen hat gezeigt, wie man sie schreibt. Nun lässt dieselbe Definition eine andere Form zu, die keine Schleife erwähnt: Die Fakultät von ist mal die Fakultät von , und die von 0 ist 1.
Das ist eine rekursive Definition: Sie definiert die Fakultät von mithilfe der Fakultät einer kleineren Zahl. Sie ist nicht zirkulär, denn jede Anwendung der zweiten Regel steigt eine Stufe hinab, und die Treppe endet bei der ersten: . Die beiden Regeln haben Namen. Die erste ist der Basisfall, der das Ergebnis liefert, ohne sich auf etwas anderes zu beziehen; die zweite ist der Rekursionsfall, der das Problem auf eine kleinere Instanz desselben Problems zurückführt.
C++ erlaubt einer Funktion, sich selbst aufzurufen, und damit lässt sich die Definition fast Wort für Wort übertragen.
#include <print>// Fakultät von n.// Vorbedingung: 0 <= n <= 12 (die Schranke wird später begründet).// Nachbedingung: gibt n! = 1 · 2 · ... · n zurück, mit 0! = 1.int factorial(int n){ if (n == 0) { return 1; // Basisfall } return n * factorial(n - 1); // Rekursionsfall}int main(){ for (int n{0}; n <= 6; ++n) { std::println("{}! = {}", n, factorial(n)); }}0! = 11! = 12! = 23! = 64! = 245! = 1206! = 720Die Funktion hat genau die Form der Definition: ein if, das den Basisfall behandelt, und ein return, das den Rekursionsfall anwendet. Der Aufruf factorial(n - 1) unterscheidet sich nicht von irgendeinem anderen Aufruf der Lektion Funktionen: Sein Argument wird ausgewertet, ein neuer Rahmen mit einem eigenen n wird angelegt, und am Ende wird der zurückgegebene Wert mit dem n des Aufrufers multipliziert.
Der Vertrag ist keine Zierde: Mit würde der Rekursionsfall mit aufrufen, danach mit , und nie die 0 erreichen; und passt nicht in einen int. Viele einführende Texte schreiben diese Fakultät mit dem Basisfall if (n == 0 || n == 1), redundant, aber harmlos, und ohne etwas über negative Zahlen oder die Größe des Ergebnisses zu sagen: Ihr Code terminiert nicht bei und liefert ein sinnloses Ergebnis ab . Beide Mängel werden weiter unten an ihrer Stelle untersucht.
Basisfall und Rekursionsfall
Jede rekursive Funktion hat einen oder mehrere Basisfälle, die direkt gelöst werden, und einen oder mehrere Rekursionsfälle, die dieselbe Funktion mit einem in einem genauen Sinn kleineren Argument aufrufen und das Zurückgegebene weiterverarbeiten.
Die Potenz , mit natürlichem , wird durch und definiert: Die Größe ist der Exponent. Die Ziffernsumme einer natürlichen Zahl (die von 2026 ist ) wird so definiert: Ist , so ist die Summe , denn die Zahl hat nur eine Ziffer; andernfalls ist sie die Ziffernsumme von , also von ohne seine letzte Ziffer, plus diese letzte Ziffer, . Hier ist die Größe die Anzahl der Ziffern.
#include <print>// x hoch n.// Vorbedingung: n >= 0.// Nachbedingung: gibt x^n zurück, mit x^0 = 1.double power(double x, int n){ if (n == 0) { return 1.0; } return x * power(x, n - 1);}// Summe der Dezimalziffern von n.// Vorbedingung: n >= 0.// Nachbedingung: gibt die Ziffernsumme von n zurück.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("Ziffernsumme von 2026: {}", digitSum(2026));}2^10 = 10241.5^3 = 3.375Ziffernsumme von 2026: 10In digitSum entfernt die ganzzahlige Division n / 10 die letzte Ziffer, und n % 10 entnimmt sie, wie beim Basiswechsel der Lektion Numerische Algorithmen. Für verlangt die Funktion die Ziffernsumme von 202, die die von 20 verlangt, die die von 2 verlangt; diese ist ein Basisfall und gibt 2 zurück, und die Ergebnisse setzen sich auf dem Rückweg zusammen: , , .
Eine rekursive Funktion zu schreiben heißt also, zwei Fragen zu beantworten: welche Fälle ohne Hilfe gelöst werden und wie man die Lösung eines Falls aus der eines kleineren gewinnt. Sind beide Antworten richtig, so ist es auch die Funktion, und das Folgende beweist es.
Inhalt wird geladen…
Abgeschlossen
Melde dich an, um deinen Fortschritt zu speichern