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

121

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

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

  1. Что такое рекурсия?
  2. Когда я буду использовать рекурсию?
  3. Почему люди не используют рекурсию?
Майк Минутилло
источник
9
И, возможно, это поможет: stackoverflow.com/questions/126756/…
kennytm
3
Это может помочь понять концепцию: перейдите по ссылке, указанной во втором комментарии к вопросу на этой странице, и сделайте то, что говорится в комментариях: stackoverflow.com/questions/3021/…
dtmland

Ответы:

86

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

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

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

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

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

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

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

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

Питер Бернс
источник
1
Приятно видеть объяснение накладных расходов, присущих рекурсии. Я коснулся этого и в своем ответе. Но для меня большая сила рекурсии - это то, что вы можете делать со стеком вызовов. Вы можете написать краткий алгоритм с рекурсией, которая многократно разветвляется, что позволит вам с легкостью выполнять такие вещи, как сканирование иерархий (отношения родитель / потомок). См. Мой ответ для примера.
Steve Wortham
7
Очень разочарован, когда нашел главный ответ на вопрос «Что такое рекурсия и когда мне ее использовать?» на самом деле не отвечаю ни на один из них, не говоря уже о чрезвычайно предвзятом предупреждении о рекурсии, несмотря на его широкое использование на большинстве упомянутых вами языков (нет ничего особенного в том, что вы сказали, но вы, похоже, преувеличиваете проблему и недооцениваете полезность).
Бернхард Баркер
2
Вы, наверное, правы, @Dukeling. Для контекста, когда я писал этот ответ, было уже написано много замечательных объяснений рекурсии, и я написал это с намерением стать дополнением к этой информации, а не главным ответом. На практике, когда мне нужно пройтись по дереву или обработать любую другую вложенную структуру данных, я обычно обращаюсь к рекурсии, и я еще не столкнулся с переполнением стека, созданным мной в дикой природе.
Питер Бернс
63

Простой английский пример рекурсии.

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.
DMin
источник
1
вверх + для трогательного сердца :)
Сухайль Мумтаз Аван
Есть похожая история, похожая на эту, для маленьких детей, которые не уснут в китайских народных сказках, я только что вспомнил эту, и она напоминает мне, как работает рекурсия в реальном мире.
Харви Лин
49

В самом общем смысле информатики рекурсия - это функция, которая вызывает сама себя. Скажем, у вас есть связанная структура списка:

struct Node {
    Node* next;
};

И вы хотите узнать, какой длины есть связанный список, вы можете сделать это с помощью рекурсии:

int length(const Node* list) {
    if (!list->next) {
        return 1;
    } else {
        return 1 + length(list->next);
    }
}

(Это, конечно, можно сделать и с помощью цикла for, но это полезно в качестве иллюстрации концепции)

Андреас Бринк
источник
@Christopher: Это хороший простой пример рекурсии. В частности, это пример хвостовой рекурсии. Однако, как заявил Андреас, его можно легко переписать (более эффективно) с помощью цикла for. Как я объясняю в своем ответе, рекурсия может использоваться лучше.
Стив Уортэм,
2
вам действительно нужно здесь заявление else?
Adrien Be
1
Нет, это только для ясности.
Андреас Бринк
@SteveWortham: Это не хвостовая рекурсия, как написано; length(list->next)все еще нужно вернуться к, length(list)чтобы последний мог добавить 1 к результату. Если бы оно было написано так, чтобы передать всю длину, только тогда мы могли бы забыть о существовании вызывающего. Нравится int length(const Node* list, int count=0) { return (!list) ? count : length(list->next, count + 1); }.
cHao 02
46

Всякий раз, когда функция вызывает саму себя, создавая цикл, возникает рекурсия. Как и все, у рекурсии есть хорошие и плохие применения.

Самый простой пример - хвостовая рекурсия, где самая последняя строка функции - это вызов самой себя:

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);
    }
}

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

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

Вывод

На практике рекурсия имеет наибольший смысл, когда вам нужно итеративное ветвление.

Стив Уортэм
источник
27

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

Канонический пример - это процедура создания факториала числа 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))));

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

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

Wolfbyte
источник
2
Хорошее объяснение, но я думаю, что важно отметить, что это просто хвостовая рекурсия и не дает никаких преимуществ перед итеративным решением. Это примерно такой же объем кода, и он будет работать медленнее из-за накладных расходов на вызов функции.
Steve Wortham
1
@SteveWortham: Это не хвостовая рекурсия. На рекурсивном шаге результат FactRec()должен быть умножен на nперед возвратом.
rvighne
12

Рекурсия - это решение проблемы с функцией, которая вызывает сама себя. Хорошим примером этого является факториальная функция. Факториал - это математическая задача, в которой факториал 5, например, равен 5 * 4 * 3 * 2 * 1. Эта функция решает эту проблему в C # для положительных целых чисел (не проверено - может быть ошибка).

public int Factorial(int n)
{
    if (n <= 1)
        return 1;

    return n * Factorial(n - 1);
}
оборота jkohlhepp
источник
9

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

Например, чтобы вычислить факториал числа X, его можно представить как X times the factorial of X-1. Таким образом, метод «рекурсивно повторяет», чтобы найти факториал X-1, а затем умножает полученное значение, Xчтобы дать окончательный ответ. Конечно, чтобы найти факториал X-1, он сначала вычислит факториал X-2и так далее. Базовым случаем будет X0 или 1, и в этом случае он знает, что нужно вернуть, 1поскольку 0! = 1! = 1.

янтарный
источник
1
Я думаю, что вы имеете в виду не рекурсию, а принцип построения алгоритма <a href=" en.wikipedia.org/wiki/… и Conquer</a>. Посмотрите, например, на <a href = " en.wikipedia. org / wiki / Ackermann_function "> Функция
Акерманса
2
Нет, я не имею в виду D&C. D&C подразумевает, что существует 2 или более подзадач, рекурсия сама по себе не существует (например, приведенный здесь факториальный пример не является D&C - он полностью линейный). D&C - это, по сути, подмножество рекурсии.
Эмбер,
3
Цитата из той статьи, которую вы связали: «Алгоритм« разделяй и властвуй »работает, рекурсивно разбивая проблему на две или более подзадач одного и того же (или родственного) типа»,
Эмбер
Я не думаю, что это хорошее объяснение, поскольку, строго говоря, рекурсия вообще не решает проблему. Вы могли бы просто позвонить себе (и переполнить).
UK-AL
Я использую ваше объяснение в статье, которую пишу для PHP Master, хотя не могу приписать это вам. Надеюсь, ты не против.
морозный изумительный
9

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

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

Определение 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. Рекурсивные случаи сводят на нет проблему, чтобы перейти к простым ответам.

Понимая это, теперь вы можете оценить другие алгоритмы в статье Википедии о рекурсии !

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

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

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

  return a*fact(a-1);
}

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

Луи Бренди
источник
6

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

function cmdCheckAllClick {
    checkRecursively(TreeView1.RootNode);
}

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

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

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

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

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

Blorgbeard отсутствует
источник
5

Рекурсия - это выражение, прямо или косвенно ссылающееся на себя.

Рассмотрим в качестве простого примера рекурсивные сокращения:

  • GNU означает GNU, а не Unix
  • PHP расшифровывается как PHP: препроцессор гипертекста
  • YAML означает YAML Ain't Markup Language.
  • WINE - это аббревиатура от Wine Is Not an Emulator.
  • VISA - это аббревиатура от Visa International Service Association.

Больше примеров в Википедии

Константин
источник
4

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

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

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

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

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

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

Джои де Вилья
источник
4

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

Например, возьмите факториал:

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)

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

Надеюсь это поможет...

Грегори Браун
источник
4

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

Мне также нравится обсуждение Стивом МакКоннеллом рекурсии в Code Complete, где он критикует примеры, используемые в книгах по информатике по рекурсии.

Не используйте рекурсию для факториалов или чисел Фибоначчи

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

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

РЕДАКТИРОВАТЬ: Это не было копанием в ответе Дава - я не видел этого ответа, когда размещал это

drelihan
источник
6
Большая часть причин, по которым факториалы или последовательности Фибоначчи используются в качестве примеров, заключается в том, что они являются общими элементами, которые определяются рекурсивным образом, и, таким образом, они естественным образом поддаются примерам рекурсии для их вычисления - даже если это на самом деле не лучший метод с точки зрения CS.
Эмбер,
Я согласен - я только что обнаружил, когда читал книгу, что это был интересный момент, который нужно поднять в середине раздела о рекурсии
Robben_Ford_Fan_boy
4

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.) Люди используют рекурсию только тогда, когда очень сложно написать итеративный код. Например, методы обхода дерева, такие как предварительный и последующий порядок, могут быть как итеративными, так и рекурсивными. Но обычно мы используем рекурсию из-за ее простоты.

Виниша Вьяса
источник
А как насчет уменьшения сложности при «разделяй и властвуй» в отношении производительности?
mfrachet 03
4

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

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

  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)

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

Марк Харрисон
источник
3

Что ж, у вас есть довольно приличное определение. И в Википедии тоже есть хорошее определение. Так что я добавлю вам еще одно (возможно, худшее) определение.

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

Дэйв Маркл
источник
3

Пример: рекурсивное определение лестницы: Лестница состоит из: - одной ступени и лестницы (рекурсия) - или только одной ступени (завершение)

Ральф
источник
2

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

Питер Бернс
источник
2

Проще говоря: предположим, вы можете делать 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

Процесс повторения одного и того же, пока вы не закончите, называется рекурсией.

Я надеюсь, что это именно тот ответ на "простом английском", который вы ищете!

Бастиан Линдерс
источник
1
Подождите, у меня передо мной на столе много счетных отметок, и теперь я хочу знать, сколько их насчитывается. Можно как-нибудь использовать для этого яблоки?
Кристоффер Хаммарстрём
Если вы берете яблоко с земли (когда вы кладете их туда во время процесса) и кладете его на стол каждый раз, когда вы царапаете одну счетную метку в списке, пока не останется счетных меток, я почти уверен, что вы в итоге на столе окажется количество яблок, равное количеству ваших подсчетных отметок. Теперь просто посчитайте эти яблоки и получите мгновенный успех! (примечание: этот процесс больше не рекурсия, а бесконечный цикл)
Бастиан Линдерс
2

Рекурсивная функция - это функция, которая вызывает саму себя. Рекурсивная структура - это структура, которая содержит экземпляр самой себя. Вы можете объединить два как рекурсивный класс. Ключевой частью рекурсивного элемента является то, что он содержит экземпляр / вызов самого себя.

Представьте, что два зеркала обращены друг к другу. Мы видели аккуратный эффект бесконечности, который они производят. Каждое отражение является экземпляром зеркала, которое содержится в другом экземпляре зеркала и т. Д. Зеркало, содержащее собственное отражение, является рекурсией.

Бинарное дерево является хорошим примером программирования рекурсии. Структура рекурсивна, и каждый узел содержит 2 экземпляра узла. Функции для работы с двоичным деревом поиска также рекурсивны.

mcdon
источник
2

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

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

Таким образом, я часто избегаю рекурсии и вместо этого использую операцию стека, потому что сама рекурсия по сути является операцией стека.

fajrian
источник
1

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

Ник Берарди
источник
1

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

bennybdbc
источник
1

«Если у меня есть молоток, пусть все будет похоже на гвоздь».

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

пример

Предположим, ваш стол завален беспорядочной беспорядком из 1024 бумаг. Как с помощью рекурсии сделать одну аккуратную чистую стопку бумаг из беспорядка?

  1. Разделить: Разложите все листы так, чтобы в каждой «стопке» был только один лист.
  2. Покорять:
    1. Обойдите, кладя каждый лист поверх другого листа. Теперь у вас есть стопки по 2 штуки.
    2. Обойдите, кладя каждую стопку на две стопки. Теперь у вас есть стопки по 4 штуки.
    3. Обойдите, кладя каждую 4 стопки поверх другой 4 стопки. Теперь у вас есть стопки по 8 штук.
    4. ... снова и снова ...
    5. Теперь у вас есть одна огромная стопка из 1024 листов!

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

Андрес Яан Так
источник
6
Вы описываете «разделяй и властвуй». Хотя это пример рекурсии, он ни в коем случае не единственный.
Конрад Рудольф
Это хорошо. Я не пытаюсь здесь описать [мир рекурсии] [1] в предложении. Мне нужно интуитивное объяснение. [1]: facebook.com/pages/Recursion-Fairy/269711978049
Андрес Яан Тэк,
1

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

Шивам
источник
1

эй, извините, если мое мнение с кем-то согласуется, я просто пытаюсь объяснить рекурсию простым английским языком.

Предположим, у вас есть три менеджера - Джек, Джон и Морган. Джек управляет двумя программистами, Джон - тремя, а Морган - 5. Вы собираетесь дать каждому менеджеру по 300 долларов и хотите знать, сколько это будет стоить. Ответ очевиден - а что, если двое сотрудников Моргана также являются менеджерами?

ЗДЕСЬ идет рекурсия. вы начинаете с вершины иерархии. летняя стоимость 0 $. вы начинаете с Джека, а затем проверьте, есть ли у него менеджеры в качестве сотрудников. если вы обнаружите, что кто-то из них есть, проверьте, есть ли у них менеджеры в качестве сотрудников и так далее. Добавляйте 300 $ к летней стоимости каждый раз, когда найдете менеджера. Когда вы закончите с Джеком, идите к Джону, его сотрудникам, а затем к Моргану.

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

Рекурсия - это дерево с ветвями и листьями, которые называются родителями и потомками соответственно. Когда вы используете алгоритм рекурсии, вы более или менее сознательно строите дерево из данных.

Лука Рамишвили
источник
1

На простом английском языке рекурсия означает повторение чего-то снова и снова.

В программировании один пример - вызов функции внутри себя.

Посмотрите на следующий пример вычисления факториала числа:

public int fact(int n)
{
    if (n==0) return 1;
    else return n*fact(n-1)
}
Indigo Praveen
источник
1
Говоря простым языком, повторение чего-либо снова и снова называется итерацией.
toon81
1

Любой алгоритм демонстрирует структурную рекурсию для типа данных, если в основном состоит из оператора switch с регистром для каждого случая типа данных.

например, когда вы работаете над шрифтом

  tree = null 
       | leaf(value:integer) 
       | node(left: tree, right:tree)

структурный рекурсивный алгоритм будет иметь вид

 function computeSomething(x : tree) =
   if x is null: base case
   if x is leaf: do something with x.value
   if x is node: do something with x.left,
                 do something with x.right,
                 combine the results

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

теперь, когда вы смотрите на целые числа (ну, натуральные числа), как они определены с помощью аксиом Пеано

 integer = 0 | succ(integer)

вы видите, что структурный рекурсивный алгоритм для целых чисел выглядит так

 function computeSomething(x : integer) =
   if x is 0 : base case
   if x is succ(prev) : do something with prev

слишком хорошо известная факториальная функция - один из самых тривиальных примеров такой формы.

MFX
источник
1

функция вызывает сама себя или использует собственное определение.

Сайед Тайяб Али
источник