Я задавался вопросом, является ли цикл while по своей сути рекурсией?
Я думаю, это потому, что цикл while можно рассматривать как функцию, которая вызывает себя в конце. Если это не рекурсия, то в чем разница?
Я задавался вопросом, является ли цикл while по своей сути рекурсией?
Я думаю, это потому, что цикл while можно рассматривать как функцию, которая вызывает себя в конце. Если это не рекурсия, то в чем разница?
Ответы:
Петли очень не рекурсивные. Фактически, они являются ярким примером противоположного механизма: итерации .
Точка рекурсии заключается в том, что один элемент обработки вызывает другой экземпляр самого себя. Контроль цикла машина просто прыгает обратно в точку , где она началась.
Перепрыгивание в коде и вызов другого блока кода - это разные операции. Например, когда вы переходите к началу цикла, переменная управления циклом по-прежнему имеет то же значение, что и до перехода. Но если вы вызываете другой экземпляр подпрограммы, в которой вы находитесь, то у нового экземпляра появляются новые, не связанные копии всех его переменных. По сути, одна переменная может иметь одно значение на первом уровне обработки и другое значение на более низком уровне.
Эта возможность крайне важна для работы многих рекурсивных алгоритмов, и именно поэтому вы не можете эмулировать рекурсию с помощью итерации, не управляя также стеком вызываемых фреймов, который отслеживает все эти значения.
Сказать, что X по сути Y, имеет смысл, только если вы имеете в виду некоторую (формальную) систему, в которой вы выражаете X. Если вы определяете семантику while
в терминах лямбда-исчисления, вы можете упомянуть рекурсию *; если вы определите его в терминах регистрационной машины, вы, вероятно, не будете.
В любом случае люди, вероятно, не поймут вас, если вы вызовете рекурсивную функцию только потому, что она содержит цикл while.
* Хотя, возможно, только косвенно, например, если вы определяете его в терминах fold
.
while
рекурсивностью конструкции, как правило, это свойство функций, я просто не могу придумать что-либо еще, чтобы описать его как «рекурсивное» в этом контексте.
Это зависит от вашей точки зрения.
Если вы посмотрите на теорию вычислимости , то итерации и рекурсии одинаково выразительны . Это означает, что вы можете написать функцию, которая что-то вычисляет, и не важно, будете ли вы делать это рекурсивно или итеративно, вы сможете выбрать оба подхода. Нет ничего, что вы можете вычислить рекурсивно, что вы не можете вычислить итеративно и наоборот (хотя внутренняя работа программы может отличаться).
Многие языки программирования не рассматривают рекурсию и итерацию одинаково и по уважительной причине. Обычно рекурсия означает, что язык / компилятор обрабатывает стек вызовов, а итерация означает, что вам, возможно, придется выполнять обработку стека самостоятельно.
Однако есть языки, особенно функциональные, в которых такие вещи, как циклы (for, while) действительно являются лишь синтаксическим сахаром для рекурсии и реализованы таким образом за кулисами. Это часто желательно в функциональных языках, потому что у них обычно нет концепции циклического выполнения, и добавление этого сделало бы их исчисление более сложным по малой практической причине.
Так что нет, по сути они не одинаковы . Они одинаково выразительны , то есть вы не можете вычислять что-то итеративно, вы не можете вычислить рекурсивно и наоборот, но это все в общем случае (согласно тезису Черча-Тьюринга).
Обратите внимание, что мы говорим о рекурсивных программах здесь. Существуют и другие формы рекурсии, например, в структурах данных (например, деревья).
Если вы посмотрите на это с точки зрения реализации , то рекурсия и итерация в значительной степени не совпадают. Рекурсия создает новый кадр стека для каждого вызова. Каждый шаг рекурсии является самодостаточным, получая аргументы для вычисления от вызываемого (самого себя).
Циклы, с другой стороны, не создают фреймы вызовов. Для них контекст не сохраняется на каждом этапе. Для цикла программа просто переходит обратно к началу цикла, пока условие цикла не будет выполнено.
Это очень важно знать, так как это может привести к радикальным изменениям в реальном мире. Для рекурсии весь контекст должен быть сохранен при каждом вызове. Для итерации у вас есть точный контроль над тем, какие переменные находятся в памяти и что сохраняется где.
Если вы посмотрите на это так, вы быстро увидите, что для большинства языков итерации и рекурсии принципиально различны и имеют разные свойства. В зависимости от ситуации, некоторые свойства более желательны, чем другие.
Рекурсия может сделать программы более простыми и легкими для тестирования и проверки . Преобразование рекурсии в итерацию обычно делает код более сложным, увеличивая вероятность сбоя. С другой стороны, преобразование в итерацию и уменьшение количества кадров стека вызовов может сэкономить столь необходимую память.
Разница заключается в неявном стеке и семантике.
Цикл while, который «вызывает себя в конце», не имеет стека для повторного сканирования после завершения. Последняя итерация устанавливает, какое состояние будет в конце.
Однако рекурсия не может быть выполнена без этого неявного стека, который запоминает состояние работы, выполненной ранее.
Это правда, что вы можете решить любую проблему рекурсии с помощью итерации, если вы дадите ей доступ к стеку явно. Но делать это не так.
Семантическое различие связано с тем фактом, что просмотр рекурсивного кода передает идею совершенно иначе, чем итеративный код. Итерационный код делает вещи шаг за шагом. Он принимает любое состояние, которое пришло ранее, и работает только для создания следующего состояния.
Рекурсивный код разбивает проблему на фракталы. Эта маленькая часть выглядит как большая часть, так что мы можем сделать только этот кусочек и этот кусочек одинаково. Это другой способ думать о проблемах. Это очень мощный и требует привыкания. Многое можно сказать в несколько строк. Вы просто не можете получить это из цикла while, даже если у него есть доступ к стеку.
Это все петли на вашем использовании термина искробезопасности . На уровне языка программирования они синтаксически и семантически различны, и у них совершенно разная производительность и использование памяти. Но если вы углубитесь в теорию достаточно глубоко, они могут быть определены в терминах друг друга, и, следовательно, являются «одинаковыми» в некотором теоретическом смысле.
Реальный вопрос: когда имеет смысл проводить различие между итерацией (циклами) и рекурсией, и когда полезно думать о ней как об одних и тех же вещах? Ответ заключается в том, что при программировании (в отличие от написания математических доказательств) важно различать итерацию и рекурсию.
Рекурсия создает новый фрейм стека, то есть новый набор локальных переменных для каждого вызова. Это накладные расходы и занимает место в стеке, что означает, что достаточно глубокая рекурсия может переполнить стек, что приводит к сбою программы. Итерация, с другой стороны, только изменяет существующие переменные, поэтому, как правило, быстрее и занимает только постоянный объем памяти. Так что это очень важное отличие для разработчика!
В языках с рекурсией хвостового вызова (обычно это функциональные языки) компилятор может оптимизировать рекурсивные вызовы таким образом, что они занимают только постоянный объем памяти. В этих языках важным отличием является не итерация против рекурсии, а не-хвостовая-рекурсивная версия-хвостовая-рекурсия и итерация.
Итог: вы должны быть в состоянии заметить разницу, иначе ваша программа потерпит крах.
while
циклы - это форма рекурсии, см., например, принятый ответ на этот вопрос . Они соответствуют µ-оператору в теории вычислимости (см., Например, здесь ).
Все вариации for
циклов, которые повторяются в диапазоне чисел, конечной коллекции, массиве и т. Д., Соответствуют примитивной рекурсии, см., Например, здесь и здесь . Обратите внимание, что for
циклы C, C ++, Java и т. Д. На самом деле являются синтаксическим сахаром для while
цикла, и поэтому они не соответствуют примитивной рекурсии. Цикл Паскаля for
является примером примитивной рекурсии.
Важным отличием является то, что примитивная рекурсия всегда заканчивается, тогда как обобщенная рекурсия ( while
циклы) может не заканчиваться.
РЕДАКТИРОВАТЬ
Некоторые разъяснения по поводу комментариев и других ответов. «Рекурсия возникает, когда вещь определяется в терминах себя или своего типа». (см. википедию ). Так,
Является ли цикл while по своей сути рекурсией?
Поскольку вы можете определить while
цикл в терминах самого себя
while p do c := if p then (c; while p do c))
тогда да , while
цикл - это форма рекурсии. Рекурсивные функции - это еще одна форма рекурсии (еще один пример рекурсивного определения). Списки и деревья - это другие формы рекурсии.
Другой вопрос, который неявно предполагается многими ответами и комментариями:
Являются ли циклы while и рекурсивные функции эквивалентными?
Ответ на этот вопрос - нет : while
цикл соответствует хвостовой рекурсивной функции, где переменные, к которым обращается этот цикл, соответствуют аргументам неявной рекурсивной функции, но, как отмечали другие, не хвостовые рекурсивные функции не может моделироваться while
циклом без использования дополнительного стека.
Таким образом, тот факт, что « while
цикл является формой рекурсии», не противоречит тому факту, что «некоторые рекурсивные функции не могут быть выражены while
циклом».
FOR
циклом может вычислять ровно все примитивные рекурсивные функции, а язык с простым WHILE
циклом может вычислять ровно все µ-рекурсивные функции (и оказывается, что µ-рекурсивные функции - это именно те функции, которые машина Тьюринга может вычислить). Или, если коротко: примитивная рекурсия и µ-рекурсия - это технические термины из математики / теории вычислимости.
Хвост вызов (или хвост рекурсивный вызов) точно реализован как «Гото с аргументами» (без нажатия какого - либо дополнительный кадра вызова на стеке вызовов ) , а в некоторых функциональных языках (Ocaml в частности) является обычным способом зацикливания.
Таким образом, цикл while (в тех языках, где они есть) может рассматриваться как завершающийся хвостовым вызовом его тела (или его тестом головы).
Аналогично, обычные (не хвостовые) рекурсивные вызовы могут моделироваться циклами (используя некоторый стек).
Читайте также о продолжениях и стиле прохождения продолжения .
Таким образом, «рекурсия» и «итерация» глубоко эквивалентны.
Это правда, что и рекурсия, и неограниченные циклы while эквивалентны с точки зрения вычислительной выразительности. То есть любая программа, написанная рекурсивно, может быть переписана в эквивалентную программу с использованием циклов, и наоборот. Оба подхода полны по Тьюрингу , то есть любой может быть использован для вычисления любой вычислимой функции.
Принципиальное различие в программировании заключается в том, что рекурсия позволяет использовать данные, которые хранятся в стеке вызовов. Чтобы проиллюстрировать это, предположим, что вы хотите напечатать элементы односвязного списка, используя цикл или рекурсию. Я буду использовать C для примера кода:
typedef struct List List;
struct List
{
List* next;
int element;
};
void print_list_loop(List* l)
{
List* it = l;
while(it != NULL)
{
printf("Element: %d\n", it->element);
it = it->next;
}
}
void print_list_rec(List* l)
{
if(l == NULL) return;
printf("Element: %d\n", l->element);
print_list_rec(l->next);
}
Просто, правда? Теперь давайте сделаем одну небольшую модификацию: напечатать список в обратном порядке.
Для рекурсивного варианта это почти тривиальная модификация исходной функции:
void print_list_reverse_rec(List* l)
{
if (l == NULL) return;
print_list_reverse_rec(l->next);
printf("Element: %d\n", l->element);
}
Для функции цикла у нас есть проблема. Наш список однозначно связан и поэтому может быть пройден только вперед. Но так как мы печатаем в обратном порядке, мы должны начать печатать последний элемент. Как только мы достигли последнего элемента, мы больше не можем возвращаться ко второму-последнему элементу.
Таким образом, мы либо должны сделать много повторного обхода, либо мы должны создать вспомогательную структуру данных, которая отслеживает посещенные элементы и из которой мы можем затем эффективно печатать.
Почему у нас нет этой проблемы с рекурсией? Потому что в рекурсии у нас уже есть вспомогательная структура данных: стек вызовов функций.
Поскольку рекурсия позволяет нам вернуться к предыдущему вызову рекурсивного вызова, при этом все локальные переменные и состояние для этого вызова остаются неизменными, мы получаем некоторую гибкость, которую было бы утомительно моделировать в итеративном случае.
Циклы - это особая форма рекурсии для достижения конкретной задачи (в основном, итерации). Можно реализовать цикл в рекурсивном стиле с одинаковой производительностью [1] на нескольких языках. и в SICP [2], вы можете видеть, что петли описаны как «синтетический сахар». В большинстве обязательных языков программирования блоки for и while используют ту же область видимости, что и их родительская функция. Тем не менее, в большинстве функциональных языков программирования не существует ни циклов for, ни while, поскольку в них нет необходимости.
Причина, по которой императивные языки имеют циклы for / while, заключается в том, что они обрабатывают состояния, изменяя их. Но на самом деле, если вы смотрите с другой точки зрения, если вы думаете о блоке while как о самой функции, принимающей параметр, обрабатывающей его и возвращающей новое состояние - которое также может быть вызовом одной и той же функции с разными параметрами - вы может думать о цикле как о рекурсии.
Мир также может быть определен как изменчивый или неизменный. если мы определяем мир как набор правил и вызываем конечную функцию, которая принимает все правила, и текущее состояние в качестве параметров, и возвращаем новое состояние в соответствии с этими параметрами, которые имеют ту же функциональность (генерировать следующее состояние в том же Кстати, мы могли бы также сказать, что это рекурсия и цикл.
в следующем примере, функция life is принимает два параметра «rules» и «state», и новое состояние будет построено в следующий раз.
life rules state = life rules new_state
where new_state = construct_state_in_time rules state
[1]: оптимизация хвостовых вызовов - это общая оптимизация в функциональных языках программирования для использования существующего стека функций в рекурсивных вызовах вместо создания нового.
[2]: Структура и интерпретация компьютерных программ, MIT. https://mitpress.mit.edu/books/structure-and-interpretation-computer-programs
Цикл while отличается от рекурсии.
Когда вызывается функция, происходит следующее:
Кадр стека добавляется в стек.
Указатель кода перемещается в начало функции.
Когда цикл while заканчивается, происходит следующее:
Условие спрашивает, правда ли что-то.
Если это так, код переходит в точку.
В общем, цикл while похож на следующий псевдокод:
if (x)
{
Jump_to(y);
}
Самое главное, что рекурсия и циклы имеют разные представления кода сборки и представления машинного кода. Это означает, что они не одинаковы. Они могут иметь одинаковые результаты, но другой машинный код доказывает, что они не на 100% одно и то же.
Одной итерации недостаточно, чтобы быть в целом эквивалентной рекурсии, но итерация со стеком обычно эквивалентна. Любая рекурсивная функция может быть перепрограммирована как итерационный цикл со стеком, и наоборот. Это не означает, что это практично, и в любой конкретной ситуации та или иная форма может иметь явные преимущества по сравнению с другой версией.
Я не уверен, почему это противоречиво. Рекурсия и итерация со стеком - это один и тот же вычислительный процесс. Это одно и то же «явление», так сказать.
Единственное, о чем я могу думать, это то, что, рассматривая их как «инструменты программирования», я бы согласился, что вы не должны думать о них как об одном и том же. Они «математически» или «вычислительно» эквивалентны (опять итерация со стеком , а не итерация в целом), но это не означает, что вы должны подходить к проблемам с мыслью, что любой из них сделает. С точки зрения реализации / решения проблем, некоторые проблемы могут работать лучше так или иначе, и ваша задача как программиста - правильно решить, какая из них лучше подходит.
Чтобы уточнить, ответ на вопрос Является ли цикл while по сути своей рекурсией? является определенным нет , или, по крайней мере, «нет, если у вас также есть стек».