Будет ли эта программа завершена для каждого целого числа?

14

В Частичном тесте для подготовки к GATE возник вопрос:

f(n):
     if n is even: f(n) = n/2
     else f(n) = f(f(n-1))

Я ответил: «Это прекратится для всех целых чисел», потому что даже для некоторых отрицательных целых чисел это прекратится как ошибка переполнения стека .

Но мой друг не согласился, сказав, что, поскольку это не реализованный код, а просто псевдокод, в случае некоторых отрицательных целых чисел это будет бесконечная рекурсия.

Какой ответ правильный и почему?

прахар лонд
источник
8
Это не заканчивается для n = -1. В большинстве случаев рассматриваются теоретические пределы.
Глубокий Джоши
9
Если переполнение стека следует рассматривать как завершение, то все программы будут завершены, и это побеждает цель этого вопроса ...
xuq01
10
@ xuq01 while (true);не прекратит работу и не вызовет переполнения стека.
TripeHound
3
@leftaroundabout Я, вероятно, не должен был использовать « на что-нибудь разумное », потому что это совершенно другой уровень « разумного » ... обнаружение и реализация рекурсии хвоста - это хорошо (или даже разумно ), но не делать это лишь слегка » не разумно» ». Все, что реализовано while(true);способом, использующим любой стек, определенно не будет иметь смысла . Суть в том, что, если вы сознательно не пошли своим путем, чтобы быть неловким, while(true);вы не прекратите и не вызовете переполнение стека.
TripeHound
14
@ xuq01 Я не думаю, что «разрушение вселенной» считается решением проблемы остановки.
TripeHound

Ответы:

49

Правильный ответ заключается в том, что эта функция не завершается для всех целых чисел (в частности, она не заканчивается на -1). Ваш друг прав, утверждая, что это псевдокод, и псевдокод не завершается при переполнении стека. Псевдокод формально не определен, но идея в том, что он делает то, что говорит на жестяной банке. Если код не говорит «завершиться с ошибкой переполнения стека», то ошибки переполнения стека нет.

Даже если бы это был настоящий язык программирования, правильный ответ все равно был бы «не заканчивается», если только использование стека не является частью определения языка. Большинство языков не определяют поведение программ, которые могут переполнять стек, потому что трудно точно знать, сколько стека будет использовать программа.

Если выполнение кода на реальном интерпретаторе или компиляторе вызывает переполнение стека, во многих языках это несоответствие между формальной семантикой языка и реализацией. Обычно считается, что реализации языка будут делать только то, что может быть сделано на конкретном компьютере с ограниченной памятью. Если программа умирает с переполнением стека, вы должны купить больший компьютер, перекомпилировать систему, если необходимо, для поддержки всей этой памяти, и попытаться снова. Если программа не завершается, возможно, вам придется продолжать делать это вечно.

Даже тот факт, что программа будет или не будет переполнять стек, недостаточно определен, поскольку некоторые оптимизации, такие как оптимизация хвостовых вызовов и запоминание, могут допускать бесконечную цепочку вызовов функций в пространстве стека с постоянной привязкой. В некоторых языковых спецификациях даже требуется, чтобы реализации выполняли оптимизацию хвостового вызова, когда это возможно (это часто встречается в функциональных языках программирования). Для этой функции f(-1)расширяется до f(f(-2)); внешний вызов f- это хвостовой вызов, поэтому он ничего не помещает в стек, таким образом, он f(-2)переходит только в стек, и он возвращается -1, поэтому стек возвращается к тому же состоянию, в котором он находился в начале. Таким образом, с оптимизацией вызовов хвоста f(-1)в постоянной памяти.

Жиль "ТАК - перестань быть злым"
источник
3
Примером, когда код, переведенный на язык программирования, не приводит к переполнению стека, является Haskell. Это просто зацикливается до бесконечности:let f :: Int -> Int; f n = if even n then n `div` 2 else f (f (n - 1)) in f (-1)
JoL
5

Если мы посмотрим на это с точки зрения языка Си, реализация может заменить код кодом, который дает одинаковый результат во всех случаях, когда оригинал не вызывает неопределенное поведение. Так что это может заменить

f(n):
   if n is even: f(n) = n/2
   else f(n) = f(f(n-1))

с

f(n):
   if n is even: f(n) = n/2
   else f(n) = f((n-1) / 2)

Теперь для реализации разрешено применять хвостовую рекурсию:

f(n):
   while n is not even do n = (n-1) / 2
   f(n) = n/2

И это зацикливается навсегда, если и только если n = -1.

gnasher729
источник
Я думаю, что в C этот вызов f(-1)является неопределенным поведением (реализация может предполагать, что каждый поток либо завершает, либо делает что-то еще в коротком списке действий, которые не выполняет эта функция), поэтому компилятор может фактически делать все, что он хочет в этом кейс!