Dataczwartek, 13 sierpnia 2026 Czas19:03:54
← Programowanie konsolowe (C#)

Lekcja 20. Rekurencja

Średni

Po co się tego uczymy?

Niektóre problemy naturalnie definiuje się "przez samego siebie" — silnia liczby, przeszukiwanie folderów i podfolderów na dysku, przechodzenie po drzewie komentarzy pod postem, algorytm dziel-i-zwyciężaj. Rekurencja to metoda pisania takich rozwiązań: funkcja, która w swoim ciele wywołuje samą siebie. To jeden z efektów kształcenia w podstawie programowej (INF.04.4.2) i regularnie pojawia się na egzaminie, dlatego trzeba to rozumieć, a nie tylko przepisać wzór.

Teoria

Funkcja rekurencyjna składa się zawsze z dwóch elementów:

  • przypadku bazowego (ang. base case) — najprostszej wersji problemu, którą da się rozwiązać wprost, bez dalszego wywoływania funkcji. To "hamulec", który zatrzymuje rekurencję;
  • kroku rekurencyjnego — rozbicia problemu na mniejszy podobny problem i wywołania funkcji samej na sobie z tym mniejszym problemem.

Jeśli zabraknie przypadku bazowego (albo jest błędny), funkcja będzie wywoływać samą siebie w nieskończoność, aż program wyrzuci błąd StackOverflowException — pamięć przeznaczona na wywołania funkcji ("stos wywołań") się skończy. To najczęstszy błąd początkujących.

Każde wywołanie rekurencyjne trafia na stos wywołań i czeka tam, aż wywołanie, które samo uruchomiło, zwróci wynik. Dlatego rekurencja "schodzi w dół" do przypadku bazowego, a potem wyniki "wracają w górę", łącząc się po drodze (zobacz schemat).

Rekurencja nie jest jedyną metodą — prawie każdy algorytm rekurencyjny da się też zapisać iteracyjnie (przez pętlę). Czasem wersja iteracyjna jest szybsza i zużywa mniej pamięci, ale wersja rekurencyjna bywa dużo czytelniejsza dla problemów, które same w sobie mają strukturę drzewiastą lub "zagnieżdżoną".

Schemat

Silnia(4) = 4 * Silnia(3)
              |
              4 * (3 * Silnia(2))
                       |
                       4 * (3 * (2 * Silnia(1)))
                                     |
                                     4 * (3 * (2 * 1))   <- przypadek bazowy, Silnia(1) = 1

Wywołania "schodzą w dół" (każde czeka na wynik następnego),
a potem "wracają w górę" mnożąc po drodze:

Silnia(4)
 └─ 4 * Silnia(3)
     └─ 3 * Silnia(2)
         └─ 2 * Silnia(1)
             └─ zwraca 1   (przypadek bazowy — tu się zatrzymujemy!)
         zwraca 2 * 1 = 2
     zwraca 3 * 2 = 6
 zwraca 4 * 6 = 24

Przykład z życia

Eksplorator plików w Windows, kiedy liczysz rozmiar folderu, musi zajrzeć do każdego podfolderu, a w nim do kolejnych podfolderów, i tak dalej, aż trafi na same pliki (przypadek bazowy). To naturalna rekurencja — struktura folderów sama w sobie jest rekurencyjna (folder może zawierać foldery). Podobnie działa wyświetlanie komentarzy z odpowiedziami na odpowiedzi pod artykułem, albo menu firmy z działami i poddziałami.

Kod (C#)

using System;

class Program
{
    // Silnia liczby n (n!) metodą rekurencyjną.
    static long Silnia(int n)
    {
        // Przypadek bazowy — silnia z 0 i z 1 wynosi 1.
        // Bez tego warunku funkcja wywoływałaby się w nieskończoność.
        if (n <= 1)
        {
            return 1;
        }

        // Krok rekurencyjny: n! = n * (n-1)!
        return n * Silnia(n - 1);
    }

    // Suma cyfr liczby, też rekurencyjnie.
    static int SumaCyfr(int liczba)
    {
        if (liczba < 10)
        {
            return liczba; // przypadek bazowy: jedna cyfra
        }

        return liczba % 10 + SumaCyfr(liczba / 10);
    }

    static void Main()
    {
        Console.WriteLine($"5! = {Silnia(5)}");
        Console.WriteLine($"Suma cyfr 4829 = {SumaCyfr(4829)}");
    }
}

Komentarz i wyjaśnienie kodu

Silnia(n) najpierw sprawdza przypadek bazowy (n <= 1) — jeśli go nie ma, funkcja zwraca 1 od razu, bez dalszych wywołań. Jeśli warunek nie jest spełniony, funkcja wywołuje samą siebie z mniejszym argumentem (n - 1) i mnoży wynik przez n. Dzięki temu, że argument za każdym razem maleje, funkcja prędzej czy później dotrze do przypadku bazowego.

SumaCyfr działa podobnie, ale "obcina" liczbę z prawej strony przez dzielenie całkowite (liczba / 10) i dodaje ostatnią cyfrę (liczba % 10) do wyniku wywołania rekurencyjnego dla reszty liczby.

Ćwiczenie samodzielne

Napisz rekurencyjną metodę SumaDo(int n), która liczy sumę liczb od 1 do n (np. SumaDo(5) = 1+2+3+4+5 = 15). Zastanów się najpierw: jaki jest przypadek bazowy? Jaki jest krok rekurencyjny?

Plac zabaw — wypróbuj online

Poniższy kod wykonuje się od razu w Twojej przeglądarce — nic nie trzeba instalować. Zmień kod i kliknij „Uruchom”.

CSHARP

Zadania do pracy własnej

  1. Napisz metodę rekurencyjną CzyPalindrom, która sprawdza, czy tekst czyta się tak samo od przodu i od tyłu (np. "kajak"). Podpowiedź: przypadek bazowy to tekst o długości 0 lub 1.

  2. Napisz metodę rekurencyjną obliczającą n-ty wyraz ciągu Fibonacciego (0, 1, 1, 2, 3, 5, 8, 13...). Uruchom ją dla n=35 i zaobserwuj, jak długo się liczy — potem zastanów się (albo poszukaj hasła "memoizacja"), dlaczego jest to aż tak wolne.

  3. Napisz metodę rekurencyjną, która wypisuje wszystkie sposoby wydania danej kwoty za pomocą monet o nominałach 1, 2 i 5 groszy (bez powtarzania tych samych kombinacji w innej kolejności).

Typowe błędy

Brak przypadku bazowego albo błędny warunek — program "zawiesza się" i po chwili wywala StackOverflowException. Zawsze najpierw zapisz na kartce: "kiedy funkcja może odpowiedzieć bez dalszego wywoływania samej siebie?".

Argument nie maleje w kierunku przypadku bazowego — np. pomyłkowe wywołanie Silnia(n) zamiast Silnia(n - 1) tworzy nieskończoną pętlę wywołań identyczną jak brak przypadku bazowego.

Nadużywanie rekurencji tam, gdzie prosta pętla wystarczy — rekurencja zużywa pamięć stosu i jest zwykle wolniejsza niż pętla. Dobieraj narzędzie do problemu: jeśli struktura nie jest "zagnieżdżona" (jak drzewo czy foldery), zwykła pętla for/while często będzie lepszym wyborem.

Nawiązanie do egzaminu zawodowego

Rekurencja to jeden z algorytmów wymienionych wprost w podstawie programowej (INF.04.3.3 — "algorytmy iteracyjne, tekstowe, szyfrowania, tablicowe, rekurencyjne") oraz w zasadach programowania obiektowego (INF.04.4.2). Na egzaminie może pojawić się jako zadanie "napisz funkcję rekurencyjną obliczającą..." albo jako pytanie teoretyczne o przypadek bazowy i stos wywołań.