В Частичном тесте для подготовки к GATE возник вопрос:
f(n):
if n is even: f(n) = n/2
else f(n) = f(f(n-1))
Я ответил: «Это прекратится для всех целых чисел», потому что даже для некоторых отрицательных целых чисел это прекратится как ошибка переполнения стека .
Но мой друг не согласился, сказав, что, поскольку это не реализованный код, а просто псевдокод, в случае некоторых отрицательных целых чисел это будет бесконечная рекурсия.
Какой ответ правильный и почему?
algorithms
recursion
pseudocode
прахар лонд
источник
источник
while (true);
не прекратит работу и не вызовет переполнения стека.while(true);
способом, использующим любой стек, определенно не будет иметь смысла . Суть в том, что, если вы сознательно не пошли своим путем, чтобы быть неловким,while(true);
вы не прекратите и не вызовете переполнение стека.Ответы:
Правильный ответ заключается в том, что эта функция не завершается для всех целых чисел (в частности, она не заканчивается на -1). Ваш друг прав, утверждая, что это псевдокод, и псевдокод не завершается при переполнении стека. Псевдокод формально не определен, но идея в том, что он делает то, что говорит на жестяной банке. Если код не говорит «завершиться с ошибкой переполнения стека», то ошибки переполнения стека нет.
Даже если бы это был настоящий язык программирования, правильный ответ все равно был бы «не заканчивается», если только использование стека не является частью определения языка. Большинство языков не определяют поведение программ, которые могут переполнять стек, потому что трудно точно знать, сколько стека будет использовать программа.
Если выполнение кода на реальном интерпретаторе или компиляторе вызывает переполнение стека, во многих языках это несоответствие между формальной семантикой языка и реализацией. Обычно считается, что реализации языка будут делать только то, что может быть сделано на конкретном компьютере с ограниченной памятью. Если программа умирает с переполнением стека, вы должны купить больший компьютер, перекомпилировать систему, если необходимо, для поддержки всей этой памяти, и попытаться снова. Если программа не завершается, возможно, вам придется продолжать делать это вечно.
Даже тот факт, что программа будет или не будет переполнять стек, недостаточно определен, поскольку некоторые оптимизации, такие как оптимизация хвостовых вызовов и запоминание, могут допускать бесконечную цепочку вызовов функций в пространстве стека с постоянной привязкой. В некоторых языковых спецификациях даже требуется, чтобы реализации выполняли оптимизацию хвостового вызова, когда это возможно (это часто встречается в функциональных языках программирования). Для этой функции
f(-1)
расширяется доf(f(-2))
; внешний вызовf
- это хвостовой вызов, поэтому он ничего не помещает в стек, таким образом, онf(-2)
переходит только в стек, и он возвращается-1
, поэтому стек возвращается к тому же состоянию, в котором он находился в начале. Таким образом, с оптимизацией вызовов хвостаf(-1)
в постоянной памяти.источник
let f :: Int -> Int; f n = if even n then n `div` 2 else f (f (n - 1)) in f (-1)
Если мы посмотрим на это с точки зрения языка Си, реализация может заменить код кодом, который дает одинаковый результат во всех случаях, когда оригинал не вызывает неопределенное поведение. Так что это может заменить
с
Теперь для реализации разрешено применять хвостовую рекурсию:
И это зацикливается навсегда, если и только если n = -1.
источник
f(-1)
является неопределенным поведением (реализация может предполагать, что каждый поток либо завершает, либо делает что-то еще в коротком списке действий, которые не выполняет эта функция), поэтому компилятор может фактически делать все, что он хочет в этом кейс!