Kiedy zaczynałem uczyć się seplenienia, natknąłem się na termin rekurencyjny . Co to dokładnie znaczy?
1692
Kiedy zaczynałem uczyć się seplenienia, natknąłem się na termin rekurencyjny . Co to dokładnie znaczy?
Odpowiedzi:
Rozważ prostą funkcję, która dodaje pierwsze N liczb naturalnych. (np
sum(5) = 1 + 2 + 3 + 4 + 5 = 15
.).Oto prosta implementacja JavaScript, która wykorzystuje rekurencję:
Jeśli zadzwoniłeś
recsum(5)
, to właśnie ocenia interpreter JavaScript:Zwróć uwagę, jak każde wywołanie rekurencyjne musi się zakończyć, zanim interpreter JavaScript zacznie faktycznie obliczać sumę.
Oto rekurencyjna wersja tej samej funkcji:
Oto sekwencja zdarzeń, które wystąpiłyby, gdybyś zadzwonił
tailrecsum(5)
(co byłoby efektywnetailrecsum(5, 0)
z powodu domyślnego drugiego argumentu).W przypadku rekursywnego ogona, przy każdej ocenie wezwania rekurencyjnego,
running_total
aktualizacja jest aktualizowana.Uwaga: w oryginalnej odpowiedzi użyto przykładów z Python. Zostały one zmienione na JavaScript, ponieważ interpretery Python nie obsługują optymalizacji wywołania ogona . Jednak chociaż optymalizacja wywołania ogona jest częścią specyfikacji ECMAScript 2015 , większość interpreterów JavaScript nie obsługuje tego .
źródło
tail recursion
można to osiągnąć w języku, który nie optymalizuje połączeń z ogonem.W tradycyjnej rekurencji typowym modelem jest to, że najpierw wykonujesz wywołania rekurencyjne, a następnie bierzesz wartość zwracaną wywołania rekurencyjnego i obliczasz wynik. W ten sposób nie otrzymujesz wyniku obliczeń, dopóki nie wrócisz z każdego połączenia rekurencyjnego.
W rekursji ogona najpierw wykonujesz obliczenia, a następnie wywoływanie rekurencyjne, przekazując wyniki bieżącego kroku do następnego kroku rekurencyjnego. Powoduje to, że ostatnie zdanie ma postać
(return (recursive-function params))
. Zasadniczo wartość zwracana dla dowolnego danego kroku rekurencyjnego jest taka sama, jak wartość zwracana następnego wywołania rekurencyjnego .Konsekwencją tego jest to, że gdy będziesz gotowy do wykonania następnego kroku rekurencyjnego, nie potrzebujesz już bieżącej ramki stosu. Pozwala to na pewną optymalizację. W rzeczywistości przy odpowiednio napisanym kompilatorze nigdy nie powinieneś mieć snickera przepełnienia stosu z rekurencyjnym wywołaniem ogona. Po prostu ponownie użyj bieżącej ramki stosu do następnego kroku rekurencyjnego. Jestem prawie pewien, że Lisp to robi.
źródło
Ważną kwestią jest to, że rekurencja ogona jest zasadniczo równoważna zapętleniu. To nie tylko kwestia optymalizacji kompilatora, ale podstawowy fakt dotyczący ekspresji. To działa w obie strony: możesz wziąć dowolną pętlę formularza
gdzie
E
iQ
są wyrażeniami iS
są sekwencją instrukcji, i zamieniają je w funkcję rekurencji ogonaOczywiście
E
,S
iQ
muszą być zdefiniowane, aby obliczyć jakąś ciekawą wartość w niektórych zmiennych. Na przykład funkcja zapętlaniajest równoważne funkcji (-om) rekurencyjnej
(To „zawijanie” funkcji rekurencyjnej za pomocą funkcji o mniejszej liczbie parametrów jest powszechnym idiomem funkcjonalnym).
źródło
else { return k; }
można zmienić nareturn k;
Ten fragment książki „ Programowanie w Lua” pokazuje, jak zrobić właściwą rekurencję ogona (w Lua, ale powinien również dotyczyć Lispa) i dlaczego jest lepszy.
Widzisz więc, kiedy wykonujesz połączenie rekurencyjne, takie jak:
Nie jest to rekurencyjne, ponieważ po wykonaniu wywołania rekurencyjnego nadal masz do zrobienia (dodaj 1) tę funkcję. Jeśli wprowadzisz bardzo dużą liczbę, prawdopodobnie spowoduje to przepełnienie stosu.
źródło
Używając zwykłej rekurencji, każde wywołanie rekurencyjne wypycha kolejny wpis na stos wywołań. Po zakończeniu rekursji aplikacja musi usunąć wszystkie wpisy z powrotem.
W przypadku rekurencji ogona w zależności od języka kompilator może zwinąć stos do jednego wpisu, co pozwala zaoszczędzić miejsce na stosie ... Duże zapytanie rekurencyjne może w rzeczywistości spowodować przepełnienie stosu.
Zasadniczo rekurencje Tail można zoptymalizować do iteracji.
źródło
Plik żargonu mówi o definicji rekurencji ogona:
rekurencja ogona /n./
Jeśli nie masz tego dość, zobacz rekurencję ogona.
źródło
Zamiast wyjaśniać to słowami, oto przykład. To jest wersja schematu funkcji silniowej:
Oto wersja silnia rekurencyjna:
W pierwszej wersji zauważysz, że rekursywne wywołanie faktu jest podawane do wyrażenia mnożenia, a zatem stan należy zapisać na stosie podczas wykonywania wywołania rekurencyjnego. W wersji z rekursywnym ogonem nie ma innego wyrażenia S oczekującego na wartość wywołania rekurencyjnego, a ponieważ nie trzeba wykonywać żadnej pracy, stan nie musi być zapisywany na stosie. Z reguły funkcje rekurencyjne schematu używają stałej przestrzeni stosu.
źródło
list-reverse
procedura rekursywna lista-mutacja-ogon będzie działała na stałym obszarze stosu, ale utworzy i rozwinie strukturę danych na stercie. Przejście drzewa może użyć symulowanego stosu, w dodatkowym argumencie. itd.Rekurencja ogona odnosi się do ostatniego wywołania rekurencyjnego w ostatniej instrukcji logicznej w algorytmie rekurencyjnym.
Zwykle w rekurencji masz przypadek podstawowy, który zatrzymuje rekurencyjne połączenia i zaczyna wyskakiwać stos wywołań. Aby użyć klasycznego przykładu, choć bardziej C-izh niż Lisp, funkcja silnia ilustruje rekurencję ogona. Wywołanie rekurencyjne następuje po sprawdzeniu stanu podstawowego.
Początkowe wywołanie do silnia byłoby
factorial(n)
gdziefac=1
(wartość domyślna), a n jest liczbą, dla której należy obliczyć silnię.źródło
else
jest krokiem, który możesz nazwać „przypadkiem podstawowym”, ale obejmuje kilka linii. Czy źle cię rozumiem, czy moje założenie jest prawidłowe? Rekurencja ogona jest dobra tylko dla jednej wkładki?factorial
przykład to tylko klasyczny prosty przykład, to wszystko.Oznacza to, że zamiast wciskać wskaźnik instrukcji na stos, możesz po prostu przeskoczyć na górę funkcji rekurencyjnej i kontynuować wykonywanie. Pozwala to funkcjom na powtarzanie się w nieskończoność bez przepełnienia stosu.
Napisałem post na blogu na ten temat, który zawiera graficzne przykłady tego, jak wyglądają ramki stosu.
źródło
Oto krótki fragment kodu porównujący dwie funkcje. Pierwszym z nich jest tradycyjna rekurencja w celu znalezienia silni danej liczby. Drugi wykorzystuje rekurencję ogona.
Bardzo prosty i intuicyjny do zrozumienia.
Łatwym sposobem na stwierdzenie, czy funkcja rekurencyjna jest rekurencyjna, jest zwrócenie konkretnej wartości w przypadku podstawowym. Oznacza to, że nie zwraca 1, prawdy ani niczego podobnego. Prawdopodobnie zwróci jakiś wariant jednego z parametrów metody.
Innym sposobem jest stwierdzenie, czy wywołanie rekurencyjne jest wolne od jakichkolwiek dodatków, arytmetyki, modyfikacji itp. Oznacza to, że jest to zwykłe wywołanie rekurencyjne.
źródło
Najlepszym sposobem na zrozumienie
tail call recursion
jest szczególny przypadek rekurencji, w którym ostatnie wywołanie (lub wywołanie ogonowe) jest samą funkcją.Porównywanie przykładów podanych w Pythonie:
^ REKURSJA
^ ODBICIE OGONU
Jak widać w ogólnej wersji rekurencyjnej, ostatnie wywołanie w bloku kodu to
x + recsum(x - 1)
. Po wywołaniurecsum
metody jest już inna operacjax + ..
.Jednak w wersji rekurencyjnej tail ostatnie wywołanie (lub tail tail) w bloku kodu
tailrecsum(x - 1, running_total + x)
oznacza, że ostatnie wywołanie jest wykonywane do samej metody i po tym nie następuje żadna operacja.Ten punkt jest ważny, ponieważ pokazana tutaj rekurencja ogona nie powoduje powiększenia pamięci, ponieważ gdy leżąca pod nią maszyna wirtualna widzi funkcję wywołującą się w pozycji ogona (ostatnie wyrażenie, które ma być ocenione w funkcji), eliminuje bieżącą ramkę stosu, która jest znany jako Optymalizacja wezwania ogona (TCO).
EDYTOWAĆ
NB Pamiętaj, że powyższy przykład został napisany w języku Python, którego środowisko wykonawcze nie obsługuje TCO. To tylko przykład na wyjaśnienie tego. TCO jest obsługiwany w językach takich jak Scheme, Haskell itp
źródło
W Javie jest możliwa możliwa rekurencyjna implementacja funkcji Fibonacciego:
Porównaj to ze standardową implementacją rekurencyjną:
źródło
iter
doacc
kiedyiter < (n-1)
.Nie jestem programistą Lisp, ale myślę, że to pomoże.
Zasadniczo jest to styl programowania, w którym wywołanie rekurencyjne jest ostatnią rzeczą, którą robisz.
źródło
Oto przykład Common Lisp, który wykonuje silnię przy użyciu rekurencji ogona. Z powodu natury bez stosów można było wykonywać szalenie duże obliczenia czynnikowe ...
A potem dla zabawy możesz spróbować
(format nil "~R" (! 25))
źródło
W skrócie, rekursja ogona ma wywołanie rekurencyjne jako ostatnią instrukcję w funkcji, dzięki czemu nie musi czekać na wywołanie rekurencyjne.
Jest to więc rekurencja ogona, tj. N (x - 1, p * x) jest ostatnią instrukcją w funkcji, w której kompilator jest sprytny, aby dowiedzieć się, że można go zoptymalizować do pętli for (silnia). Drugi parametr p przenosi wartość pośrednią produktu.
Jest to nierekurencyjny sposób pisania powyższej funkcji silni (chociaż niektóre kompilatory C ++ i tak mogą ją zoptymalizować).
ale to nie jest:
Napisałem długi post zatytułowany „ Zrozumienie rekurencji ogona - Visual Studio C ++ - widok zespołu ”
źródło
tutaj jest wersja Perla 5
tailrecsum
wspomnianej wcześniej funkcji.źródło
Jest to fragment Struktury i interpretacji programów komputerowych dotyczący rekurencji ogona.
źródło
Funkcja rekurencyjna jest funkcją, która sama się wywołuje
Pozwala programistom pisać wydajne programy przy użyciu minimalnej ilości kodu .
Minusem jest to, że mogą powodować nieskończone pętle i inne nieoczekiwane wyniki, jeśli nie zostaną poprawnie zapisane .
Wyjaśnię zarówno funkcję Simple Recursive, jak i Tail Recursive
Aby napisać Prostą funkcję rekurencyjną
Z podanego przykładu:
Z powyższego przykładu
Decyduje, kiedy wyjść z pętli
Czy faktyczne przetwarzanie ma zostać wykonane
Pozwólcie, że podzielę to zadanie jeden po drugim dla łatwego zrozumienia.
Zobaczmy, co się stanie wewnętrznie, jeśli uruchomię
fact(4)
If
pętla kończy się niepowodzeniem, więc przechodzi doelse
pętli, więc wraca4 * fact(3)
W pamięci stosu mamy
4 * fact(3)
Podstawienie n = 3
If
pętla zawodzi, więc przechodzi doelse
pętliwięc wraca
3 * fact(2)
Pamiętaj, że nazwaliśmy `` 4 * fakt (3) ''
Dane wyjściowe dla
fact(3) = 3 * fact(2)
Do tej pory stos miał
4 * fact(3) = 4 * 3 * fact(2)
W pamięci stosu mamy
4 * 3 * fact(2)
Podstawienie n = 2
If
pętla zawodzi, więc przechodzi doelse
pętliwięc wraca
2 * fact(1)
Pamiętaj, że zadzwoniliśmy
4 * 3 * fact(2)
Dane wyjściowe dla
fact(2) = 2 * fact(1)
Do tej pory stos miał
4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
W pamięci stosu mamy
4 * 3 * 2 * fact(1)
Podstawienie n = 1
If
pętla jest prawdziwawięc wraca
1
Pamiętaj, że zadzwoniliśmy
4 * 3 * 2 * fact(1)
Dane wyjściowe dla
fact(1) = 1
Do tej pory stos miał
4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
Wreszcie wynik faktu (4) = 4 * 3 * 2 * 1 = 24
Recursion Tail byłoby
If
pętla kończy się niepowodzeniem, więc przechodzi doelse
pętli, więc wracafact(3, 4)
W pamięci stosu mamy
fact(3, 4)
Podstawienie n = 3
If
pętla zawodzi, więc przechodzi doelse
pętliwięc wraca
fact(2, 12)
W pamięci stosu mamy
fact(2, 12)
Podstawienie n = 2
If
pętla zawodzi, więc przechodzi doelse
pętliwięc wraca
fact(1, 24)
W pamięci stosu mamy
fact(1, 24)
Podstawienie n = 1
If
pętla jest prawdziwawięc wraca
running_total
Dane wyjściowe dla
running_total = 24
Wreszcie wynik faktu (4,1) = 24
źródło
Rekurencja ogona to życie, w którym żyjesz teraz. Ciągle przetwarzasz tę samą ramkę stosu w kółko, ponieważ nie ma powodu ani środków, aby powrócić do „poprzedniej” ramki. Przeszłość się skończyła, więc można ją odrzucić. Dostajesz jedną klatkę, która zawsze przenosi się w przyszłość, dopóki proces nieuchronnie umrze.
Analogia jest załamana, gdy weźmie się pod uwagę, że niektóre procesy mogą wykorzystywać dodatkowe ramki, ale nadal uważa się je za rekurencyjne, jeśli stos nie rośnie nieskończenie.
źródło
Rozważ problem obliczania silni liczby.
Prostym podejściem byłoby:
Załóżmy, że wywołujesz silnię (4). Drzewem rekurencyjnym byłoby:
Maksymalna głębokość rekurencji w powyższym przypadku wynosi O (n).
Rozważ jednak następujący przykład:
Drzewo rekurencji dla factTail (4) byłoby:
Tutaj również maksymalna głębokość rekurencji wynosi O (n), ale żadne z wywołań nie dodaje żadnej dodatkowej zmiennej do stosu. Dlatego kompilator może pozbyć się stosu.
źródło
Rekurencja ogona jest dość szybka w porównaniu do normalnej rekurencji. Jest szybki, ponieważ dane wyjściowe wywołania przodków nie będą zapisywane w stosie, aby zachować ścieżkę. Ale w normalnej rekurencji wszystkie wywołania przodka zapisywane są w stosie, aby zachować ścieżkę.
źródło
Funkcja rekurencyjna typu tail jest funkcją rekurencyjną, w której ostatnią operacją wykonywaną przed powrotem jest wywołanie funkcji rekurencyjnej. Oznacza to, że zwracana wartość rekurencyjnego wywołania funkcji jest natychmiast zwracana. Na przykład kod wyglądałby tak:
Kompilatory i interpretery, które wdrażają optymalizację ogona lub eliminację ogona mogą zoptymalizować kod rekurencyjny, aby zapobiec przepełnieniu stosu. Jeśli Twój kompilator lub interpreter nie implementuje optymalizacji wywołania ogona (takiego jak interpreter CPython), pisanie kodu w ten sposób nie przynosi dodatkowych korzyści.
Na przykład jest to standardowa rekurencyjna funkcja silnia w Pythonie:
A to jest rekurencyjna wersja funkcji silniakowej:
(Należy pamiętać, że chociaż jest to kod Python, interpreter CPython nie wykonuje optymalizacji wywołania ogona, więc takie ułożenie kodu nie zapewnia korzyści w czasie wykonywania).
Być może będziesz musiał uczynić kod nieco bardziej nieczytelnym, aby skorzystać z optymalizacji wywołania ogona, jak pokazano na przykładzie silni. (Na przykład przypadek podstawowy jest teraz nieco nieintuicyjny, a
accumulator
parametr jest skutecznie wykorzystywany jako rodzaj zmiennej globalnej).Ale zaletą optymalizacji wywołania ogona jest to, że zapobiega błędom przepełnienia stosu. (Zwrócę uwagę, że tę samą korzyść można uzyskać, stosując algorytm iteracyjny zamiast rekurencyjnego).
Przepełnienie stosu powstaje, gdy stos wywołań ma narzucone zbyt wiele obiektów ramki. Obiekt ramki jest wypychany na stos wywołań, gdy funkcja jest wywoływana, i wyskakuje ze stosu wywołań, gdy funkcja zwraca Obiekty ramki zawierają informacje, takie jak zmienne lokalne i wiersz wiersza kodu, do którego należy wrócić po powrocie funkcji.
Jeśli funkcja rekurencyjna wykonuje zbyt wiele wywołań rekurencyjnych bez zwracania, stos wywołań może przekroczyć limit obiektu ramki. (Liczba zależy od platformy; w Pythonie jest to domyślnie 1000 obiektów ramki.) To powoduje błąd przepełnienia stosu . (Hej, stąd pochodzi nazwa tej strony!)
Jeśli jednak ostatnią rzeczą, którą wykonuje funkcja rekurencyjna, jest wywołanie rekurencyjne i zwrócenie jego wartości zwracanej, nie ma powodu, dla którego musiałby utrzymywać bieżący obiekt ramki w celu pozostania na stosie wywołań. W końcu, jeśli po rekurencyjnym wywołaniu funkcji nie ma kodu, nie ma powodu, aby trzymać się zmiennych lokalnych bieżącego obiektu ramki. Dzięki temu możemy natychmiast pozbyć się bieżącego obiektu ramki, zamiast trzymać go na stosie wywołań. Efektem końcowym jest to, że stos wywołań nie powiększa się, a zatem nie może przepełnić stosu.
Kompilator lub interpreter musi mieć funkcję optymalizacji wywołania ogona jako funkcję, aby mogła rozpoznać, kiedy można zastosować optymalizację wywołania ogona. Nawet wtedy możesz zmienić układ kodu w funkcji rekurencyjnej, aby skorzystać z optymalizacji wywołania ogonowego, i to od Ciebie zależy, czy ten potencjalny spadek czytelności jest wart optymalizacji.
źródło
Aby zrozumieć niektóre podstawowe różnice między rekurencją typu tail-call a rekurencją typu non-tail-tail, możemy zapoznać się z implementacjami tych technik .NET.
Oto artykuł z kilkoma przykładami w C #, F # i C ++ \ CLI: Adventures in Tail Recursion w C #, F # i C ++ \ CLI .
C # nie optymalizuje się pod kątem rekurencji wywołania ogona, podczas gdy F # robi to.
Różnice zasad dotyczą pętli a rachunku Lambda. C # jest zaprojektowany z myślą o pętlach, podczas gdy F # jest zbudowany na zasadach rachunku Lambda. Aby uzyskać bardzo dobrą (i darmową) książkę na temat zasad rachunku Lambda, zobacz: Struktura i interpretacja programów komputerowych autorstwa Abelsona, Sussmana i Sussmana .
Jeśli chodzi o wezwania ogona w F #, bardzo dobry artykuł wprowadzający, zobacz Szczegółowe wprowadzenie do ogonów w F # . Na koniec, oto artykuł, który omawia różnicę między rekurencją bez ogona a rekurencją przywołania ogona (w F #): Rekurencja ogona vs. rekurencja bez ogona w Fis .
Jeśli chcesz przeczytać o niektórych różnicach projektowych rekurencji wywołania ogona między C # i F #, zobacz Generowanie kodu operacji ogona w C # i F # .
Jeśli zależy ci na tym, aby wiedzieć, jakie warunki uniemożliwiają kompilatorowi C # przeprowadzanie optymalizacji wywołania ogonowego, zobacz ten artykuł: Warunki wywołania ogonowego JIT CLR .
źródło
Istnieją dwa podstawowe rodzaje rekurencji: rekurencja głowy i rekurencja.
Zaczerpnięte z tego super niesamowitego postu. Proszę, przeczytaj to.
źródło
Rekurencja oznacza funkcję wywołującą samą siebie. Na przykład:
Rekonstrukcja ogona oznacza rekursję kończącą funkcję:
Widzisz, ostatnią rzeczą, jaką nieskończona funkcja (procedura, w żargonie Schematu) jest wywołanie siebie. Innym (bardziej użytecznym) przykładem jest:
W procedurze pomocnika OSTATNIM, co robi, gdy lewica nie jest zero, jest nazywanie siebie (PO czymś przeciw i coś cdr). Jest to w zasadzie sposób mapowania listy.
Rekurencja ogona ma tę wielką zaletę, że interpreter (lub kompilator, w zależności od języka i dostawcy) może go zoptymalizować i przekształcić w coś równoważnego pętli while. W rzeczywistości, w tradycji schematu, większość pętli „for” i „while” odbywa się w sposób rekurencyjny (o ile wiem, nie ma ani chwili, ani czasu).
źródło
To pytanie ma wiele świetnych odpowiedzi ... ale nie mogę nie poradzić sobie z alternatywnym podejściem do definiowania „rekurencji ogona” lub przynajmniej „właściwej rekurencji ogona”. Mianowicie: czy należy na to patrzeć jako właściwość określonego wyrażenia w programie? A może należy na to spojrzeć jako właściwość implementacji języka programowania ?
Aby uzyskać więcej informacji na temat tego drugiego widoku, istnieje klasyczny papier Willa Clingera „Prawidłowa rekurencja ogona i efektywność przestrzeni” (PLDI 1998), w którym zdefiniowano „właściwą rekurencję ogona” jako właściwość implementacji języka programowania. Definicja jest tak skonstruowana, aby pozwolić na zignorowanie szczegółów implementacji (takich jak to, czy stos wywołań jest faktycznie reprezentowany przez stos środowiska wykonawczego, czy poprzez przydzieloną stertę połączoną listę ramek).
Aby to osiągnąć, wykorzystuje analizę asymptotyczną: nie czasu wykonywania programu, jak się zwykle widzi, ale raczej wykorzystania przestrzeni programowej . W ten sposób wykorzystanie miejsca przez połączoną listę przydzieloną do stosu w porównaniu do stosu wywołań środowiska wykonawczego kończy się asymptotycznie; więc można zignorować ten szczegół implementacji języka programowania (szczegół, który z pewnością ma duże znaczenie w praktyce, ale może trochę zamazać wody, gdy próbuje się ustalić, czy dana implementacja spełnia wymóg „rekurencyjnego ogona właściwości” )
Artykuł warto uważnie przestudiować z kilku powodów:
Daje indukcyjną definicję wyrażeń ogona i wywołań ogona programu. (Taka definicja i dlaczego takie połączenia są ważne, wydaje się być przedmiotem większości innych odpowiedzi tutaj podanych).
Oto te definicje, aby zapewnić smak tekstu:
(rekursywne wywołanie ogona lub, jak mówi gazeta, „wywołanie samo-ogona” jest szczególnym przypadkiem wywołania ogona, w którym procedura jest wywoływana sama).
Zapewnia formalne definicje dla sześciu różnych „maszyn” dla oceny Programu rdzenia, gdzie każda maszyna ma taką samą zaobserwowania zachowanie z wyjątkiem dla asymptotycznej klasie przestrzeń złożoności, że każdy znajduje.
Na przykład po podaniu definicji odpowiednio dla maszyn z: 1. zarządzaniem pamięcią stosu, 2. zbieraniem śmieci, ale bez wywołań ogona, 3. zbieraniem śmieci i wywołaniami ogona, papier kontynuuje pracę z jeszcze bardziej zaawansowanymi strategiami zarządzania pamięcią, takimi jak 4. „rekurencja ogona evlis”, w której środowisko nie musi być zachowane podczas oceny ostatniego argumentu wyrażenia podrzędnego w wywołaniu ogona, 5. zredukowanie środowiska zamknięcia do samych zmiennych zmiennych tego zamknięcia, oraz 6. tak zwana semantyka „bezpieczna dla przestrzeni kosmicznej” zgodnie z definicją Appela i Shao .
Aby udowodnić, że maszyny faktycznie należą do sześciu różnych klas złożoności przestrzeni, w artykule dla każdej porównywanej pary maszyn podano konkretne przykłady programów, które ujawnią asymptotyczne powiększenie przestrzeni na jednej maszynie, ale nie na drugiej.
(Czytając teraz moją odpowiedź, nie jestem pewien, czy udało mi się uchwycić kluczowe punkty artykułu Clingera . Ale niestety nie mogę teraz poświęcić więcej czasu na opracowanie tej odpowiedzi.)
źródło
Wiele osób już wyjaśniło tutaj rekurencję. Chciałbym przytoczyć kilka przemyśleń na temat zalet, jakie rekurencja daje z książki „Współbieżność w .NET, Nowoczesne wzorce programowania współbieżnego i równoległego” Riccardo Terrella:
Oto także kilka interesujących uwag z tej samej książki na temat rekurencji ogona:
źródło