Wprowadzenie do rekurencji - jak myśleć rekurencyjnie

1. Czym jest rekurencja?

Rekurencja to technika programowania, w której funkcja wywołuje samą siebie w celu rozwiązania problemu.

Definicja matematyczna:

Rekurencja: patrz "rekurencja" 😄

Analogia ze świata rzeczywistego:

Rosyjskie matroszki (babuszki): - Każda lalka zawiera mniejszą lalkę - Najmniejsza lalka nie zawiera nic (przypadek bazowy) - Otwierając lalkę, odkrywamy mniejszą wersję tego samego problemu

🪆 → 🪆 → 🪆 → 🪆 → 🪆 (najmniejsza, nie da się otworzyć)

2. Anatomia funkcji rekurencyjnej

Każda funkcja rekurencyjna musi mieć dwa kluczowe elementy:

2.1. Przypadek bazowy (base case)

Warunek, który zatrzymuje rekurencję. Bez niego funkcja wywołuje się w nieskończoność!

2.2. Krok rekurencyjny (recursive case)

Wywołanie funkcji dla mniejszego problemu.

Kompletny przykład – silnia:


3. Jak działa rekurencja? (krok po kroku)

Przykład: factorial(3)

factorial(3)
  ↓
  3 * factorial(2)
        ↓
        2 * factorial(1)
              ↓
              1 * factorial(0)
                    ↓
                    1  ← przypadek bazowy

Teraz wracamy:
                    1
              1 * 1 = 1
        2 * 1 = 2
  3 * 2 = 6

Wynik: 6

Wizualizacja drzewa wywołań:

            factorial(3)
                 |
            3 * factorial(2)
                     |
                2 * factorial(1)
                         |
                    1 * factorial(0)
                             |
                            1

4. Pierwszy przykład: Odliczanie

4.1. Wersja iteracyjna

4.2. Wersja rekurencyjna

Jak to działa:

  1. odliczanie(5) → wypisz 5, wywołaj odliczanie(4)
  2. odliczanie(4) → wypisz 4, wywołaj odliczanie(3)
  3. odliczanie(3) → wypisz 3, wywołaj odliczanie(2)
  4. odliczanie(2) → wypisz 2, wywołaj odliczanie(1)
  5. odliczanie(1) → wypisz 1, wywołaj odliczanie(0)
  6. odliczanie(0) → wypisz "Start!", STOP

5. Jak myśleć rekurencyjnie?

5.1. Zasada "Divide and Conquer" (Dziel i zwyciężaj)

Podziel problem na mniejsze wersje tego samego problemu.

Przykład: Suma liczb od 1 do n

suma(5) = 5 + suma(4)
suma(4) = 4 + suma(3)
suma(3) = 3 + suma(2)
suma(2) = 2 + suma(1)
suma(1) = 1  ← przypadek bazowy

5.2. Wzór myślenia rekurencyjnego

  1. Znajdź przypadek bazowy – kiedy problem jest trywialny?
  2. Zdefiniuj krok rekurencyjny – jak rozwiązać problem używając mniejszej wersji?
  3. Upewnij się, że zbiegasz do przypadku bazowego – problem musi się zmniejszać!

5.3. Przykład: Potęgowanie

Problem: Oblicz a^n (a do potęgi n)

Myślenie rekurencyjne: - Przypadek bazowy: a^0 = 1 - Krok rekurencyjny: a^n = a * a^(n-1)

Wizualizacja dla potega(2, 4):

potega(2, 4)
= 2 * potega(2, 3)
= 2 * (2 * potega(2, 2))
= 2 * (2 * (2 * potega(2, 1)))
= 2 * (2 * (2 * (2 * potega(2, 0))))
= 2 * (2 * (2 * (2 * 1)))
= 2 * (2 * (2 * 2))
= 2 * (2 * 4)
= 2 * 8
= 16

6. Rekurencja na listach

6.1. Suma elementów listy

Wizualizacja:

suma_listy([1, 2, 3, 4, 5])
= 1 + suma_listy([2, 3, 4, 5])
= 1 + (2 + suma_listy([3, 4, 5]))
= 1 + (2 + (3 + suma_listy([4, 5])))
= 1 + (2 + (3 + (4 + suma_listy([5]))))
= 1 + (2 + (3 + (4 + (5 + suma_listy([])))))
= 1 + (2 + (3 + (4 + (5 + 0))))
= 15

6.2. Długość listy

6.3. Odwracanie listy

6.4. Maximum w liście


7. Rekurencja na stringach

7.1. Odwracanie stringa

7.2. Palindrom (sprawdzanie)

7.3. Zliczanie wystąpień znaku


8. Wizualizacja rekurencji z printem

Dodajmy printy, żeby zobaczyć, co się dzieje:

Wynik:

→ factorial(4)
  → factorial(3)
    → factorial(2)
      → factorial(1)
        → factorial(0)
        ← zwracam 1
      ← zwracam 1
    ← zwracam 2
  ← zwracam 6
← zwracam 24

9. Typowe błędy początkujących

Błąd 1: Brak przypadku bazowego

Błąd 2: Przypadek bazowy nigdy nie jest osiągany

Błąd 3: Zapomnienie o return

Poprawnie:


10. Wzorce rekurencyjne

10.1. Wzorzec "Zmniejsz o 1"

Przykłady: silnia, suma, potęga

10.2. Wzorzec "Podziel na pół"

Przykłady: merge sort, binary search

10.3. Wzorzec "Głowa + ogon"

Przykłady: suma listy, długość listy


11. Kiedy używać rekurencji?

Używaj rekurencji gdy:

✅ Problem ma strukturę rekurencyjną (drzewa, grafy) ✅ Dzielenie problemu jest naturalne (merge sort, quick sort) ✅ Czytelność jest ważniejsza niż wydajność ✅ Problem wymaga backtrackingu (permutacje, sudoku)

Unikaj rekurencji gdy:

❌ Można to łatwo zrobić iteracyjnie (proste pętle) ❌ Wydajność jest krytyczna (rekurencja wolniejsza) ❌ Głębokość może być bardzo duża (stack overflow) ❌ Problem nie ma struktury rekurencyjnej


12. Przykłady problemów rekurencyjnych

12.1. Liczby Fibonacciego

12.2. Liczba cyfr w liczbie

12.3. Suma cyfr liczby

12.4. NWD (Największy Wspólny Dzielnik) – Algorytm Euklidesa

Wizualizacja:

nwd(48, 18)
= nwd(18, 48 % 18)
= nwd(18, 12)
= nwd(12, 18 % 12)
= nwd(12, 6)
= nwd(6, 12 % 6)
= nwd(6, 0)
= 6

13. Rekurencja wielokrotna

Niektóre funkcje wywołują się więcej niż raz w kroku rekurencyjnym.

13.1. Fibonacci (dwukrotne wywołanie)

Drzewo wywołań dla fibonacci(5):

                    fib(5)
                   /      \
              fib(4)      fib(3)
             /     \       /    \
        fib(3)   fib(2) fib(2) fib(1)
        /   \     /  \   /  \
    fib(2) fib(1) ...
    /  \
fib(1) fib(0)

Problem: Wiele powtórzonych obliczeń! (fib(3) liczone 2 razy, fib(2) aż 3 razy!)


14. Ćwiczenia

Zadanie 1: Suma liczb parzystych

Napisz funkcję rekurencyjną, która sumuje liczby parzyste od 1 do n.

Rozwiązanie

Zadanie 2: Iloczyn elementów listy

Zadanie 3: Czy lista jest posortowana?


15. Podsumowanie

Rekurencja to potężna technika programowania:

  • Funkcja wywołuje samą siebie
  • Wymaga przypadku bazowego (STOP)
  • Naturalny sposób myślenia o niektórych problemach
  • Elegancki kod, często krótszy niż iteracja

Kluczowe zasady:

  1. Zawsze definiuj przypadek bazowy
  2. Upewnij się, że zbiegasz do przypadku bazowego
  3. Pamiętaj o return w kroku rekurencyjnym
  4. Myśl "dziel i zwyciężaj"

Zalety rekurencji:

  • Elegancki, czytelny kod
  • Naturalne dla drzew, grafów
  • Łatwe backtracking

Wady rekurencji:

  • Zużywa pamięć (stos wywołań)
  • Może być wolniejsza
  • Stack overflow dla dużych danych

Co dalej warto poznać:

  • Rekurencja vs iteracja
  • Stos wywołań
  • Rekurencja ogonowa
  • Memoizacja (optymalizacja)