Понимание рекурсии

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

Я всю ночь пытался решить Башни Ханоя и полностью взорвал свой разум. У моего учебника есть только около 30 страниц в рекурсии, поэтому он не слишком полезен. Кто-нибудь знает книги или ресурсы, которые могут помочь прояснить эту тему?

Ответ 1

Как вы опорожняете вазу с пятью цветами?

Ответ: если ваза не пуста, вы вынимаете один цветок  а затем вы опустошаете вазу с четырьмя цветами.

Как вы опорожняете вазу с четырьмя цветами?

Ответ: если ваза не пуста, вы вынимаете один цветок  а затем вы опустошаете вазу с тремя цветами.

Как вы опорожняете вазу с тремя цветами?

Ответ: если ваза не пуста, вы вынимаете один цветок  а затем вы опустошаете вазу с двумя цветами.

Как вы опорожняете вазу с двумя цветами?

Ответ: если ваза не пуста, вы вынимаете один цветок  а затем вы опустошаете вазу с одним цветком.

Как вы опорожняете вазу с одним цветком?

Ответ: если ваза не пуста, вы вынимаете один цветок  а затем вы опустошаете вазу без цветов.

Как вы опорожняете вазу без цветов?

Ответ: если ваза не пуста, вы вынимаете один цветок но ваза пуста, так что все готово.

Это повторяется. Позвольте обобщить это:

Как вы опорожняете вазу с N цветами?

Ответ: если ваза не пуста, вы вынимаете один цветок а затем вы опустошаете вазу с цветами N-1.

Хм, мы можем увидеть это в коде?

void emptyVase( int flowersInVase ) {
  if( flowersInVase > 0 ) {
   // take one flower and
    emptyVase( flowersInVase - 1 ) ;

  } else {
   // the vase is empty, nothing to do
  }
}

Хм, разве мы не могли сделать это в цикле for?

Почему да, рекурсию можно заменить итерацией, но часто рекурсия более элегантна.

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

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

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

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

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

Возможно, вы предвидели, куда я пойду с этим, и хотели бы увидеть какой-нибудь код? OK:

struct node {
  node* left;
  node* right;
  int value;
} ;

int sumNode( node* root ) {
  // if there is no tree, its sum is zero
  if( root == null ) {
    return 0 ;

  } else { // there is a tree
    return root->value + sumNode( root->left ) + sumNode( root->right ) ;
  }
}

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

Допустим, у нас есть дерево, которое выглядит следующим образом (числа являются значениями, косая черта указывает на дочерние элементы, а @означает, что указатель указывает на ноль):

     5
    / \
   4   3
  /\   /\
 2  1 @  @
/\  /\
@@  @@

Если мы вызовем sumNode для корня (узел со значением 5), мы вернемся:

return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;

Давайте расширим это на месте. Везде, где мы видим sumNode, мы заменяем его расширением оператора return:

sumNode( node-with-value-5);
return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;

return 5 + 4 + sumNode( node-with-value-2 ) + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ;  

return 5 + 4 
 + 2 + sumNode(null ) + sumNode( null )
 + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ;  

return 5 + 4 
 + 2 + 0 + 0
 + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + sumNode(null ) + sumNode( null )
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 + sumNode(null ) + sumNode( null ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 + 0 + 0 ;

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 ;

return 5 + 4 
 + 2 + 0 + 0
 + 1 
 + 3  ;

return 5 + 4 
 + 2 
 + 1 
 + 3  ;

return 5 + 4 
 + 3
 + 3  ;

return 5 + 7
 + 3  ;

return 5 + 10 ;

return 15 ;

Теперь посмотрим, как мы победили структуру произвольной глубины и "веткистости", рассматривая ее как повторное применение составного шаблона? каждый раз с помощью нашей функции sumNode мы работали только с одним узлом, используя одну ветвь if/then и два простых оператора возврата, которые почти писали сами, непосредственно из нашей спецификации?

How to sum a node:
 If a node is null 
   its sum is zero
 otherwise 
   its sum is its value 
   plus the sum of its left child node
   plus the sum of its right child node

Это сила рекурсии.


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

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

На самом деле, порядок, в котором мы вызывали дочерние элементы и добавляли текущее значение узла, вообще не имел значения, потому что сложение коммутативно.

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

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

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

Мы не просто хотим печатать волей-неволей, поэтому мы передадим нашей функции что-то для печати. Это будет объект с функцией print (char); нам не нужно беспокоиться о том, как это работает, просто когда вызывается print, он что-то печатает где-то.

Давайте посмотрим, что в коде:

struct node {
  node* left;
  node* right;
  char value;
} ;

// don't worry about this code
class Printer {
  private ostream& out;
  Printer( ostream& o ) :out(o) {}
  void print( char c ) { out << c; }
}

// worry about this code
int printNode( node* root, Printer& printer ) {
  // if there is no tree, do nothing
  if( root == null ) {
    return ;

  } else { // there is a tree
    printNode( root->left, printer );
    printer.print( value );
    printNode( root->right, printer );
}

Printer printer( std::cout ) ;
node* root = makeTree() ; // this function returns a tree, somehow
printNode( root, printer );

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

Теперь, если наше дерево выглядит так:

         k
        / \
       h   n
      /\   /\
     a  j @  @
    /\ /\
    @@ [email protected]
       /\
       @@

Что мы будем печатать?

From k, we go left to
  h, where we go left to
    a, where we go left to 
      null, where we do nothing and so
    we return to a, where we print 'a' and then go right to
      null, where we do nothing and so
    we return to a and are done, so
  we return to h, where we print 'h' and then go right to
    j, where we go left to
      i, where we go left to 
        null, where we do nothing and so
      we return to i, where we print 'i' and then go right to
        null, where we do nothing and so
      we return to i and are done, so
    we return to j, where we print 'j' and then go right to
      null, where we do nothing and so
    we return to j and are done, so
  we return to h and are done, so
we return to k, where we print 'k' and then go right to
  n where we go left to 
    null, where we do nothing and so
  we return to n, where we print 'n' and then go right to
    null, where we do nothing and so
  we return to n and are done, so 
we return to k and are done, so we return to the caller

Итак, если мы просто посмотрим на строки, мы напечатали:

    we return to a, where we print 'a' and then go right to
  we return to h, where we print 'h' and then go right to
      we return to i, where we print 'i' and then go right to
    we return to j, where we print 'j' and then go right to
we return to k, where we print 'k' and then go right to
  we return to n, where we print 'n' and then go right to

Мы видим, что мы напечатали "ahijkn", который действительно в алфавитном порядке.

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

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

Напоминая, что в большинстве языков оператор || ("или"), если его первый операнд имеет значение true, общая рекурсивная функция:

void recurse() { doWeStop() || recurse(); } 

Люк М комментирует:

ТАК следует создать значок для такого рода ответа. Поздравляем!

Спасибо, Люк! Но, на самом деле, поскольку я редактировал этот ответ более четырех раз (чтобы добавить последний пример, но в основном для исправления опечаток и полировки - набирать текст на крошечной клавиатуре нетбука сложно), я не могу получить больше очков за него, Что несколько отговаривает меня от того, чтобы вкладывать столько усилий в будущие ответы.

Смотрите мой комментарий здесь: https://stackoverflow.com/questions/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699

Ответ 2

Ваш мозг взорвался, потому что он попал в бесконечную рекурсию. Это общая ошибка начинающего.

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

Подумайте вместо задачи или процедуры, например, "Узнайте больше о рекурсии в сети". Это рекурсивно, и у вас нет проблем с этим. Для выполнения этой задачи вы можете:

a) Read a Google result page for "recursion"
b) Once you've read it, follow the first link on it and...
a.1)Read that new page about recursion 
b.1)Once you've read it, follow the first link on it and...
a.2)Read that new page about recursion 
b.2)Once you've read it, follow the first link on it and...

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

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

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

Компьютер не может вывести гнездо, поэтому вы должны включить явное окончание: "Узнайте больше о рекурсии в сети, ДОПОЛНИТЕЛЬНО, вы это понимаете, или вы прочитали максимум 10 страниц".

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

"узнайте больше о рекурсии в сети, Пока вы это понимаете или вы прочитали максимум 10 страниц и , начиная со страницы www.google.com/search?q=recursion"

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

  • Общий Lisp: Нежное введение в символические вычисления. Это самое нелекое математическое объяснение рекурсии.
  • Маленький интриган.

Ответ 3

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

function repeat()
{
   rinse();
   lather();
   repeat();
}

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

Ответ 4

Если вы хотите, чтобы книга, которая хорошо объясняет рекурсию в простых выражениях, взгляните на Геделя, Эшера, Баха: Вечную золотую оплетку Дугласа Хофстадтера, в частности главу 5. В дополнение к рекурсии она делает приятную работу по объяснению ряда сложных понятий в информатике и математике понятным образом, с одним объяснением, строящим на другом. Если раньше у вас не было большей подверженности этим типам концепций, это может быть довольно умопомрачительная книга.

Ответ 5

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

Говоря об умножении, подумайте об этом.

Вопрос:

Что a * b?

Ответ:

Если b равно 1, то a. В противном случае это a + a * (b-1).

Что a * (b-1)? См. Вышеупомянутый вопрос для способа его работы.

Ответ 6

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

function writeNumbers( aNumber ){
 write(aNumber);
 if( aNumber > 0 ){
  writeNumbers( aNumber - 1 );
 }
 else{
  return;
 }
}

Эта функция будет распечатывать все номера с первого номера, которое вы будете подавать до 0. Таким образом:

writeNumbers( 10 );
//This wil write: 10 9 8 7 6 5 4 3 2 1 0
//and then stop because aNumber is no longer larger then 0

Что происходит с басичностью, так это то, что writeNumbers (10) будет писать 10, а затем вызовет writeNumbers (9), который будет писать 9, а затем вызовет writeNumber (8) и т.д. До записиNumbers (1) записывает 1, а затем вызывает writeNumbers (0), который будет писать 0, но не будет вызывать writeNumbers (-1);

Этот код по существу совпадает с:

for(i=10; i>0; i--){
 write(i);
}

Тогда зачем использовать рекурсию, которую вы можете задать, если for-loop делает практически то же самое. Ну, вы в основном используете рекурсию, когда вам нужно будет гнездиться для циклов, но не будете знать, насколько глубоко они вложены. Например, при печати элементов из вложенных массивов:

var nestedArray = Array('Im a string', 
                        Array('Im a string nested in an array', 'me too!'),
                        'Im a string again',
                        Array('More nesting!',
                              Array('nested even more!')
                              ),
                        'Im the last string');
function printArrayItems( stringOrArray ){
 if(typeof stringOrArray === 'Array'){
   for(i=0; i<stringOrArray.length; i++){ 
     printArrayItems( stringOrArray[i] );
   }
 }
 else{
   write( stringOrArray );
 }
}

printArrayItems( stringOrArray );
//this will write:
//'Im a string' 'Im a string nested in an array' 'me too' 'Im a string again'
//'More nesting' 'Nested even more' 'Im the last string'

Эта функция может принимать массив, который может быть вложен в 100 уровней, тогда как вы пишете цикл for, тогда вам потребуется вложить его 100 раз:

for(i=0; i<nestedArray.length; i++){
 if(typeof nestedArray[i] == 'Array'){
  for(a=0; i<nestedArray[i].length; a++){
   if(typeof nestedArray[i][a] == 'Array'){
    for(b=0; b<nestedArray[i][a].length; b++){
     //This would be enough for the nestedAaray we have now, but you would have
     //to nest the for loops even more if you would nest the array another level
     write( nestedArray[i][a][b] );
    }//end for b
   }//endif typeod nestedArray[i][a] == 'Array'
   else{ write( nestedArray[i][a] ); }
  }//end for a
 }//endif typeod nestedArray[i] == 'Array'
 else{ write( nestedArray[i] ); }
}//end for i

Как вы можете видеть, рекурсивный метод намного лучше.

Ответ 7

На самом деле вы используете рекурсию, чтобы уменьшить сложность вашей проблемы. Вы применяете рекурсию, пока не достигнете простого базового варианта, который можно легко решить. С помощью этого вы можете решить последний рекурсивный шаг. И с этим все другие рекурсивные шаги до вашей первоначальной проблемы.

Ответ 8

рекурсии

Метод A, вызывает метод A вызывает метод A. В конце концов один из этих методов A не будет вызывать и не выходить, а рекурсировать, потому что что-то вызывает сам.

Пример рекурсии, где я хочу распечатать каждое имя папки на жестком диске: (в С#)

public void PrintFolderNames(DirectoryInfo directory)
{
    Console.WriteLine(directory.Name);

    DirectoryInfo[] children = directory.GetDirectories();

    foreach(var child in children)
    {
        PrintFolderNames(child); // See we call ourself here...
    }
}

Ответ 9

Я попытаюсь объяснить это с помощью примера.

Вы знаете, что n! означает? Если нет: http://en.wikipedia.org/wiki/Factorial

3!= 1 * 2 * 3 = 6

здесь идет некоторый псевдокод

function factorial(n) {
  if (n==0) return 1
  else return (n * factorial(n-1))
}

Итак, попробуйте:

factorial(3)

есть n 0?

нет!

поэтому мы углубимся в нашу рекурсию:

3 * factorial(3-1)

3-1 = 2

равно 2 == 0?

нет!

поэтому мы идем глубже!   3 * 2 * факториал (2-1) 2-1 = 1

равно 1 == 0?

нет!

поэтому мы идем глубже!   3 * 2 * 1 * факториал (1-1) 1-1 = 0

равно 0 == 0?

да!

имеем тривиальный случай

поэтому мы имеем   3 * 2 * 1 * 1 = 6

Надеюсь, вам помогли

Ответ 10

Какую книгу вы используете?

Стандартный учебник по алгоритмам, который на самом деле хорош, - Cormen и Rivest. Мой опыт в том, что он хорошо учит рекурсии.

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

Кроме того, 30 страниц в целом много, 30 страниц на одном языке программирования запутывают. Не пытайтесь учиться рекурсии на C или Java, прежде чем вы поймете рекурсию вообще из общей книги.

Ответ 11

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

Ответ 12

http://javabat.com - интересное место для практики рекурсии. Их примеры начинаются довольно легко и работают через обширные (если вы хотите занять это так далеко). Примечание: их подход учится на практике. Вот рекурсивная функция, которую я написал, чтобы просто заменить цикл for.

Цикл for:

public printBar(length)
{
  String holder = "";
  for (int index = 0; i < length; i++)
  {
    holder += "*"
  }
  return holder;
}

Вот рекурсия, чтобы сделать то же самое. (обратите внимание, что мы перегружаем первый метод, чтобы убедиться, что он используется так же, как и выше). У нас также есть другой способ поддержания нашего индекса (аналогично тому, как это делается для оператора for выше). Рекурсивная функция должна поддерживать свой собственный индекс.

public String printBar(int Length) // Method, to call the recursive function
{
  printBar(length, 0);
}

public String printBar(int length, int index) //Overloaded recursive method
{
  // To get a better idea of how this works without a for loop
  // you can also replace this if/else with the for loop and
  // operationally, it should do the same thing.
  if (index >= length)
    return "";
  else
    return "*" + printBar(length, index + 1); // Make recursive call
}

Короче говоря, рекурсия - хороший способ написать меньше кода. В последнем printBar обратите внимание, что мы имеем оператор if. Если наше условие достигнуто, мы выйдем из рекурсии и вернемся к предыдущему методу, который вернется к предыдущему методу и т.д. Если я отправил в printBar (8), я получаю ********. Я надеюсь, что с примером простой функции, которая делает то же самое, что и цикл for, что, возможно, это поможет. Однако вы можете попрактиковаться в Java Bat.

Ответ 13

Простой рекурсивный пример в Common Lisp:

MYMAP применяет функцию к каждому элементу в списке.

1) пустой список не имеет элемента, поэтому мы возвращаем пустой список -(), а NIL - пустой список.

2) примените функцию к первому списку, вызовите MYMAP для остальной части списка (рекурсивный вызов) и объедините оба результата в новый список.

(DEFUN MYMAP (FUNCTION LIST)
  (IF (NULL LIST)
      ()
      (CONS (FUNCALL FUNCTION (FIRST LIST))
            (MYMAP FUNCTION (REST LIST)))))

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

Этот пример вызывает функцию SIN для каждого номера в списке (1 2 3 4).

Command: (mymap 'sin '(1 2 3 4))

1 Enter MYMAP SIN (1 2 3 4)
| 2 Enter MYMAP SIN (2 3 4)
|   3 Enter MYMAP SIN (3 4)
|   | 4 Enter MYMAP SIN (4)
|   |   5 Enter MYMAP SIN NIL
|   |   5 Exit MYMAP NIL
|   | 4 Exit MYMAP (-0.75680256)
|   3 Exit MYMAP (0.14112002 -0.75680256)
| 2 Exit MYMAP (0.9092975 0.14112002 -0.75680256)
1 Exit MYMAP (0.841471 0.9092975 0.14112002 -0.75680256)

Это наш результат:

(0.841471 0.9092975 0.14112002 -0.75680256)

Ответ 14

Поистине математический способ взглянуть на создание рекурсивной функции будет следующим:

1: Представьте, что у вас есть функция, которая верна для f (n-1), построение f такое, что f (n) является правильным. 2: построим f, так что f (1) является правильным.

Вот как вы можете доказать, что функция правильная, математически и называется Induction. Это эквивалентно наличию различных базовых случаев или более сложных функций для множества переменных). Это также эквивалентно представлению, что f (x) корректно для всех x

Теперь для "простого" примера. Создайте функцию, которая может определить, возможно ли иметь комбинацию монет в 5 центов и 7 центов, чтобы сделать x центов. Например, возможно иметь 17 центов на 2x5 + 1x7, но невозможно иметь 16 центов.

Теперь представьте, что у вас есть функция, которая сообщает вам, можно ли создавать x центов, если x < п. Вызовите эту функцию can_create_coins_small. Достаточно просто представить себе, как сделать функцию для n. Теперь создайте свою функцию:

bool can_create_coins(int n)
{
    if (n >= 7 && can_create_coins_small(n-7))
        return true;
    else if (n >= 5 && can_create_coins_small(n-5))
        return true;
    else
        return false;
}

Трюк здесь заключается в том, чтобы понять, что тот факт, что can_create_coins работает для n, означает, что вы можете заменить can_create_coins на can_create_coins_small, указав:

bool can_create_coins(int n)
{
    if (n >= 7 && can_create_coins(n-7))
        return true;
    else if (n >= 5 && can_create_coins(n-5))
        return true;
    else
        return false;
}

Последнее, что нужно сделать, - иметь базовый случай, чтобы остановить бесконечную рекурсию. Обратите внимание: если вы пытаетесь создать 0 центов, то это возможно, если у вас нет монет. Добавление этого условия дает:

bool can_create_coins(int n)
{
    if (n == 0)
        return true;
    else if (n >= 7 && can_create_coins(n-7))
        return true;
    else if (n >= 5 && can_create_coins(n-5))
        return true;
    else
        return false;
}

Можно доказать, что эта функция всегда будет возвращаться, используя метод бесконечный спуск, но здесь это не обязательно. Вы можете себе представить, что f (n) вызывает только нижние значения n и всегда будет достигать 0.

Чтобы использовать эту информацию для решения вашей проблемы с Tower of Hanoi, я думаю, что трюк состоит в том, чтобы предположить, что у вас есть функция для перемещения n-1 таблиц от a до b (для любого a/b), пытаясь переместить n таблиц из a-b.

Ответ 15

Чтобы объяснить рекурсию шестилетнему ребенку, сначала объясните это пятилетнему ребенку, а затем подождите год.

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

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

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

Ответ 16

Дети неявно используют рекурсию, например:

Дорожная поездка в Мир Диснея

Мы еще там? (нет)

Мы еще там? (Скоро)

Мы еще там? (Почти...)

Мы еще там? (SHHHH)

Мы еще там? (!!!!!)

В этот момент ребенок засыпает...

Эта функция обратного отсчета является простым примером:

function countdown()
      {
      return (arguments[0] > 0 ?
        (
        console.log(arguments[0]),countdown(arguments[0] - 1)) : 
        "done"
        );
      }
countdown(10);

Ответ 17

При работе с рекурсивными решениями я всегда стараюсь:

  • Сначала установите базовый регистр, т.е. когда n = 1 в решении факториала
  • Попробуйте придумать общее правило для каждого другого случая

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

Это также помогло бы, если бы вы могли работать над более сложными проблемами, прежде всего, чтобы просто повесить его. Некоторые примеры решаются для факториала и порождают n-е число фибоначчи.

В качестве ссылок я настоятельно рекомендую алгоритмы Роберта Седжуика.

Надеюсь, что это поможет. Удачи.

Ответ 18

Уч. В прошлом году я попытался выяснить Башни Ханоя. Трудная вещь о TOH - это не простой пример рекурсии - у вас есть вложенные рекурсии, которые также изменяют роли башен при каждом вызове. Единственный способ, которым я мог понять это, - это буквально визуализировать движение колец в моем воображаемом глазу и вербализовать то, что будет рекурсивным вызовом. Я бы начал с одного кольца, затем два, затем три. Я фактически заказал игру в Интернете. Мне понадобилось два или три дня, чтобы раздробить мои мозги.

Ответ 19

Рекурсивная функция похожа на spring, вы сжимаете бит для каждого вызова. На каждом шаге вы помещаете немного информации (текущий контекст) в стек. Когда последний шаг будет достигнут, освободится spring, собирая сразу все значения (контексты)!

Не уверен, что эта метафора эффективна...: -)

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

Очень распространенным случаем является прогулка по дереву (или графику, но деревья чаще встречаются вообще).
Например, иерархия папок: чтобы перечислить файлы, вы перебираете их. Если вы найдете подкаталог, функция, перечисляющая файлы, вызывается в качестве аргумента новой папке. Возвращаясь из этой новой папки (и ее подпапок!), Она возобновляет свой контекст, в следующий файл (или папку).
Другим конкретным случаем является рисование иерархии компонентов GUI: обычно для контейнеров, например панелей, для хранения компонентов, которые могут быть также панелей, или составных компонентов и т.д. Обычная процедура рисования вызывает рекурсивно функцию рисования каждого компонента, которая вызывает функцию рисования всех компонентов, которые она содержит, и т.д.

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

Ответ 20

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

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

Например, мы хотим рассчитать длину списка. Позволяет называть нашу функцию magical_length и наш магический помощник с magical_length Мы знаем, что если мы дадим подсписку, у которой нет первого элемента, это даст нам длину подслова по магии. Тогда нам нужно только подумать, как интегрировать эту информацию с нашей работой. Длина первого элемента равна 1, а magic_counter дает длину подвыражения n-1, поэтому полная длина равна (n-1) + 1 → n

int magical_length( list )
  sublist = rest_of_the_list( list )
  sublist_length = magical_length( sublist ) // you can think this function as magical and given to you
  return 1 + sublist_length

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

int magical_length( list )
  if ( list is empty) then
    return 0
  else
    sublist_length = magical_length( sublist ) // you can think this function as magical and given to you
    return 1 + sublist_length