Poznaliśmy wiele ciekawych elementów Pythona. To dobry moment, aby na chwilę się od tego oderwać i bardziej skoncentrować na pewnych podstawach myślenia algorytmicznego i poznać podstawowe narzędzia do rozwiązywania różnych problemów.
Skupimy się tutaj na dwóch pojęciach, iteracji i rekurencji, postaramy się za ich pomocą rozwiązać kilka problemów algorytmicznych.
Iteracja – powtórz n razy
Iteracja czyli używanie pętli. Napiszmy sobie funkcję, która wypisze nam jakiś tekst n razy, korzystając z pętli. Nie jest to raczej jakieś wielkie wyzwanie, poznaliśmy wszystkie elementy Pythona, które nam to umożliwią.
Dla przypomnienia: powtórzyć coś n razy:
for _ in range(3):
print("Hello World!")
# Hello World!
# Hello World!
# Hello World!
Napisać funkcję, która wypisze jakiś tekst:
def say_hello():
print("hello world")
say_hello()
#hello world
Napisać funkcję, która przyjmuje jakiś argument i coś z nim robi:
def times_two(n):
return n * 2
print(times_two(10)) #20
Połączyć to wszystko w jedną całość i mamy nasz program, który wypisuje „hello world” n razy:
def n_hello(n):
for _ in range(n):
print("Hello World")
n_hello(4)
# Hello World
# Hello World
# Hello World
# Hello World
Jest to rozwiązanie iteracyjne, czyli wykorzystujące pętlę.
Rekurencja – powtórz n razy
Ten sam problem możemy rozwiązać za pomocą rekurencji. Jest to podejście, w którym funkcja posiada pewien warunek, który kończy jej wykonanie, jak również wywołanie samej siebie. I będzie samą siebie wywoływać, aż warunek kończący zostanie spełniony.
Jak działa nasza funkcja powtórz n razy w wersji rekurencyjnej?
def hello_rec(n):
if n == 0:
return
print("Hello World")
hello_rec(n-1)
hello_rec(3)
# Hello World
# Hello World
# Hello World
Mamy funkcję hello rec. Przyjmuje ona tak samo warunek n. Jeżeli n jest równe zero mamy słówko kluczowe return, które oznacza wyjście z funkcji i zwrócenie wartości (funkcje „printujące” niczego nie zwracają, ale w rekurencji wyjście z funkcji musi zostać zaznaczone, stąd słówko return bez niczego).
Ale nasze n to nie jest 0, tylko 3. W takim wypadku Wypisujemy hello world i wywołujemy funkcję hello_rec wewnątrz niej samej, ale z n zmienionym na n-1.
Nasze n równa się 2. Po raz kolejny piszemy hello world i wywołujemy hello_rec z niej samej, n-1 czyli 1.
Raz jeszcze piszemy hello world, wywołujemy hello_rec z niej samej, tym razem n-1 czyli 0. I spełnia nasz warunek wyjścia a jako że mamy funkcję „printującą” czyli nie zwracającą niczego, mamy tam pusty return.
Trochę trzeba nad tym posiedzieć aby zrozumieć pokrętną, rekurencyjną logikę, ale warto.
Silnia – iteracja
Silnia to takie coś, co dla 2 daje 1 * 2. Dla 3 1*2*3. I tak dalej, silnia dla 4 to 1*2*3*4. Można ten problem rozwiązać iteracyjnie, czyli w pętli.
Tutaj mamy przykład z internetu jak się za to zabrać:
def factorial(n):
fact = 1
for num in range(2, n + 1):
fact *= num
return fact
print(factorial(2)) #2
print(factorial(3)) #6
Czyli tak, silnia 2 ma dać 1 * 2. A zatem zmienna fact równa 1, bo w każdej silnii zaczynamy od 1. Następnie w zakresie od 2 do 2+1 czyli w zakresie od 2 do 3 ale bez 3, mnożymy fact przez num, tutaj 2.
Możemy to sobie przeanalizować na kartce papieru, ale jest to taki algorytm i działa.
Załóżmy, że mamy silnię 3, czyli mamy dostać 1 * 2 * 3.
1 już mamy, zmienna fact. W zakresie od 2 do 3+1 czyli w zakresie od 2 do 4 bez 4, mnożymy fact (1) najpierw przez 2, potem przez 3 (4 już nie).
Zwracamy wynik mnożenia 1 * 2 * 3 dla argumentu 3. Działa. Chodzi. Wszystko w pętli, czyli iteracyjnie.
Nie są to najłatwiejsze zadania, ale zawsze można je na chłodno przeanalizować i zrozumieć, jak co działa.
Silnia – rekurencyjnie
Funkcja rekurencyjna wywołuje samą siebie aż spełniony zostanie „warunek wyjścia”. Ponadto silnia to już funkcja, która coś zwraca, a zatem będzie ciekawie.
Oto nasz przykład rekurencyjnego rozwiązania silnii:
def fact_rec(n):
if n == 1:
return n
return n * fact_rec(n-1)
print(fact_rec(2)) #2
print(fact_rec(3)) #6
Działamy w ten sposób – jeżeli n jest równe 1, zwróć n (czyli 1). Jeżeli jest większe, zwróć wynik mnożenia n razy silnia n – 1.
Czyli po ludzku – bierzesz dwa? Okej, 2 to nie jeden. Zwróć mi wynik z mnożenia 2 razy wywołanie mnie samej ale z 2-1.
A zatem wywołuję się z wnętrza funkcji, ale z n jako 2-1 czyli z 1. Warunek spełniony, zwracam n. Czyli 1. Okej, ale jeszcze coś mi tam leży do zrobienia.
To coś, to było 2 razy wynik tego, co już mam, czyli 1. Aha, zwróć 2 * 1. Gotowe.
Dla silnii równej 3 ten proces po prostu powtarzamy jeden raz więcej. Zaczynamy od 3, mnożymy 3 * silnia 2. Silnia 2 to 2 * silnia 1. Silnia 1 to 1. I tak idąc od tamtąd mamy zwrócić 1 * 2 * 3. Taki wynik dostajemy na końcu.
Nie musimy lubić ani w pełni rozumieć rekurencji. Ważne, że wiemy, że coś takiego istnieje, potrafimy to sobie, jeżeli się wysilimy, rozpisać na kartce i zrozumieć zarówno sam algorytm, czyli nasze podejście do rozwiązania problemu, jak i sposób działania rekurencji. Wprawa nadejdzie z czasem, o ile będziemy z tej rekurencji w ogóle korzystać. Aczkolwiek grzechem byłoby jej nie znać.
Odwróć tekst – iteracyjnie
Piszemy funkcję, aby iteracyjnie (czyli w pętli a nie uciekając się do jakichś sztuczek, jak choćby [::-1]) odwrócić tekst.
Do dzieła. Tym razem zastosujemy pętlę while, bo nie samym for człowiek żyje:
def reverse_text(text):
rev = ""
idx = len(text) - 1
while idx >= 0:
rev += text[idx]
idx -= 1
return rev
print(reverse_text("hello"))
#olleh
A zatem tak – przyjmujemy text jako nasz argument. Zmienna rev to na razie pusty napis „”. Bierzemy indeks ostatniego znaku w naszym tekście, a to jest jego długość -1 (bo zawsze indeksujemy od 0 a zatem długość tekstu jest o 1 większa niż jego ostatni indeks).
Mamy już ostatni indeks. Teraz warunek, że zmienna idx jest większa lub równa 0. Do rev dodajemy element z text o indeksie ostatnim. Zmniejszamy idx o 1 i już mamy indeks przedostatni. I tak idziemy, tak długo, aż idx jest większe lub równe 0 (indeks ostatni). Wtedy go dodajemy, zmniejszamy o 1 i pętla stopuje, -1 nie jest większe ani nawet równe zeru, zaś my dodaliśmy już (wspak) wszystko, co było do dodania, nic tylko zwrócić wynik.
Tak to działa i do rev z hello dopisane jest po koleji o,l,l,e,h. I zwrócony tekst.
Odwracanie tekstu – rekurencyjnie
Odwrócić tekst da się też rekurencyjnie, można to zrobić funkcją, która wywołuje siebie samą. Tak to wygląda:
def reverse(s):
if s == "":
return s
else:
return reverse(s[1:]) + s[0]
s = "abc"
print(reverse(s))
Warunek brzegowy to pusty string „”. To już daje nam do myślenia, że napis z każdym wywołaniem funkcji będzie skracany, aż do „”.
Dalej mamy zupełnie niepotrzebny else, bo ten przykład jest z internetu. Wywalmy ten else:
def reverse(s):
if s == "":
return s
return reverse(s[1:]) + s[0]
s = "abc"
print(reverse(s)) #cba
Dalej mamy coś ciekawego, bowiem najpierw mamy wywołanie samej siebie a potem znak dodawania (w przypadku tekstów – sklejania tudzież konkatenacji) i element zerowy tekstu.
Działa to tak: przyjmij „abc”. Czy „abc” jest pustym napisem? Nie? To wywołaj się sam na „bc” (wszystko od indeksu 1 do końca) + „a”.
„bc” nie jest pustym napisem, a zatem wywołaj się na „c” (wszystko od indeksu 1 do końca) plus element zerowy „b” po prawej stronie.
Możemy to sobie przeanalizować na kartce, koniec końców ta funkcja zwraca „” +”c” +”b” +”a” co daje nam „cba”, odwrócony tekst. Oczywiście „abc” to tylko taki przykład.
Mam nadzieję, że z tego wpisu coś wynieśliśmy, nawet jeżeli niektóre zagadnienia są problematyczne, powinniśmy znać i rozumieć rekurencję i iterację oraz odrobinę poczuć jak się pracuje z takim bardziej algorytmicznym programowaniem. Od nas zależy, czy chcemy podążać w tym kierunku, czy bardziej pisać programy albo aplikacje internetowe. Ale podstawy podstaw mamy i w razie czego (nie zawsze jest to pisanie samodzielnego kodu, często czytanie cudzego) – jesteśmy w stanie zrozumieć rekurencyjne bądź iteracyjne algorytmy.