W częściowym teście na przygotowanie GATE pojawiło się pytanie:
f(n):
if n is even: f(n) = n/2
else f(n) = f(f(n-1))
Odpowiedziałem: „Zakończy się dla wszystkich liczb całkowitych”, ponieważ nawet w przypadku niektórych liczb całkowitych ujemnych zakończy się jako Błąd przepełnienia stosu .
Ale mój przyjaciel nie zgodził się z twierdzeniem, że skoro nie jest to zaimplementowany kod, a tylko pseudokod, w przypadku niektórych ujemnych liczb całkowitych będzie to nieskończona rekurencja.
Która odpowiedź jest prawidłowa i dlaczego?
algorithms
recursion
pseudocode
prakhar londhe
źródło
źródło
while (true);
nie zakończy się, ani, w uzasadniony sposób, nie spowoduje przepełnienia stosu.while(true);
w sposób wykorzystujący dowolny stos, zdecydowanie nie byłoby rozsądne . Chodzi o to, że dopóki celowo nie postawisz się niezręcznie,while(true);
nie zakończy się ani nie spowoduje przepełnienia stosu.Odpowiedzi:
Prawidłowa odpowiedź jest taka, że ta funkcja nie kończy się na wszystkich liczbach całkowitych (w szczególności nie kończy się na -1). Twój przyjaciel ma rację twierdząc, że jest to pseudokod, a pseudokod nie kończy się na przepełnieniu stosu. Pseudokod nie jest formalnie zdefiniowany, ale chodzi o to, że robi to, co jest napisane na puszce. Jeśli kod nie mówi „zakończ z błędem przepełnienia stosu”, oznacza to, że nie ma błędu przepełnienia stosu.
Nawet jeśli byłby to prawdziwy język programowania, poprawną odpowiedzią byłoby nadal „nie kończy się”, chyba że użycie stosu jest częścią definicji języka. Większość języków nie określa zachowania programów, które mogą przepełnić stos, ponieważ trudno jest dokładnie określić, ile stosu zużyje program.
Jeśli uruchomienie kodu na rzeczywistym interpretatorze lub kompilatorze powoduje przepełnienie stosu, w wielu językach, jest to rozbieżność między formalną semantyką języka a implementacją. Zrozumiałe jest, że implementacje języka będą działały tylko na konkretnym komputerze ze skończoną pamięcią. Jeśli program umiera z powodu przepełnienia stosu, należy kupić większy komputer, w razie potrzeby ponownie skompilować system w celu obsługi całej pamięci i spróbować ponownie. Jeśli program się nie kończy, być może będziesz musiał to robić wiecznie.
Nawet fakt, że program przepełni lub nie przepełni stosu, nie jest dobrze zdefiniowany, ponieważ niektóre optymalizacje, takie jak optymalizacja wywołania ogona i zapamiętywanie, mogą pozwolić na nieskończony łańcuch wywołań funkcji w stałej przestrzeni stosu. Niektóre specyfikacje językowe wymagają nawet, aby implementacje przeprowadzały optymalizację wywołania ogonowego, gdy to możliwe (jest to powszechne w funkcjonalnych językach programowania). Dla tej funkcji
f(-1)
rozwija się dof(f(-2))
; zewnętrzne wywołanie dof
jest wywołaniem ogonowym, więc nie wypycha niczego na stos, dlategof(-2)
trafia tylko na stos, a to zwraca-1
, więc stos powraca do tego samego stanu, w jakim był na początku. Tak więc z optymalizacją ogonaf(-1)
pętle na zawsze w stałej pamięci.źródło
let f :: Int -> Int; f n = if even n then n `div` 2 else f (f (n - 1)) in f (-1)
Jeśli spojrzymy na to pod kątem języka C, implementacja może dowolnie zamieniać kod na kod, który daje taki sam wynik we wszystkich przypadkach, w których oryginał nie wywołuje niezdefiniowanego zachowania. Więc może zastąpić
z
Teraz implementacja może zastosować rekurencję ogona:
I to zapętla się na zawsze wtedy i tylko wtedy, gdy n = -1.
źródło
f(-1)
jest niezdefiniowanym zachowaniem (implementacja może zakładać, że każdy wątek kończy działanie lub robi coś innego na krótkiej liście działań, których ta funkcja nie wykonuje), więc kompilator może w rzeczywistości zrobić wszystko, co chce walizka!