
Ciąg Fibonacciego Python w praktyce – rekurencja, iteracja i analiza złożoności algorytmu
Liczby Fibonacciego są jednym z najczęściej używanych przykładów podczas nauki algorytmiki, ponieważ na prostym schemacie pokazują kilka ważnych rzeczy naraz: zależność rekurencyjną, koszt obliczeń, pamięć programu i różnicę między rozwiązaniem eleganckim a rozwiązaniem praktycznym. Ten temat wraca zarówno na studiach, jak i podczas rozmów technicznych, bo dobrze ujawnia sposób myślenia programisty. Dobrym punktem wejścia do takich ćwiczeń jest Ciąg Fibonacciego Python.
Spis Treści
Dlaczego Ciąg Fibonacciego Python dobrze pokazuje różnicę między rekurencją a iteracją
Ciąg Fibonacciego definiuje się bardzo prosto. Dwa pierwsze wyrazy to najczęściej:
- F(0) = 0
- F(1) = 1
Każdy kolejny wyraz jest sumą dwóch poprzednich:
- F(n) = F(n – 1) + F(n – 2)
Powstaje więc sekwencja:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34…
To wygląda niewinnie, ale właśnie tutaj dobrze widać różnicę między zapisem matematycznym a rzeczywistym wykonaniem programu. Matematycznie definicja rekurencyjna jest naturalna. Programistycznie może być bardzo kosztowna.
Najczęstszy błąd początkujących polega na założeniu, że skoro zapis rekurencyjny jest krótki i „ładny”, to będzie też najlepszy wydajnościowo. W praktyce zwykle jest odwrotnie.
Wzór matematyczny i podstawowe zależności
| Element | Zapis |
|---|---|
| Definicja początkowa | F(0) = 0 |
| Definicja początkowa | F(1) = 1 |
| Zależność rekurencyjna | F(n) = F(n – 1) + F(n – 2) |
| Przykład | F(5) = F(4) + F(3) = 3 + 2 = 5 |
Najprostsza implementacja rekurencyjna w Python
| Język | Kod |
|---|---|
| Python | def fib(n):if n <= 1:return nreturn fib(n - 1) + fib(n - 2) |
Kod jest krótki i dobrze oddaje definicję matematyczną. Problem pojawia się przy większych wartościach.
Dla fib(40) program wykonuje ogromną liczbę powtórzonych obliczeń. fib(30) jest liczone wielokrotnie, podobnie fib(29), fib(28) i tak dalej. Proces zaczyna przypominać lawinę.
Wersja iteracyjna znacznie bliższa praktyce
| Język | Kod |
|---|---|
| Python | def fib_iter(n):a = 0b = 1for i in range(n):a, b = b, a + breturn a |
Tutaj nie ma powtórnych obliczeń. Program przechodzi po kolejnych wartościach raz i kończy działanie w czasie liniowym.
To jest wersja, którą realnie stosuje się częściej.
Jak Ciąg Fibonacciego Python ujawnia koszt czasowy i problem nadmiarowych obliczeń
Analiza złożoności to moment, w którym ten przykład przestaje być tylko ćwiczeniem akademickim.
Rekurencja bez pamięci podręcznej ma złożoność wykładniczą, w przybliżeniu:
O(2^n)
To oznacza bardzo szybki wzrost czasu wykonania. Różnica między n = 20 a n = 40 nie jest „dwa razy większa”, tylko wielokrotnie większa.
Wersja iteracyjna działa w:
O(n)
i zużywa stałą ilość pamięci.
To ogromna różnica.
Porównanie podejść
| Metoda | Złożoność czasowa | Złożoność pamięciowa | Praktyczne użycie |
|---|---|---|---|
| Rekurencja naiwna | O(2^n) | O(n) | głównie nauka |
| Iteracja | O(n) | O(1) | bardzo praktyczne |
| Rekurencja z memoizacją | O(n) | O(n) | dobra równowaga |
Memoizacja jako rozwiązanie pośrednie
Memoizacja polega na zapamiętywaniu wcześniej obliczonych wyników. Dzięki temu rekurencja nie liczy tych samych wartości wiele razy.
| Język | Kod |
|---|---|
| Python | memo = {}def fib_memo(n):if n in memo:return memo[n]if n <= 1:return nmemo[n] = fib_memo(n - 1) + fib_memo(n - 2)return memo[n] |
To rozwiązanie jest często wybierane na zajęciach z dynamic programming, bo dobrze pokazuje ideę pamięci podręcznej.
Ten sam problem w C
| Język | Kod |
|---|---|
| C | #include <stdio.h>int fib(int n){if (n <= 1)return n;return fib(n - 1) + fib(n - 2);}int main(){printf("%d\n", fib(10));return 0;} |
W C problem wydajności jest dokładnie taki sam. Język nie rozwiązuje złej strategii algorytmicznej.
Wersja iteracyjna w C++
| Język | Kod |
|---|---|
| C++ | #include <iostream>using namespace std;int fib(int n){int a = 0;int b = 1;for (int i = 0; i < n; i++){int temp = a;a = b;b = temp + b;}return a;} |
Tu dobrze widać proceduralny, przewidywalny przebieg programu bez kosztownego rozgałęziania.
Zastosowania poza zadaniami akademickimi i zależność z programowaniem dynamicznym
Sam ciąg rzadko jest celem biznesowym projektu. Nie buduje się systemu płatności tylko po to, żeby liczyć Fibonacciego. Ważne jest jednak to, czego ten przykład uczy.
Najważniejsza lekcja to rozpoznawanie problemów z nakładającymi się podproblemami.
Jeżeli algorytm wielokrotnie liczy te same fragmenty pracy, warto sprawdzić:
- czy można użyć memoizacji
- czy da się przejść na bottom-up
- czy iteracja będzie prostsza i szybsza
To jest dokładnie fundament programowania dynamicznego.
Podobny mechanizm pojawia się w:
- obliczaniu najkrótszych ścieżek
- problemie plecakowym
- obliczaniu odległości Levenshteina
- analizie sekwencji DNA
- optymalizacji kosztów i harmonogramów
Student zwykle widzi tylko „kolejny przykład z Fibonaccim”, ale później ten sam schemat wraca w systemach produkcyjnych.
Praktyczne zastosowania i testowanie kodu na przykładzie Ciąg Fibonacciego Python
Samo napisanie funkcji to za mało. Trzeba jeszcze sprawdzić poprawność.
Typowe błędy:
- złe warunki początkowe
- przesunięcie indeksów (
F(1)=1czyF(1)=0) - przepełnienie typu liczbowego w C/C++
- mylenie rekurencji z wydajnością
- brak testów dla małych wartości
Błąd w definicji początkowej powoduje, że cały ciąg staje się inny, a program nadal „wygląda poprawnie”.
Prosty test kontrolny
| Wejście | Oczekiwany wynik |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 2 | 1 |
| 3 | 2 |
| 5 | 5 |
| 10 | 55 |
Krótki test w Python
| Język | Kod |
|---|---|
| Python | for i in range(11):print(i, fib_iter(i)) |
Takie testy są banalne, ale oszczędzają godziny szukania błędu później.
W praktyce warto też mierzyć czas wykonania. Program, który daje poprawny wynik po 40 sekundach, często jest po prostu zły projektowo.
Na co uważać przy pracy z dużymi wartościami n i przy zadaniach rekrutacyjnych
Dla dużych n pojawia się kolejny problem: rozmiar liczby.
Python radzi sobie dobrze, ponieważ obsługuje liczby całkowite o arbitralnej długości. W C i C++ standardowe typy mają ograniczenia.
Przykładowo:
intzwykle kończy się w okolicach 2 miliardówlong longdaje większy zakres, ale nadal skończony
Dla większych indeksów trzeba używać bibliotek big integer albo liczyć modulo, jeśli zadanie tego wymaga.
W zadaniach rekrutacyjnych bardzo często pojawia się wariant:
„Oblicz n-ty wyraz modulo 1 000 000 007”
To już nie jest szkolny przykład, tylko test znajomości ograniczeń pamięci i czasu.
Wersja z modulo
| Język | Kod |
|---|---|
| Python | def fib_mod(n):mod = 1000000007a = 0b = 1for i in range(n):a, b = b % mod, (a + b) % modreturn a |
Takie podejście pojawia się bardzo często w konkursach algorytmicznych.
FAQ
Czy rekurencja zawsze jest gorsza od iteracji?
Nie. Rekurencja bywa czytelniejsza i naturalna dla problemów drzewiastych, np. DFS czy parsowanie struktur. Problem pojawia się wtedy, gdy powtarza te same obliczenia bez kontroli.
Dlaczego fib(40) działa tak długo w wersji naiwnej?
Bo funkcja wielokrotnie liczy te same wartości. fib(38) i fib(37) uruchamiają kolejne identyczne gałęzie obliczeń, a ich liczba rośnie wykładniczo.
Czy memoizacja zawsze jest najlepszym wyborem?
Nie zawsze. Czasem prostsza i szybsza będzie iteracja bottom-up. Memoizacja jest świetna dydaktycznie i wygodna przy bardziej złożonych zależnościach.
Czy warto uczyć się tego przykładu, skoro rzadko występuje w realnym projekcie?
Tak, bo nie chodzi o sam ciąg, tylko o sposób myślenia algorytmicznego. Ten sam schemat wraca później w dużo poważniejszych problemach.
Czy Python jest dobrym językiem do takich ćwiczeń?
Tak, szczególnie na początku. Kod jest krótki, łatwo testować przypadki brzegowe i nie trzeba od razu walczyć z ograniczeniami typów liczbowych.
Ciąg Fibonacciego jest prosty tylko na pierwszy rzut oka. W rzeczywistości to bardzo dobry test rozumienia algorytmów, złożoności i jakości decyzji programistycznych. Właśnie dlatego wraca tak często i właśnie dlatego warto go pisać więcej niż raz.
Źródło Foto: Freepik


