Что такое рекурсия и когда ее использовать?

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

Итак, вопрос:

  1. Что такое рекурсия?
  2. Когда я буду использовать рекурсию?
  3. Почему люди не используют рекурсию?

Ответов (25)

В этом потоке есть несколько хороших объяснений рекурсии , этот ответ о том, почему вы не должны использовать ее на большинстве языков. * В большинстве основных реализаций императивного языка (то есть во всех основных реализациях C, C++, Basic, Python , Ruby, Java и C#) итерация намного предпочтительнее рекурсии.

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

  1. в стеке выделяется пространство для аргументов функции и локальных переменных
  2. аргументы функции копируются в это новое пространство
  3. управление переходит к функции
  4. код функции запускается
  5. результат функции копируется в возвращаемое значение
  6. стек перематывается в предыдущую позицию
  7. элемент управления возвращается туда, где была вызвана функция

Выполнение всех этих шагов требует времени, обычно немного больше, чем требуется на итерацию цикла. Однако настоящая проблема находится на шаге №1. Когда многие программы запускаются, они выделяют один кусок памяти для своего стека, а когда у них заканчивается эта память (часто, но не всегда из-за рекурсии), программа вылетает из-за переполнения стека .

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

Существует метод, который могут использовать разработчики языка, называемые оптимизацией хвостового вызова, которая может устранить некоторые классы переполнения стека. Короче говоря: если возвращаемое выражение функции является просто результатом вызова функции, тогда вам не нужно добавлять новый уровень в стек, вы можете повторно использовать текущий для вызываемой функции. К сожалению, немногие императивные языковые реализации имеют встроенную оптимизацию хвостового вызова.

* Я люблю рекурсию. Мой любимый статический язык вообще не использует циклы, рекурсия - единственный способ делать что-то многократно. Я просто не думаю, что рекурсия - это вообще хорошая идея для языков, которые для нее не настроены.

** Между прочим, Марио, типичное имя для вашей функции ArrangeString - «join», и я был бы удивлен, если на выбранном вами языке еще нет ее реализации.

Рассмотрим старую, хорошо известную проблему :

В математике наибольший общий делитель (НОД)… двух или более ненулевых целых чисел - это наибольшее положительное целое число, которое делит числа без остатка.

Определение gcd на удивление простое:

определение gcd

где mod - оператор по модулю (то есть остаток после целочисленного деления).

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

Если вы хотите узнать, почему это работает, см. Статью в Википедии об алгоритме Евклида .

Давайте в качестве примера вычислим gcd (10, 8). Каждый шаг равен предыдущему:

  1. gcd (10, 8)
  2. gcd (10, 10 mod 8)
  3. gcd (8, 2)
  4. gcd (8, 8 mod 2)
  5. gcd (2, 0)
  6. 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))));

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

Поучительно отметить, что каждый вызов метода приводит либо к срабатыванию базового случая, либо к вызову того же метода, параметры которого ближе к базовому (часто называемый рекурсивным вызовом). Если это не так, метод будет работать вечно.

Чтобы вернуться к решенной проблеме: ничего не делайте, все готово.
Чтобы вернуться к открытой проблеме: выполните следующий шаг, а затем повторите все остальное.

  1. Функция, которая вызывает сама себя
  2. Когда функция может быть (легко) разложена на простую операцию плюс ту же функцию для некоторой меньшей части проблемы. Скорее, я должен сказать, что это делает его хорошим кандидатом для рекурсии.
  3. Они делают!

Канонический пример - факториал, который выглядит так:

int fact(int a) 
{
  if(a==1)
    return 1;

  return a*fact(a-1);
}

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

Пользуюсь рекурсией. При чем здесь степень CS ... (кстати, у меня нет)

Общие применения, которые я нашел:

  1. карты сайта - рекурсивный просмотр файловой системы, начиная с корня документа
  2. пауки - сканирование веб-сайта в поисках адреса электронной почты, ссылок и т. д.
  3. ?

Рекурсивная функция - это функция, которая вызывает сама себя. Самая частая причина, по которой я его использую, - это обход древовидной структуры. Например, если у меня есть TreeView с флажками (подумайте об установке новой программы, странице «выберите компоненты для установки»), мне может понадобиться кнопка «проверить все», которая будет примерно такой (псевдокод):

function cmdCheckAllClick {
    checkRecursively(TreeView1.RootNode);
}

function checkRecursively(Node n) {
    n.Checked = True;
    foreach ( n.Children as child ) {
        checkRecursively(child);
    }
}

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

Вам нужно быть немного осторожнее с рекурсией. Если вы попадете в бесконечный рекурсивный цикл, вы получите исключение Stack Overflow :)

Я не могу придумать причину, по которой люди не должны использовать его, когда это необходимо. В одних обстоятельствах это полезно, в других - нет.

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

Вот простой пример: сколько элементов в наборе. (есть способы посчитать лучше, но это хороший простой рекурсивный пример.)

Во-первых, нам понадобятся два правила:

  1. если набор пуст, количество элементов в наборе равно нулю (да!).
  2. если набор не пуст, счетчик равен единице плюс количество элементов в наборе после удаления одного элемента.

Предположим, у вас есть такой набор: [xxx]. посчитаем, сколько там предметов.

  1. набор равен [xxx], который не является пустым, поэтому мы применяем правило 2. количество элементов равно одному плюс количество элементов в [xx] (т. е. мы удалили элемент).
  2. набор равен [xx], поэтому мы снова применяем правило 2: один + количество элементов в [x].
  3. набор равен [x], что по-прежнему соответствует правилу 2: один + количество элементов в [].
  4. Теперь набор равен [], что соответствует правилу 1: счетчик равен нулю!
  5. Теперь, когда мы знаем ответ на шаге 4 (0), мы можем решить шаг 3 (1 + 0)
  6. Аналогичным образом, теперь, когда мы знаем ответ на шаге 3 (1), мы можем решить шаг 2 (1 + 1)
  7. И, наконец, теперь, когда мы знаем ответ на шаге 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)

Есть гораздо больше полезных примеров (например, обход дерева), которые, я уверен, другие люди охватят.

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

Люди избегают рекурсии по ряду причин:

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

  2. Тем из нас, кто нарезал себе зубы на процедурном или объектно-ориентированном программировании, часто советуют избегать рекурсии, потому что она подвержена ошибкам.

  3. Нам часто говорят, что рекурсия медленная. Многократный вызов и возврат из подпрограммы включает в себя большое количество выталкиваний и выталкиваний стека, что медленнее, чем цикл. Я думаю, что некоторые языки справляются с этим лучше, чем другие, и эти языки, скорее всего, не те, в которых доминирующая парадигма является процедурной или объектно-ориентированной.

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

Марио, я не понимаю, почему вы использовали рекурсию для этого примера ... Почему бы просто не перебрать каждую запись? Что-то вроде этого:

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 вещи:

  1. Возьми одно яблоко
  2. Запишите метки подсчета
  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 экземпляра узла. Функции для работы с двоичным деревом поиска также рекурсивны.