Funkcja rekurencyjna wywołuje samą siebie z mniejszym problemem, aż dojdzie do przypadku, który rozwiązuje bez dalszych wywołań (przypadek bazowy). Na lekcji uczniowie oglądają ślad wywołań silni, liczą, ile razy wywoła się naiwny Fibonacci, i sprawdzają, że wieże Hanoi dla 10 dysków wymagają 1023 ruchów.
Scenariusz jest przeznaczony na 45 minut dla klasy po lekcji o liczbach pierwszych w C++ i funkcjach. Ten sam temat w Pythonie omawia lekcja o rekurencji.
Przygotowanie stanowisk
Potrzebny jest kompilator C++ (na przykład g++ z MinGW-w64, Code::Blocks lub Dev-C++) i edytor tekstu. Rozpakuj pakiet z sekcji materiałów do osobnego folderu. W folderze projektu skompiluj program poleceniem g++ -std=c++17 -Wall start.cpp -o program, a potem uruchom go: ./program (Linux, macOS) albo program.exe (Windows). W środowisku z przyciskiem Build and Run (Code::Blocks) wystarczy otworzyć plik .cpp i je użyć. Rozwiązanie jest w pliku rozwiazanie.cpp, zadanie dla chętnych — w rozszerzenie.cpp.
Cele i plan lekcji
Po zajęciach uczeń potrafi:
- wskazać przypadek bazowy i krok rekurencyjny;
- odczytać ślad wywołań funkcji rekurencyjnej;
- policzyć liczbę wywołań naiwnego Fibonacciego;
- zapisać rekurencyjnie sumę cyfr i potęgę;
- wyjaśnić działanie wież Hanoi i wzór
2^n − 1.
| Czas | Praca na lekcji |
|---|---|
| 0–5 min | Problem: matrioszki i lustra |
| 5–15 min | Przypadek bazowy, krok rekurencyjny, ślad |
| 15–22 min | Fibonacci i liczba wywołań |
| 22–37 min | Trzy zadania samodzielne |
| 37–42 min | Omówienie wyników |
| 42–45 min | Pytania kontrolne |
Przypadek bazowy i krok
W silnia(n) przypadkiem bazowym jest n <= 1 (wynik 1), a krokiem — n * silnia(n - 1). Bez przypadku bazowego funkcja wywoływałaby się w nieskończoność, aż skończy się pamięć stosu. Ślad wywołań pokazuje, że funkcje „schodzą w dół” do silnia(1), a potem zwracają wyniki „w górę”.
Fibonacci i koszt
fib(n) = fib(n−1) + fib(n−2) jest elegancki, ale wołany naiwnie liczy te same wartości wielokrotnie. Dla fib(10) funkcja wywołuje się aż 177 razy, chociaż wynik to tylko 55. Zliczanie wywołań licznikiem globalnym pokazuje, jak szybko rośnie koszt.
Wieże Hanoi
Aby przenieść n dysków z A na C: przenieś n−1 dysków na B, przenieś największy na C, przenieś n−1 dysków z B na C. Liczba ruchów to 2^n − 1: 7 dla 3 dysków, 1023 dla 10.
Zadania
Zadanie 1. Ślad silni. Napisz silniaZeSladem(n, poziom), która wypisuje wcięciem kolejne wywołania i wyniki, i wywołaj ją dla 4.
Zadanie 2. Trzy funkcje. Napisz fib, sumaCyfr i potega rekurencyjnie. Wypisz fib(10) wraz z liczbą wywołań, sumę cyfr liczby 12345 i 2^10.
Zadanie 3. Hanoi. Wypisz ruchy dla 3 dysków i policz ruchy dla 3 oraz 10 dysków.
Dla chętnych: napisz rekurencyjną funkcję nwd(a, b) (plik rozszerzenie.cpp).
Rozwiązanie i wyniki
#include <iostream>
#include <string>
using namespace std;
int wywolania = 0;
int silniaZeSladem(int n, int poziom) {
cout << string(poziom * 2, ' ') << "silnia(" << n << ")" << endl;
if (n <= 1) {
return 1;
}
int wynik = n * silniaZeSladem(n - 1, poziom + 1);
cout << string(poziom * 2, ' ') << "wynik " << wynik << endl;
return wynik;
}
int fib(int n) {
wywolania++;
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
int sumaCyfr(int n) {
if (n < 10) {
return n;
}
return n % 10 + sumaCyfr(n / 10);
}
int potega(int podstawa, int wykladnik) {
if (wykladnik == 0) {
return 1;
}
return podstawa * potega(podstawa, wykladnik - 1);
}
int liczbaRuchow = 0;
void hanoi(int n, char z, char na, char pomocniczy, bool pokaz) {
if (n == 0) {
return;
}
hanoi(n - 1, z, pomocniczy, na, pokaz);
liczbaRuchow++;
if (pokaz) {
cout << "dysk " << n << ": " << z << " -> " << na << endl;
}
hanoi(n - 1, pomocniczy, na, z, pokaz);
}
int main() {
// Zadanie 1: silnia ze sladem wywolan
int w = silniaZeSladem(4, 0);
cout << "wynik: " << w << endl;
// Zadanie 2: Fibonacci, suma cyfr, potega
cout << "fib(10) = " << fib(10) << ", wywolan: " << wywolania << endl;
cout << "suma cyfr 12345 = " << sumaCyfr(12345) << ", 2^10 = " << potega(2, 10) << endl;
// Zadanie 3: wieze Hanoi
hanoi(3, 'A', 'C', 'B', true);
cout << "ruchow dla 3 dyskow: " << liczbaRuchow << endl;
liczbaRuchow = 0;
hanoi(10, 'A', 'C', 'B', false);
cout << "ruchow dla 10 dyskow: " << liczbaRuchow << endl;
return 0;
}
Program wypisze:
silnia(4)
silnia(3)
silnia(2)
silnia(1)
wynik 2
wynik 6
wynik 24
wynik: 24
fib(10) = 55, wywolan: 177
suma cyfr 12345 = 15, 2^10 = 1024
dysk 1: A -> C
dysk 2: A -> B
dysk 1: C -> B
dysk 3: A -> C
dysk 1: B -> A
dysk 2: B -> C
dysk 1: A -> C
ruchow dla 3 dyskow: 7
ruchow dla 10 dyskow: 1023
Zadanie dla chętnych (rozszerzenie.cpp):
#include <iostream>
using namespace std;
int nwd(int a, int b) {
if (b == 0) {
return a;
}
return nwd(b, a % b);
}
int main() {
cout << nwd(48, 18) << " " << nwd(17, 5) << endl;
return 0;
}
Wynik:
6 1
Programy kompilowano kompilatorem g++ 15.2 z opcjami -std=c++17 -Wall -Wextra, bez ostrzeżeń.
Typowe błędy
| Objaw | Co sprawdzić |
|---|---|
| Segmentation fault lub „Stack overflow” | Brakuje przypadku bazowego albo wywołanie nie zbliża się do niego. |
| Zły wynik silni dla 0 | Przypadek bazowy n <= 1 zwraca 1 (także dla 0). |
fib(40) trwa bardzo długo | Naiwna rekurencja liczy te same wartości wielokrotnie. |
| Licznik wywołań nie zeruje się | Zeruj zmienną globalną przed nowym pomiarem. |
| Złe litery w Hanoi | Kolejność argumentów: z, na, pomocniczy; w drugim wywołaniu rekurencyjnym zamień role. |
Brak return w gałęzi rekurencyjnej | Funkcja zwraca niezdefiniowaną wartość; zwróć wynik wywołania. |
Pytania kontrolne i punktacja
- Co to jest przypadek bazowy i po co on jest?
- Dlaczego
fib(10)wywołuje się 177 razy? - Ile ruchów potrzeba dla 10 dysków i dlaczego?
Odpowiedzi: To przypadek rozwiązywany bez kolejnego wywołania; zatrzymuje rekurencję. Bo każde wywołanie rozgałęzia się na dwa i te same wartości są liczone wielokrotnie. 1023, bo liczba ruchów to 2^n − 1.
Proponowana punktacja (razem 10 punktów): zadanie 1 — ślad i wynik 3 pkt; zadanie 2 — trzy funkcje 3 pkt, liczba wywołań 1 pkt; zadanie 3 — ruchy i liczniki 2 pkt; pytania kontrolne 1 pkt. Zadanie dla chętnych nie jest wymagane do pełnej oceny.
Materiały do pobrania
Rozpakuj ZIP przed otwarciem plików. Materiały są dostępne bez logowania; publiczne rozwiązanie służy nauce i samodzielnemu sprawdzeniu, a nie poufnemu sprawdzianowi.
Co dalej?
Następny krok to dziedziczenie i polimorfizm. Ten materiał rozwija dział o algorytmach w kursie C++ w TablicaPro, gdzie znajdziesz prezentacje, ćwiczenia i testy do prowadzenia lekcji.
Źródła techniczne: Rekurencja w Wikipedii oraz Wieże Hanoi w Wikipedii. Zobacz też lekcję o funkcjach w C++.
Poprowadź kolejne lekcje C++
Pełny kurs: prezentacje, ćwiczenia i testy do pracy z klasą.
Kup dostęp do kursu C++ →