Twoje Centrum Szkoleniowe

Nauczmy się dziś czegoś nowego!

Kurs programowania - C++

C++: Funkcje rekurencyjne

Rekurencja w programowaniu to technika, w której funkcja wywołuje samą siebie bezpośrednio lub pośrednio. Jest to popularna metoda rozwiązywania złożonych problemów przez dzielenie ich na mniejsze, bardziej zarządzalne zadania.

 

Struktura Funkcji Rekurencyjnych

Struktura funkcji rekurencyjnej w C++ opiera się na dwóch głównych elementach: warunku zakończenia i samym wywołaniu rekurencyjnym. Prawidłowo zbudowana funkcja rekurencyjna zawiera:

  1. Warunek Zakończenia (Bazowy): Jest to kluczowy element, który zapobiega nieskończonej rekurencji. Określa warunki, przy których funkcja przestaje wywoływać samą siebie i zwraca wynik.
  2. Wywołanie Rekurencyjne: W tej części funkcja wywołuje samą siebie z nowym zestawem parametrów, zbliżając się do warunku bazowego.

 

Wypisywanie Liczb od 1 do 10
void wypiszLiczby(int n) {
    if (n <= 10) {
        std::cout << n << " ";
        wypiszLiczby(n + 1); // Wywołanie rekurencyjne
    }
}

W tej funkcji wypiszLiczby:

  • Rozpoczynamy od wywołania funkcji z wartością n = 1.
  • Warunek Zakończenia: Jeśli n jest mniejsze lub równe 10, kontynuujemy wykonywanie funkcji. Gdy n przekroczy 10, warunek zakończenia jest spełniony, co zatrzymuje dalsze wywołania rekurencyjne.
  • Wywołanie Rekurencyjne: Jeśli warunek jest spełniony, wypisujemy wartość n, a następnie wywołujemy tę samą funkcję z n + 1. Każde takie wywołanie zwiększa n o 1, zbliżając się do momentu, gdy n będzie większe niż 10.

 

Rekurencyjne Obliczanie Silni

Silnia liczby n (oznaczana jako n!) to iloczyn wszystkich liczb całkowitych od 1 do n. Silnię można obliczyć rekurencyjnie:

int silnia(int n) {
    if (n <= 1) return 1; // Warunek bazowy: silnia z 0 i 1 to 1
    else return n * silnia(n - 1); // Wywołanie rekurencyjne
}

 

W funkcji silnia:

  • Gdy n jest równe 1 lub mniejsze, zwracamy 1. Jest to warunek zakończenia rekurencji, ponieważ silnia z 0 i 1 wynosi 1.
  • W przeciwnym razie, funkcja wywołuje samą siebie z parametrem n - 1, co zmniejsza wartość n o 1 za każdym wywołaniem, aż do osiągnięcia warunku bazowego.

Dzięki rekurencji, funkcja silnia "rozładowuje" problem na mniejsze fragmenty (obliczanie silni z coraz mniejszych liczb), aż do momentu, gdy osiągnie łatwy do rozwiązania przypadek bazowy.

 

Efektywność i optymalizacja

Funkcje rekurencyjne, choć potężne, mogą być kosztowne pod względem zużycia zasobów, zwłaszcza pamięci stosu. Każde wywołanie rekurencyjne dodaje nową warstwę na stosie, co oznacza, że głęboka rekurencja może prowadzić do przeciążenia stosu i błędów takich jak przepełnienie stosu (stack overflow).

  • Zużycie Pamięci: Rekurencja zużywa pamięć na przechowywanie stanów funkcji. Im głębsza rekurencja, tym więcej pamięci jest wymagane.
  • Czas Wykonania: Rekurencyjne wywołania mogą prowadzić do powtórzeń tych samych obliczeń, co zwiększa czas wykonania, szczególnie w przypadku braku efektywnego warunku zakończenia.

 

Rekurencja a iteracja

Rekurencja i iteracja to dwie podstawowe techniki realizowania powtarzalnych operacji w programowaniu. Choć obie metody mogą być używane do osiągnięcia podobnych wyników, istnieją kluczowe różnice w ich zastosowaniu i wydajności.

  1. Jasność kodu: Rekurencja często prowadzi do kodu, który jest bardziej zwięzły i łatwiejszy do zrozumienia, szczególnie w przypadkach, gdy problem naturalnie pasuje do rekurencyjnego podejścia, takich jak przechodzenie po strukturach drzewiastych.

  2. Wydajność i zużycie zasobów: Iteracja jest zwykle bardziej wydajna pod względem zużycia pamięci i czasu procesora, ponieważ nie wymaga dodatkowego obciążenia stosu wywołań, jak to ma miejsce w rekurencji.

  3. Przypadki użycia: Rekurencja jest preferowana w algorytmach, gdzie rozwiązanie problemu zależy od rozwiązań mniejszych instancji tego samego problemu. Iteracja jest wybierana w scenariuszach, gdzie potrzebne jest proste powtarzanie tej samej operacji.

 

Przekształcanie rekurencji w iterację

W niektórych przypadkach, funkcje rekurencyjne mogą być przekształcone w równoważne funkcje iteracyjne, co może prowadzić do lepszej wydajności.

 

Przykładowe przekształcenie rekurencyjnej funkcji obliczającej silnię na iteracyjną:

 

Rekurencyjnie

int silniaRekurencyjnie(int n) {
    if (n <= 1) return 1;
    return n * silniaRekurencyjnie(n - 1);
}

 

  1. Warunek bazowy: if (n <= 1) return 1; - To jest warunek zakończenia rekurencji. Gdy n jest mniejsze lub równe 1, funkcja zwraca 1, kończąc rekurencję.

  2. Wywołanie rekurencyjne: return n * silniaRekurencyjnie(n - 1); - Tutaj funkcja wywołuje samą siebie, ale z argumentem n - 1. Każde wywołanie rekurencyjne zmniejsza wartość n o 1, aż osiągnięte zostanie warunek bazowy.

 

Iteracyjnie

int silniaIteracyjnie(int n) {
    int wynik = 1;
    for (int i = 2; i <= n; i++) {
        wynik *= i;
    }
    return wynik;
}

 

  1. Inicjalizacja wyniku: int wynik = 1; odpowiada przypadkowi bazowemu w rekurencji. Zaczynamy od wartości 1, ponieważ 1! = 1.
  2. Pętla iteracyjna: for (int i = 2; i <= n; i++) { wynik *= i; } - zastępuje krok rekurencyjny. Zamiast wywoływać funkcję rekurencyjnie, pętla for iteracyjnie mnoży wynik przez kolejne liczby od 2 do n. Ta część odpowiada mnożeniu n * (n-1) * (n-2) * ... * 1 w rekurencyjnym podejściu.

 

Związek między obiema funkcjami

  • W rekurencyjnym podejściu, każde wywołanie funkcji silniaRekurencyjnie dodaje nową ramkę na stosie wywołań, przechowującą lokalne zmienne i stan wywołania. W iteracyjnym podejściu, wszystkie obliczenia są wykonywane w jednej ramce wywołania funkcji, co czyni ją efektywniejszą w kontekście zarządzania pamięcią i czasem wykonania, szczególnie dla dużych wartości n.
  • Wersja iteracyjna jest bardziej bezpośrednia i zazwyczaj lepiej sprawdza się w praktycznych zastosowaniach, zwłaszcza ze względu na ryzyko przekroczenia limitu stosu w rekurencji, które może wystąpić przy bardzo dużych wartościach n.

Testy przypięte do lekcji
Aby uzyskać dostęp do testów i ćwiczeń interaktywnych - Zaloguj się
Aby widzieć ocenę lekcji - Zaloguj się