16: Rekurencja

Widzieliśmy, że funkcje umożliwiają organizowanie i ponowne wykorzystanie części naszego kodu. Widzieliśmy również, że funkcję można zdefiniować za pomocą innych funkcji. W tej lekcji dowiadujemy się, że funkcja może być zdefiniowana przez samą siebie! To bardzo użyteczne podejście nosi nazwę rekurencji (rekursji). Legenda głosi, że "aby zrozumieć rekurencję, musisz najpierw zrozumieć rekurencję".

Przykład

W naszej lekcji na pętlach użyliśmy pętli while, aby utworzyć następujące wyjście.

5
4
3
2
1
Blastoff!
Oto program, który używa rekurencji do osiągnięcia tego samego efektu.

Przykład
Liczenie w dół z użyciem rekurencji

Dodamy dodatkowe wydruki, aby zrozumieć, jak program działa. Ta wersja programu odczytuje również liczbę, od której ma sie rozpocząć liczenie.

Przykład
Odliczanie z użyciem rekurencji
Możesz wprowadzić dane dla programu w poniższym polu.

Jeśli chcesz wypróbować inne wartości wejściowe, skorzystaj w powyższym programie z opcji Wprowadź dane. Wypróbuj najpierw 0 i zobaczyć, co się dzieje, a następnie 1.

Gdy na wejściu mamy 5, program najpierw wywołuje kopię funkcji countdown dla n=5, która drukuje 5 i wywołuje countdown(4). To trwa aż do countdown(0), kiedy to drukuje "Blastoff!" I nie wywołuje więcej countdown. Gdy Python dla n=0 kończy wywoływanie funkcji countdown, Python powraca do funkcji, która go wywołała, co oznacza, że teraz n=1 dla funkcji countdown. Potem wrócimy do wywołania n=2, i tak dalej.

Aby staranniej sprawdzić nasze rozumowanie, możesz także w wizualizerze prześledzić rekurencyjny kod:

Ten „twist” sprawia, że rekurencja jest wyjątkową wśród funkcji, które widzieliśmy wcześniej, wiele wersji tej funkcji działa jednocześnie. Oznacza to, że jest więcej niż jedna ramka odpowiadająca tej samej funkcji (w wizualizerze). Jest to dość podobne do tego, co widzieliśmy w wizualizerze, gdzie jedna funkcja wywoływała drugą, z tą róznicą, że tu, funkcja wywołująca jest taka sama jak funkcja wywołana. Trzeba jednak pamiętać, że na każdym kroku są używane tak naprawdę tylko zmienne "bieżące" (najnowsza / najniższa ramka) – ramki, które nie są dolne są "wstrzymane" a ich zmienne niedostępne.

Teraz twoja kolej, by napisać kod. Zmodyfikuj funkcję countdown tak, aby liczyła w górę zamiast w dół.

Zadanie na kodowanie : Startujemy
Napisz funkcję rekurencyjną countup(n), która drukuje "Blastoff!" a następnie w kolejnych liniach numery od 1 do n. Wskazówka
Wpisz polecenia takie jak print(mafonction("test-argument")) w polu poniżej.

Następnie zmieńmy nasz program countdown tak, aby liczył w krokach co 2. Na wyjściu powinniśmy otrzymać 5, 3, 1, Blastoff! Zmienimy argument funkcji z n-1 na n-2. Czy coś jeszcze musimy zmienić?

Przykład
Próba odliczania w krokach co 2

Można zauważyć, że ten program nie działa zgodnie z naszymi oczekiwaniami. Drukował 5, 3, 1, tak jak chcemy, ale zamiast tego zatrzymywał się na -1, -3, -5 i dalej biegł w nieskończoność. (A bardziej dokładniej, zabraknie czasu i pamięci, ponieważ każde wywołanie rekurencyjne zajmuje trochę więcej pamięci roboczej, zobacz ten sam przykład w wizualizerze).

Podczas projektowania funkcji rekurencyjnej musimy być ostrożni, aby jej kolejne wywołania nie trwały w nieskończoność! Zmodyfikuj program countdownBy2 tak, aby prawidłowo zatrzymał się na 1 (lub 2, jeśli n jest parzyste) i wydrukował "Blastoff!".

Zadanie na kodowanie : Co dwa
Zmodyfikuj ten program rekurencyjny, aby poprawnie liczyć w krokach co 2.
Wpisz polecenia takie jak print(mafonction("test-argument")) w polu poniżej.

Projektowanie Funkcji Rekurencyjnych

Funkcja rekurencyjna oznacza funkcję, która wywołuje samą siebie. Ale musi istnieć kilka przypadków, kiedy funkcja nie wywołuje samej siebie, bo inaczej, program by działał w nieskończoność, jak widzieliśmy powyżej. Przypadek bazowy stanowi część funkcji rekurencyjnej, w której nie ma wywoływania samej siebie. W powyższym przykładzie dla n<=0 mieliśmy przypadek bazowy. Projektowanie funkcji rekurencyjnej wymaga starannego wybrania przypadku bazowego i upewnienia się, że każda sekwencja wywołań funkcji ostatecznie dotrze do przypadku bazowego.

W następnym ćwiczeniu został już zaprogramowany przypadek bazowy, a twoim zadaniem jest zapisać resztę funkcji rekurencyjnej.

Zadanie na kodowanie : Suma Cyfr
Suma cyfr liczby n jest sumą jej cyfr. Napisz rekurencyjną funkcję digitalSum(n), która pobiera dodatnią liczbę całkowitą n i zwraca sumę jej cyfr. Na przykład, digitalSum(2019) powinien zwrócić 12, ponieważ 2 + 0 + 1 + 9 = 12. Wskazówka #1 Wskazówka #2
Wpisz polecenia takie jak print(mafonction("test-argument")) w polu poniżej.

Teraz zapisz funkcję rekurencyjną, która wywołuje digitalSum jako podprogram.

Zadanie na kodowanie : Cyfra kontrolna
Cyfrowy pierwiastek nieujemnej liczby całkowitej n oblicza się w następujący sposób. Rozpoczynamy od sumy cyfr liczby n. Cyfry powstałego liczby są następnie sumowane, a proces ten trwa do uzyskania jednocyfrowej liczby. Na przykład cyfrowy pierwiastek roku 2019 wynosi 3, ponieważ 2 + 0 + 1 + 9 = 12 i 1 + 2 = 3. Napisz funkcję rekurencyjną digitalRoot(n), która zwraca cyfrę kontrolną n.
Załóż, że do twojego programu została już dostarczona robocza definicja programu digitalSum.
Wpisz polecenia takie jak print(mafonction("test-argument")) w polu poniżej.

Ten niezwykle krótki program oblicza największy wspólny dzielnik dwóch liczb. To jeden z najstarszych znanych algorytmów zwany algorytmem Euklidesa .

Ćwiczenia

Zadanie krótkiej odpowiedzi: NWD
Co uzyskamy na wyjściu następującego rekurencyjnego programu?

def gcd(a, b):
 if b == 0: return a
 return gcd(b, a % b)
print(gcd(20, 12))
Prawidłowa! Ten niezwykle krótki program oblicza największy wspólny dzielnik dwóch liczb. To jeden z najstarszych znanych algorytmów zwany algorytmem Euklidesa.

Zadanie na kodowanie : Grad
Sekwencja gradowa zaczynająca się od dodatniej liczby całkowitej n generowana jest poprzez zastosowanie dwóch prostych reguł. Jeśli n jest parzyste, następna liczba w sekwencji wynosi n/2. Jeśli n jest nieparzyste, następny numer w sekwencji to 3*n+1. Powtarzając ten proces wygenerujemy sekwencję gradową. Napisz rekurencyjną funkcję hailstone(n), która drukuje sekwencję gradową zaczynającą się od n. Niech program zatrzyma się, gdy sekwencja osiągnie liczbę 1 (w przeciwnym wypadku, program zapętli sie i będziemy mieć w kółko: 1, 4, 2, 1, 4, 2, ...)
Na przykład, gdy n=5, Twój program powinien wyprowadzić następującą sekwencję:

5
16
8
4
2
1
Wpisz polecenia takie jak print(mafonction("test-argument")) w polu poniżej.

Matematycy uważają, że każda sekwencja gradowa osiąga ostatecznie 1, niezależnie od tego, od jakiej wartości n zaczniemy. Jednak nikt tego jeszcze nie udowodnił.

Listy Zagnieżdżone

Oto interesujące naturalne zastosowanie rekursji. Zagnieżdżona lista to miejsce, w którym można umieścić kilka list wewnątrz innych; możliwe jest zrobienie tego wielokrotnie. Na przykład, zagnieżdżona lista liczb całkowitych to [[1, 2], [9, 10]], jak również [[1], 2] i x = [[1], 2, [3, [[4]]]]. Ostatni przykład listy zagnieżdżonej to lista trzech elementów: x[0]==[1], aby rozpocząć, a następnie x[1]==2, a następnie x[2]==[3, [[4]]]. (Więc x, postrzegane jako lista o długości 3). Zauważ, że lista jak [1, 2, 3] również liczy się jako zagnieżdżona lista. Czy możemy napisać funkcję, aby znaleźć całkowitą sumę wszystkich zagnieżdżonych list liczb całkowitych? Na przykład, przy wejściu [[5], 2, [10, 8, [[7]]]] należałoby oczekiwać wartości 32.

To zadanie jest trudne dla pętli while lub pętli for, ponieważ chcemy, aby funkcja, która działa na zagnieżdżonych listach z dowolnym kształtem/formatem. Jednak zagnieżdżone listy mają naturalnie rekurencyjną strukturę: lista zagnieżdżona jest listą, z której każdy element jest albo (a) liczbą całkowitą, albo (b) listą zagnieżdżoną. Gdy obliczymy sumę każdej części głównej listy, suma tych wartości będzie sumą całkowitą. Możemy to wyrazić następującym kodem; używając isinstance(x, int), która daje wartość typu boolowskiego informując, czy x jest typu całkowitego (w przeciwieństwie do listy).

Przykład: Sumowanie Listy zagnieżdżonej
Obliczanie sumy elementów w zagnieżdżonej liście z użyciem funkcji rekurencyjnej. Po naciśnięciu przycisku Uruchom program widzimy jego wartość w określonych testach.
Wpisz polecenia takie jak print(mafonction("test-argument")) w polu poniżej.

Rekursja służy do podziału każdej zagnieżdżonej listy na mniejsze części. Na przykład, nestedListSum([1, [3, 4], 5]) wykonuje łącznie 6 wywołań rekurencyjnych: początkowe, potem dla 1, a następnie dla [3, 4], a następnie dla 3, potem dla 4, (przy czym suma [3, 4] jest zwracana jako 7), a na końcu dla 5 (po tym otrzymano łącznie 13). Oto ten sam kod w wizualizerze.

Zadanie na kodowanie : Wyszukiwanie zagnieżdżonej listy
Zapisując coś podobnego do nestedListSum, określ funkcję rekurencyjną

nestedListContains(NL, target)
Która dla zagnieżdżonej listy NL liczb całkowitych oraz całkowitego target wskazuje, czy target znajduje się w dowolnym miejscu w zagnieżdżonej liście. Twój kod powinien zwracać wartość logiczną True, jeśli target znajduje się w zagnieżdżonej liście, a False, jeśli nie jest w niej zawarty.
Na przykład, dla nestedListContains([1, [2, [3], 4]], 3) powinien zwracać wartość True i dla nestedListContains([1, [2, [3], 4]], 5) powinien zwracać False.
Wpisz polecenia takie jak print(mafonction("test-argument")) w polu poniżej.

Linijki

Rekursja jest również związana z fraktalami - obrazy zawierające wiele kopii siebie. Przykładem jest banner na górze tej strony. Następujące ćwiczenie tworzy prosty powtarzający się wzór, który używa rekursji.

Zadanie na uporządkowanie: Fraktalowa linijka
Posortuj linie tak, aby utworzyć program, który tworzy rekurencyjny wzór. Na przykład, gdy n=3 program powinien wyprowadzić następujący wzór.

-
--
-
---
-
--
-
  • ruler(n - 1)
  • ruler(n - 1)
  • else:
  • if n == 1:
  • def ruler(n):
  • print('-')
  • print(n * '-')
Gratulacje! Jesteś gotowy, aby przejść do następnej lekcji.