Rekurencja ogonowa
1. Czym jest rekurencja ogonowa?
Rekurencja ogonowa (tail recursion) to szczególny przypadek rekurencji, gdzie wywołanie rekurencyjne jest ostatnią operacją w funkcji.
Definicja:
Funkcja ma rekurencję ogonową, jeśli wywołanie rekurencyjne jest ostatnią rzeczą, którą funkcja robi (nie ma żadnych operacji PO rekurencji).
2. Porównanie: ogonowa vs nie-ogonowa
2.1. NIE-ogonowa (zwykła rekurencja)
Dlaczego NIE jest ogonowa?
- Po powrocie z factorial_nie_ogonowa(n - 1) musimy pomnożyć wynik przez n
- Funkcja nie może zakończyć się od razu – musi poczekać na wynik i go przetworzyć
2.2. Ogonowa (tail recursive)
Dlaczego JEST ogonowa? - Wywołanie rekurencyjne to ostatnia operacja - Nie ma nic do zrobienia po powrocie z rekurencji - Funkcja może od razu przekazać sterowanie
3. Wizualizacja różnicy
3.1. NIE-ogonowa – factorial(4)
factorial(4)
return 4 * factorial(3) ← Czeka na wynik, żeby pomnożyć
return 3 * factorial(2) ← Czeka
return 2 * factorial(1)
return 1 * factorial(0)
return 1
Powrót:
1
1 * 1 = 1
2 * 1 = 2
3 * 2 = 6
4 * 6 = 24
Stos wywołań:
factorial(4) - czeka na factorial(3)
factorial(3) - czeka na factorial(2)
factorial(2) - czeka na factorial(1)
factorial(1) - czeka na factorial(0)
factorial(0) - zwraca 1
Wszystkie ramki MUSZĄ pozostać na stosie!
3.2. Ogonowa – factorial_ogonowa(4, 1)
factorial_ogonowa(4, 1)
return factorial_ogonowa(3, 4) ← Nic nie czeka, od razu przekaż
return factorial_ogonowa(2, 12)
return factorial_ogonowa(1, 24)
return factorial_ogonowa(0, 24)
return 24
W kompilatorze z TCO (Tail Call Optimization):
Tylko JEDNA ramka na stosie w danym momencie!
Kluczowa różnica: - NIE-ogonowa: O(n) pamięci (wszystkie ramki na stosie) - Ogonowa (z TCO): O(1) pamięci (jedna ramka wielokrotnie używana)
4. Przekształcanie na rekurencję ogonową
Wzorzec przekształcenia:
4.1. Przykład: Suma liczb
NIE-ogonowa:
Ogonowa:
Użycie:
4.2. Przykład: Odwracanie listy
NIE-ogonowa:
Ogonowa:
Test:
5. Python a optymalizacja rekurencji ogonowej
5.1. Prawda o Pythonie
Python NIE optymalizuje rekurencji ogonowej (TCO)!
5.2. Dlaczego Python nie ma TCO?
Guido van Rossum (twórca Pythona) celowo NIE dodał TCO:
- Stack trace byłby niekompletny (gorsze debugowanie)
- Filozofia Pythona – "Explicit is better than implicit"
- Wydajność – można użyć iteracji
5.3. Rozwiązanie: Konwersja do iteracji
6. Symulacja TCO w Pythonie
Możemy ręcznie zasymulować TCO używając pętli:
Jak to działa:
1. Zamiast rekurencji zwracamy obiekt TailCall
2. trampoline "odbija" wywołania w pętli
3. Tylko jedna ramka stosu w danym momencie
7. Języki z TCO vs bez TCO
| Język | TCO? | Uwagi |
|---|---|---|
| Python | ❌ NIE | Celowo brak, użyj iteracji |
| JavaScript | ⚠️ Zależy | ES6+ ma TCO, ale słabe wsparcie |
| Java | ❌ NIE | JVM nie wspiera |
| C/C++ | ⚠️ Zależy | Zależy od kompilatora i optymalizacji |
| Scheme | ✅ TAK | Wymaga TCO przez standard |
| Haskell | ✅ TAK | Domyślnie optymalizuje |
| Scala | ✅ TAK | Tylko dla self-recursion (@tailrec) |
| Rust | ✅ TAK | LLVM optymalizuje |
8. Rekurencja ogonowa vs iteracja
8.1. Wydajność
8.2. Konwersja rekurencja ogonowa → iteracja
Każdą rekurencję ogonową można mechanicznie przekształcić do iteracji:
Przykład – silnia:
9. Rekurencja wzajemna ogonowa
Dwie lub więcej funkcji wywołujących się nawzajem:
Problem w Pythonie: Nadal RecursionError dla dużych n!
Rozwiązanie: Iteracja z flagą
10. Ćwiczenia
Zadanie 1: Przekształć na rekurencję ogonową
Rozwiązanie
Zadanie 2: Długość listy (ogonowa)
11. Kiedy używać rekurencji ogonowej?
✅ Używaj gdy:
- Język wspiera TCO (Scheme, Haskell, Scala)
- Chcesz czytelny kod funkcyjny
- Problem ma naturę sekwencyjną
❌ Unikaj gdy:
- Język nie ma TCO (Python, Java)
- Iteracja jest prostsza
- Wydajność jest kluczowa
W Pythonie:
Rekurencja ogonowa w Pythonie = iteracja z extra steps
Lepiej użyj od razu pętli!
12. Podsumowanie
Rekurencja ogonowa:
- Wywołanie rekurencyjne to ostatnia operacja
- Nie ma nic do zrobienia po powrocie
- W językach z TCO → O(1) pamięci
- W Pythonie → bez różnicy (brak TCO)
Przekształcenie na ogonową:
- Dodaj akumulator jako parametr
- Wykonuj operacje przed rekurencją
- Zwracaj wynik akumulatora w przypadku bazowym
Python a TCO:
- Python celowo nie ma TCO
- Rekurencja ogonowa = tak samo wolna jak zwykła
- Rozwiązanie: użyj iteracji!
Konwersja:
Rekurencja ogonowa → Iteracja (mechaniczna konwersja)
while not stop_condition:
akumulator = new_value
n = decrease(n)
Kluczowa prawda:
W Pythonie rekurencja ogonowa nie daje korzyści. Jeśli planujesz rekurencję ogonową – użyj od razu iteracji!
Co dalej warto poznać:
- Trampoline pattern (symulacja TCO)
- Continuation-passing style (CPS)
- Języki funkcyjne (Haskell, Scheme)
- Programowanie funkcyjne w praktyce