Что такое рекурсия и когда ее использовать?
Одна из тем, которая, кажется, регулярно поднимается в списках рассылки и онлайн-обсуждениях, - это достоинства (или их отсутствие) получения степени по информатике. Аргумент, который, кажется, снова и снова возникает в пользу отрицательной стороны, заключается в том, что они кодировали в течение некоторого количества лет и никогда не использовали рекурсию.
Итак, вопрос:
- Что такое рекурсия?
- Когда я буду использовать рекурсию?
- Почему люди не используют рекурсию?
Ответов (25)25
В этом потоке есть несколько хороших объяснений рекурсии , этот ответ о том, почему вы не должны использовать ее на большинстве языков. * В большинстве основных реализаций императивного языка (то есть во всех основных реализациях C, C++, Basic, Python , Ruby, Java и C#) итерация намного предпочтительнее рекурсии.
Чтобы понять, почему, пройдите по шагам, которые используются в вышеперечисленных языках для вызова функции:
- в стеке выделяется пространство для аргументов функции и локальных переменных
- аргументы функции копируются в это новое пространство
- управление переходит к функции
- код функции запускается
- результат функции копируется в возвращаемое значение
- стек перематывается в предыдущую позицию
- элемент управления возвращается туда, где была вызвана функция
Выполнение всех этих шагов требует времени, обычно немного больше, чем требуется на итерацию цикла. Однако настоящая проблема находится на шаге №1. Когда многие программы запускаются, они выделяют один кусок памяти для своего стека, а когда у них заканчивается эта память (часто, но не всегда из-за рекурсии), программа вылетает из-за переполнения стека .
Таким образом, в этих языках рекурсия медленнее и делает вас уязвимыми для сбоев. Тем не менее, есть некоторые аргументы в пользу его использования. В общем, рекурсивный код становится короче и немного элегантнее, если вы знаете, как его читать.
Существует метод, который могут использовать разработчики языка, называемые оптимизацией хвостового вызова, которая может устранить некоторые классы переполнения стека. Короче говоря: если возвращаемое выражение функции является просто результатом вызова функции, тогда вам не нужно добавлять новый уровень в стек, вы можете повторно использовать текущий для вызываемой функции. К сожалению, немногие императивные языковые реализации имеют встроенную оптимизацию хвостового вызова.
* Я люблю рекурсию. Мой любимый статический язык вообще не использует циклы, рекурсия - единственный способ делать что-то многократно. Я просто не думаю, что рекурсия - это вообще хорошая идея для языков, которые для нее не настроены.
** Между прочим, Марио, типичное имя для вашей функции ArrangeString - «join», и я был бы удивлен, если на выбранном вами языке еще нет ее реализации.
Рассмотрим старую, хорошо известную проблему :
В математике наибольший общий делитель (НОД)… двух или более ненулевых целых чисел - это наибольшее положительное целое число, которое делит числа без остатка.
Определение gcd на удивление простое:
где mod - оператор по модулю (то есть остаток после целочисленного деления).
На английском языке это определение гласит, что наибольший общий делитель любого числа и ноль - это это число, а наибольший общий делитель двух чисел m и n является наибольшим общим делителем n и остатка после деления m на n .
Если вы хотите узнать, почему это работает, см. Статью в Википедии об алгоритме Евклида .
Давайте в качестве примера вычислим gcd (10, 8). Каждый шаг равен предыдущему:
- gcd (10, 8)
- gcd (10, 10 mod 8)
- gcd (8, 2)
- gcd (8, 8 mod 2)
- gcd (2, 0)
- 2
На первом этапе 8 не равно нулю, поэтому применяется вторая часть определения. 10 mod 8 = 2, потому что 8 переходит в 10 один раз с остатком 2. На шаге 3 вторая часть применяется снова, но на этот раз 8 mod 2 = 0, потому что 2 делит 8 без остатка. На шаге 5 второй аргумент равен 0, поэтому ответ равен 2.
Вы заметили, что gcd появляется как слева, так и справа от знака равенства? Математик сказал бы, что это определение рекурсивно, потому что определяемое вами выражение повторяется внутри его определения.
Рекурсивные определения обычно выглядят элегантно. Например, рекурсивное определение суммы списка:
sum l =
if empty(l)
return 0
else
return head(l) + sum(tail(l))
где head
- первый элемент в списке и tail
остальная часть списка. Обратите внимание, что sum
повторяется внутри определения в конце.
Возможно, вы вместо этого предпочтете максимальное значение в списке:
max l =
if empty(l)
error
elsif length(l) = 1
return head(l)
else
tailmax = max(tail(l))
if head(l) > tailmax
return head(l)
else
return tailmax
Вы можете определить умножение неотрицательных целых чисел рекурсивно, чтобы превратить его в серию сложений:
a * b =
if b = 0
return 0
else
return a + (a * (b - 1))
Если этот рассказ о преобразовании умножения в серию сложений не имеет смысла, попробуйте расширить несколько простых примеров, чтобы увидеть, как это работает.
Сортировка слиянием имеет прекрасное рекурсивное определение:
sort(l) =
if empty(l) or length(l) = 1
return l
else
(left,right) = split l
return merge(sort(left), sort(right))
Рекурсивные определения есть повсюду, если вы знаете, что искать. Обратите внимание, что все эти определения имеют очень простые базовые случаи, например , gcd (m, 0) = m. Рекурсивные случаи сводят на нет проблему, чтобы перейти к простым ответам.
С этим пониманием теперь вы можете оценить другие алгоритмы в статье Википедии о рекурсии !
Простой английский пример рекурсии.
A child couldn't sleep, so her mother told her a story about a little frog,
who couldn't sleep, so the frog's mother told her a story about a little bear,
who couldn't sleep, so the bear's mother told her a story about a little weasel...
who fell asleep.
...and the little bear fell asleep;
...and the little frog fell asleep;
...and the child fell asleep.
1.) Метод рекурсивен, если он может вызывать сам себя; либо напрямую:
void f() {
... f() ...
}
или косвенно:
void f() {
... g() ...
}
void g() {
... f() ...
}
2.) Когда использовать рекурсию
Q: Does using recursion usually make your code faster?
A: No.
Q: Does using recursion usually use less memory?
A: No.
Q: Then why use recursion?
A: It sometimes makes your code much simpler!
3.) Люди используют рекурсию только тогда, когда очень сложно написать итеративный код. Например, методы обхода дерева, такие как предварительный заказ и последующий порядок, могут быть как итеративными, так и рекурсивными. Но обычно мы используем рекурсивный метод из-за его простоты.
Всякий раз, когда функция вызывает саму себя, создавая цикл, возникает рекурсия. Как и все остальное, у рекурсии есть хорошие и плохие применения.
Самый простой пример - хвостовая рекурсия, где самая последняя строка функции - это вызов самой себя:
int FloorByTen(int num)
{
if (num % 10 == 0)
return num;
else
return FloorByTen(num-1);
}
Однако это неубедительный, почти бессмысленный пример, потому что его можно легко заменить более эффективной итерацией. В конце концов, рекурсия страдает от накладных расходов на вызов функции, которые в приведенном выше примере могут быть существенными по сравнению с операцией внутри самой функции.
Таким образом, вся причина, по которой нужно делать рекурсию, а не итерацию, должна заключаться в том, чтобы воспользоваться стеком вызовов для выполнения некоторых умных вещей. Например, если вы вызываете функцию несколько раз с разными параметрами внутри одного цикла, то это способ выполнить ветвление . Классический пример - треугольник Серпинского .
Вы можете очень просто нарисовать один из них с помощью рекурсии, где стек вызовов разветвляется в трех направлениях:
private void BuildVertices(double x, double y, double len)
{
if (len > 0.002)
{
mesh.Positions.Add(new Point3D(x, y + len, -len));
mesh.Positions.Add(new Point3D(x - len, y - len, -len));
mesh.Positions.Add(new Point3D(x + len, y - len, -len));
len *= 0.5;
BuildVertices(x, y + len, len);
BuildVertices(x - len, y - len, len);
BuildVertices(x + len, y - len, len);
}
}
Если вы попытаетесь сделать то же самое с итерацией, я думаю, вы обнаружите, что для выполнения требуется намного больше кода.
Другие распространенные варианты использования могут включать обход иерархий, например поисковые роботы веб-сайтов, сравнение каталогов и т. Д.
Заключение
С практической точки зрения, рекурсия имеет наибольший смысл, когда вам нужно итеративное ветвление.
Рекурсия - это метод решения проблем, основанный на менталитете «разделяй и властвуй». Основная идея состоит в том, что вы берете исходную проблему и разделяете ее на более мелкие (более легко решаемые) экземпляры самой себя, решаете эти более мелкие экземпляры (обычно снова используя тот же алгоритм), а затем повторно собираете их в окончательное решение.
Канонический пример - это процедура создания факториала числа n. Факториал n вычисляется путем умножения всех чисел от 1 до n. Итеративное решение на C# выглядит так:
public int Fact(int n)
{
int fact = 1;
for( int i = 2; i <= n; i++)
{
fact = fact * i;
}
return fact;
}
В итеративном решении нет ничего удивительного, и оно должно иметь смысл для всех, кто знаком с C#.
Рекурсивное решение находится путем распознавания того, что n-й факториал равен n * Fact (n-1). Или, другими словами, если вы знаете, что такое конкретное факториальное число, вы можете вычислить следующее. Вот рекурсивное решение на C#:
public int FactRec(int n)
{
if( n < 2 )
{
return 1;
}
return n * FactRec( n - 1 );
}
Первая часть этой функции известна как базовый случай (или иногда защитная оговорка), и именно она не позволяет алгоритму работать вечно. Он просто возвращает значение 1 всякий раз, когда функция вызывается со значением 1 или меньше. Вторая часть более интересна и известна как рекурсивный шаг . Здесь мы вызываем тот же метод с немного измененным параметром (мы уменьшаем его на 1), а затем умножаем результат на нашу копию n.
При первом обнаружении это может сбить с толку, поэтому поучительно изучить, как это работает при запуске. Представьте, что мы вызываем FactRec (5). Мы входим в рутину, базовый случай не подхватывает нас, и в итоге мы получаем следующее:
// In FactRec(5)
return 5 * FactRec( 5 - 1 );
// which is
return 5 * FactRec(4);
Если мы повторно войдем в метод с параметром 4, мы снова не остановимся на предложении защиты, и поэтому мы окажемся на:
// In FactRec(4)
return 4 * FactRec(3);
Если мы заменим это возвращаемое значение на возвращаемое значение выше, мы получим
// In FactRec(5)
return 5 * (4 * FactRec(3));
Это должно дать вам представление о том, как прийти к окончательному решению, поэтому мы быстро проследим и покажем каждый шаг на пути вниз:
return 5 * (4 * FactRec(3));
return 5 * (4 * (3 * FactRec(2)));
return 5 * (4 * (3 * (2 * FactRec(1))));
return 5 * (4 * (3 * (2 * (1))));
Эта последняя замена происходит, когда запускается базовый вариант. На данный момент у нас есть простая алгебраическая формула для решения, которая, в первую очередь, напрямую соответствует определению факториалов.
Поучительно отметить, что каждый вызов метода приводит либо к срабатыванию базового случая, либо к вызову того же метода, параметры которого ближе к базовому (часто называемый рекурсивным вызовом). Если это не так, метод будет работать вечно.
- Функция, которая вызывает сама себя
- Когда функция может быть (легко) разложена на простую операцию плюс ту же функцию для некоторой меньшей части проблемы. Скорее, я должен сказать, что это делает его хорошим кандидатом для рекурсии.
- Они делают!
Канонический пример - факториал, который выглядит так:
int fact(int a)
{
if(a==1)
return 1;
return a*fact(a-1);
}
В общем, рекурсия не обязательно быстрая (накладные расходы на вызов функции имеют тенденцию быть высокими, потому что рекурсивные функции имеют тенденцию быть небольшими, см. Выше) и могут страдать от некоторых проблем (у кого-нибудь есть переполнение стека?). Некоторые говорят, что их трудно получить «правильно» в нетривиальных случаях, но я на это не верю. В некоторых ситуациях рекурсия имеет наибольший смысл и является наиболее элегантным и понятным способом написания конкретной функции. Следует отметить, что некоторые языки предпочитают рекурсивные решения и намного больше их оптимизируют (на ум приходит LISP).
Рекурсивная функция - это функция, которая вызывает сама себя. Самая частая причина, по которой я его использую, - это обход древовидной структуры. Например, если у меня есть TreeView с флажками (подумайте об установке новой программы, странице «выберите компоненты для установки»), мне может понадобиться кнопка «проверить все», которая будет примерно такой (псевдокод):
function cmdCheckAllClick {
checkRecursively(TreeView1.RootNode);
}
function checkRecursively(Node n) {
n.Checked = True;
foreach ( n.Children as child ) {
checkRecursively(child);
}
}
Итак, вы можете видеть, что checkRecursively сначала проверяет переданный узел, а затем вызывает себя для каждого из дочерних узлов этого узла.
Вам нужно быть немного осторожнее с рекурсией. Если вы попадете в бесконечный рекурсивный цикл, вы получите исключение Stack Overflow :)
Я не могу придумать причину, по которой люди не должны использовать его, когда это необходимо. В одних обстоятельствах это полезно, в других - нет.
Я думаю, что из-за того, что это интересный метод, некоторые программисты, возможно, в конечном итоге будут использовать его чаще, чем следовало бы, без реального оправдания. Это дало рекурсию плохую репутацию в некоторых кругах.
Вот простой пример: сколько элементов в наборе. (есть способы посчитать лучше, но это хороший простой рекурсивный пример.)
Во-первых, нам понадобятся два правила:
- если набор пуст, количество элементов в наборе равно нулю (да!).
- если набор не пуст, счетчик равен единице плюс количество элементов в наборе после удаления одного элемента.
Предположим, у вас есть такой набор: [xxx]. посчитаем, сколько там предметов.
- набор равен [xxx], который не является пустым, поэтому мы применяем правило 2. количество элементов равно одному плюс количество элементов в [xx] (т. е. мы удалили элемент).
- набор равен [xx], поэтому мы снова применяем правило 2: один + количество элементов в [x].
- набор равен [x], что по-прежнему соответствует правилу 2: один + количество элементов в [].
- Теперь набор равен [], что соответствует правилу 1: счетчик равен нулю!
- Теперь, когда мы знаем ответ на шаге 4 (0), мы можем решить шаг 3 (1 + 0)
- Аналогичным образом, теперь, когда мы знаем ответ на шаге 3 (1), мы можем решить шаг 2 (1 + 1)
- И, наконец, теперь, когда мы знаем ответ на шаге 2 (2), мы можем решить шаг 1 (1 + 2) и получить количество элементов в [xxx], которое равно 3. Ура!
Мы можем представить это как:
count of [x x x] = 1 + count of [x x]
= 1 + (1 + count of [x])
= 1 + (1 + (1 + count of []))
= 1 + (1 + (1 + 0)))
= 1 + (1 + (1))
= 1 + (2)
= 3
При применении рекурсивного решения у вас обычно есть как минимум 2 правила:
- базис, простой случай, который показывает, что происходит, когда вы "израсходовали" все свои данные. Обычно это вариант ответа «если у вас закончились данные для обработки, ваш ответ - X».
- рекурсивное правило, в котором говорится, что произойдет, если у вас все еще есть данные. Обычно это какое-то правило, которое гласит: «Сделайте что-нибудь, чтобы уменьшить ваш набор данных, и повторно примените свои правила к меньшему набору данных».
Если перевести приведенное выше в псевдокод, мы получим:
numberOfItems(set)
if set is empty
return 0
else
remove 1 item from set
return 1 + numberOfItems(set)
Есть гораздо больше полезных примеров (например, обход дерева), которые, я уверен, другие люди охватят.
Рекурсия лучше всего работает с тем, что я называю «фрактальными проблемами», когда вы имеете дело с большой проблемой, состоящей из меньших версий этой большой вещи, каждая из которых является еще меньшей версией большой вещи, и так далее. Если вам когда-либо придется проходить или искать что-то вроде дерева или вложенных идентичных структур, у вас есть проблема, которая может быть хорошим кандидатом для рекурсии.
Люди избегают рекурсии по ряду причин:
Большинство людей (в том числе и я) нарезают свои зубы программированием именно на процедурном или объектно-ориентированном программировании, а не на функциональном программировании. Таким людям итеративный подход (обычно с использованием циклов) кажется более естественным.
Тем из нас, кто нарезал себе зубы на процедурном или объектно-ориентированном программировании, часто советуют избегать рекурсии, потому что она подвержена ошибкам.
Нам часто говорят, что рекурсия медленная. Многократный вызов и возврат из подпрограммы включает в себя большое количество выталкиваний и выталкиваний стека, что медленнее, чем цикл. Я думаю, что некоторые языки справляются с этим лучше, чем другие, и эти языки, скорее всего, не те, в которых доминирующая парадигма является процедурной или объектно-ориентированной.
По крайней мере, для пары языков программирования, которые я использовал, я помню, как слышал рекомендации не использовать рекурсию, если она выходит за пределы определенной глубины, потому что ее стек не такой глубокий.
Марио, я не понимаю, почему вы использовали рекурсию для этого примера ... Почему бы просто не перебрать каждую запись? Что-то вроде этого:
String ArrangeString(TStringList* items, String separator)
{
String result = items->Strings[0];
for (int position=1; position < items->count; position++) {
result += separator + items->Strings[position];
}
return result;
}
Вышеупомянутый метод будет быстрее и проще. Нет необходимости использовать рекурсию вместо простого цикла. Я думаю, что именно на таких примерах рекурсия получает плохую репутацию. Даже пример канонической факториальной функции лучше реализовать с помощью цикла.
Я создал рекурсивную функцию для объединения списка строк с разделителем между ними. Я использую его в основном для создания выражений SQL, передавая список полей в качестве элементов » и запятая + пробел » в качестве разделителя. Вот функция (она использует некоторые собственные типы данных Borland Builder, но может быть адаптирована для любой другой среды):
String ArrangeString(TStringList* items, int position, String separator)
{
String result;
result = items->Strings[position];
if (position <= items->Count)
result += separator + ArrangeString(items, position + 1, separator);
return result;
}
Я называю это так:
String columnsList;
columnsList = ArrangeString(columns, 0, ", ");
Представьте, что у вас есть массив с именем fields » с данными внутри: albumName », releaseDate », labelId ». Затем вы вызываете функцию:
ArrangeString(fields, 0, ", ");
Когда функция начинает работать, переменная result получает значение нулевой позиции в массиве, которым является albumName .
Затем он проверяет, является ли позиция, с которой он имеет дело, последней. Поскольку это не так, он объединяет результат с разделителем и результат функции, которая, о боже, является той же самой функцией. Но на этот раз проверьте это, он вызывает сам себя, добавляя 1 к позиции.
ArrangeString(fields, 1, ", ");
Он продолжает повторяться, создавая стопку LIFO, пока не достигнет точки, в которой обрабатываемая позиция ЯВЛЯЕТСЯ последней, поэтому функция возвращает только элемент в этой позиции в списке, больше не объединяясь. Затем стопку соединяют в обратном направлении.
Понятно? Если нет, у меня есть другой способ объяснить это. : o)
На самом деле лучшим рекурсивным решением для факториала должно быть:
int factorial_accumulate(int n, int accum) {
return (n < 2 ? accum : factorial_accumulate(n - 1, n * accum));
}
int factorial(int n) {
return factorial_accumulate(n, 1);
}
Поскольку эта версия является хвостовой рекурсивной
В самом общем смысле информатики рекурсия - это функция, которая вызывает сама себя. Скажем, у вас есть связанная структура списка:
struct Node {
Node* next;
};
И вы хотите узнать, какой длины связан список, вы можете сделать это с помощью рекурсии:
int length(const Node* list) {
if (!list->next) {
return 1;
} else {
return 1 + length(list->next);
}
}
(Это, конечно, можно сделать и с помощью цикла for, но это полезно в качестве иллюстрации концепции)
Мне нравится это определение:
в рекурсии подпрограмма сама решает небольшую часть проблемы, делит проблему на более мелкие части, а затем вызывает себя для решения каждой из более мелких частей.
Мне также нравится обсуждение Стивом МакКоннеллом рекурсии в Code Complete, где он критикует примеры, используемые в книгах по информатике по рекурсии.
Не используйте рекурсию для факториалов или чисел Фибоначчи
Одна проблема с учебниками по информатике состоит в том, что они представляют глупые примеры рекурсии. Типичные примеры - вычисление факториала или вычисление последовательности Фибоначчи. Рекурсия - мощный инструмент, и использовать его в любом из этих случаев действительно глупо. Если бы программист, который работал на меня, использовал рекурсию для вычисления факториала, я бы нанял кого-нибудь другого.
Я подумал, что это очень интересный вопрос, который может быть причиной того, что рекурсию часто неправильно понимают.
РЕДАКТИРОВАТЬ: Это не было копанием в ответе Дава - я не видел этого ответа, когда размещал это
Что ж, у вас есть довольно приличное определение. И в Википедии тоже есть хорошее определение. Поэтому я добавлю вам другое (возможно, худшее) определение.
Когда люди говорят о «рекурсии», они обычно говорят о написанной ими функции, которая вызывает себя неоднократно, пока не завершит свою работу. Рекурсия может быть полезна при обходе иерархий в структурах данных.
Рекурсия относится к методу, который решает проблему, решая меньшую версию проблемы, а затем используя этот результат плюс некоторые другие вычисления для формулирования ответа на исходную проблему. Часто в процессе решения меньшей версии метод решает еще меньшую версию проблемы и так далее, пока не будет достигнут «базовый случай», который легко решить.
Например, чтобы вычислить факториал числа X
, можно представить его как X times the factorial of X-1
. Таким образом, метод «рекурсивно повторяет», чтобы найти факториал X-1
, а затем умножает полученное значение, X
чтобы дать окончательный ответ. Конечно, чтобы найти факториал X-1
, он сначала вычислит факториал X-2
и так далее. В базовом случае будет X
0 или 1, и в этом случае он знает, что нужно вернуть, 1
поскольку 0! = 1! = 1
.
Рекурсия - это решение проблемы с функцией, которая вызывает сама себя. Хорошим примером этого является факториальная функция. Факториал - это математическая задача, где факториал 5, например, равен 5 * 4 * 3 * 2 * 1. Эта функция решает эту проблему в C# для положительных целых чисел (не проверено - может быть ошибка).
public int Factorial(int n)
{
if (n <= 1)
return 1;
return n * Factorial(n - 1);
}
Рекурсия - это выражение, прямо или косвенно ссылающееся на себя.
Рассмотрим в качестве простого примера рекурсивные аббревиатуры:
- GNU означает GNU, а не Unix
- PHP означает PHP: препроцессор гипертекста.
- YAML означает YAML Ain't Markup Language.
- WINE - это аббревиатура от Wine Is Not an Emulator.
- VISA - это аббревиатура от Visa International Service Association.
Рекурсивный оператор - это оператор, в котором вы определяете процесс того, что делать дальше, как комбинацию входных данных и того, что вы уже сделали.
Например, возьмите факториал:
factorial(6) = 6*5*4*3*2*1
Но легко увидеть, что факториал (6) также:
6 * factorial(5) = 6*(5*4*3*2*1).
Итак, в общем:
factorial(n) = n*factorial(n-1)
Конечно, сложность рекурсии состоит в том, что если вы хотите определить вещи в терминах того, что вы уже сделали, нужно с чего начать.
В этом примере мы просто создаем особый случай, определяя factorial (1) = 1.
Теперь мы видим это снизу вверх:
factorial(6) = 6*factorial(5)
= 6*5*factorial(4)
= 6*5*4*factorial(3) = 6*5*4*3*factorial(2) = 6*5*4*3*2*factorial(1) = 6*5*4*3*2*1
Поскольку мы определили factorial (1) = 1, мы достигаем «дна».
Вообще говоря, рекурсивные процедуры состоят из двух частей:
1) Рекурсивная часть, которая определяет некоторую процедуру с точки зрения новых входных данных в сочетании с тем, что вы «уже сделали» с помощью той же процедуры. (т.е. factorial(n) = n*factorial(n-1)
)
2) Базовая часть, которая гарантирует, что процесс не повторяется вечно, давая ему место для начала (т.е. factorial(1) = 1
)
Сначала может показаться немного запутанным, но просто посмотрите на кучу примеров, и все должно получиться. Если вы хотите глубже понять эту концепцию, изучите математическую индукцию. Также имейте в виду, что некоторые языки оптимизированы для рекурсивных вызовов, а другие - нет. Довольно легко сделать безумно медленные рекурсивные функции, если вы не будете осторожны, но есть также методы, чтобы сделать их работоспособными в большинстве случаев.
Надеюсь это поможет...
Проще говоря: предположим, вы можете делать 3 вещи:
- Возьми одно яблоко
- Запишите метки подсчета
- Подсчет отметок
Перед вами на столе много яблок, и вы хотите знать, сколько там яблок.
start
Is the table empty?
yes: Count the tally marks and cheer like it's your birthday!
no: Take 1 apple and put it aside
Write down a tally mark
goto start
Процесс повторения одного и того же, пока вы не закончите, называется рекурсией.
Я надеюсь, что это именно тот ответ на "простом английском", который вы ищете!
Рекурсивная функция - это функция, которая вызывает саму себя. Рекурсивная структура - это структура, которая содержит экземпляр самой себя. Вы можете объединить два как рекурсивный класс. Ключевой частью рекурсивного элемента является то, что он содержит экземпляр / вызов самого себя.
Представьте, что два зеркала обращены друг к другу. Мы видели аккуратный эффект бесконечности, который они производят. Каждое отражение является экземпляром зеркала, которое содержится в другом экземпляре зеркала и т. Д. Зеркало, содержащее собственное отражение, является рекурсией.
Бинарное дерево является хорошим примером программирования рекурсии. Структура рекурсивна, и каждый узел содержит 2 экземпляра узла. Функции для работы с двоичным деревом поиска также рекурсивны.