Розуміння рекурсії [закрито]


225

У мене виникають великі проблеми з розумінням рекурсії в школі. Кожен раз, коли професор говорить про це, я, здається, отримую це, але як тільки я спробую його самостійно, він повністю роздуває мізки.

Я цілу ніч намагався вирішити Ханойські вежі і повністю підірвав розум. У моєму підручнику рекурсії всього близько 30 сторінок, тому це не надто корисно. Хтось знає про книги чи ресурси, які можуть допомогти з’ясувати цю тему?


200
Для того, щоб зрозуміти рекурсію, потрібно спочатку зрозуміти рекурсію.
Пол Томблін

40
Рекурсія: Дивіться рекурсію
Лорен Печтел

36
@Paul: Я жартую, але я завжди вважав, що це технічно неправильно. Де основна умова, яка змушує алгоритм закінчитися? Це основний реквізит для рекурсії. =)
Серхіо Акоста

70
Я дам йому постріл: "Щоб зрозуміти рекурсію, вам потрібно зрозуміти рекурсію, поки не зрозумієте її". =)
Серхіо Акоста

91
Погляньте на це питання може допомогти stackoverflow.com/questions/717725/understanding-recursion
Омар Kooheji

Відповіді:


598

Як ви спустошите вазу, що містить п’ять квіток?

Відповідь: якщо ваза не порожня, ви виймаєте одну квітку, а потім порожню вазу, що містить чотири квітки.

Як ви спустошите вазу, що містить чотири квітки?

Відповідь: якщо ваза не порожня, ви виймаєте одну квітку, а потім порожню вазу, яка містить три квітки.

Як ви спустошите вазу, що містить три квітки?

Відповідь: якщо ваза не порожня, ви виймаєте одну квітку, а потім порожню вазу, що містить дві квітки.

Як ви спустошите вазу, що містить дві квітки?

Відповідь: якщо ваза не порожня, ви виймаєте одну квітку, а потім порожню вазу, що містить одну квітку.

Як ви спустошите вазу, що містить одну квітку?

Відповідь: якщо ваза не порожня, ви виймаєте одну квітку, а потім порожню вазу, яка не містить квітів.

Як ви спустошите вазу, що не містить квітів?

Відповідь: якщо ваза не порожня, ви виймаєте одну квітку, але ваза порожня, так що ви готові.

Це повторюється. Давайте узагальнимо це:

Як ви спустошите вазу, що містить 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?

Чому так, рекурсію можна замінити ітерацією, але часто рекурсія є більш елегантною.

Поговоримо про дерева. В інформатиці дерево - це структура, що складається з вузлів , де кожен вузол має деяку кількість дітей, які також є вузлами, або нульовими. Бінарне дерево являє собою дерево з вузлів, що мають рівно два дитини, як правило , називають «лівої» і «правою»; знову діти можуть бути вузлами, або нульовими. Корінь є вузлом , який не є нащадком будь-якого іншого вузла.

Уявіть, що вузол, окрім своїх дітей, має значення, число, і уявіть, що ми хочемо підсумувати всі значення в якомусь дереві.

Щоб підсумовувати значення в будь-якому одному вузлі, ми додамо значення самого вузла до значення його лівого дочірнього, якщо воно є, і значення його правого дочірнього, якщо воно є. Тепер пригадайте, що діти, якщо вони не нульові, також є вузлами.

Отже, підсумовуючи ліву дитину, ми додали б значення самого дочірнього вузла до значення його лівої дитини, якщо воно є, і значення його правої дитини, якщо така є.

Отже, щоб підсумовувати значення лівої дитини лівої дитини, ми додали б значення самого вузла дитини до значення його лівої дитини, якщо воно є, і значення його правої дитини, якщо така є.

Можливо, ви передбачили, куди я йду з цим, і хотіли б побачити якийсь код? ГАРАЗД:

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

Ось сила рекурсії.


Приклад вази вище - приклад хвостової рекурсії . Все, що означає рекурсію хвоста, - це те, що у випадку рекурсивної функції, якщо ми повторювались (тобто якщо ми знову викликали функцію), це було останнє, що ми зробили.

Приклад дерева не був рекурсивним хвостом, тому що хоч це останнє, що ми робили, - це повторити правильну дитину, до цього ми повторювали ліву дитину.

Насправді порядок, в якому ми зателефонували до дітей та додав значення поточного вузла, взагалі не має значення, оскільки додавання є комутативним.

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

Наше дерево матиме особливу властивість, що для будь-якого вузла його персонаж приходить після (в алфавітному порядку) символу, який тримає його ліва дитина, а раніше (в алфавітному порядку) символу, який тримає його права дитина.

Ми хочемо зробити друк дерева в алфавітному порядку. Це легко зробити, враховуючи дерево особливою властивістю. Ми просто роздруковуємо ліву дитину, потім символ вузла, потім праву дитину.

Ми не просто хочемо друкувати вольно-невольно, тому передамо нашій функції щось надрукувати. Це буде об’єкт з функцією друку (char); нам не потрібно турбуватися про те, як це працює, лише коли виклик друку він щось надрукує десь.

Подивимось це в коді:

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 @  @
    /\ /\
    @@ i@
       /\
       @@

Що ми друкуємо?

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", що дійсно в алфавітному порядку.

Нам вдається надрукувати ціле дерево в алфавітному порядку, просто знаючи, як надрукувати один вузол в алфавітному порядку. Що було справедливим (оскільки наше дерево мало особливу властивість впорядковувати значення ліворуч від алфавітно пізніших значень), знаючи надрукувати ліву дочірню перед друком значення вузла та друкувати праву дочірню після друку значення вузла.

І в цьому полягає сила рекурсії: вміння робити цілі речі, знаючи лише, як зробити частину цілого (і знаючи, коли припинити рецидиви).

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

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

Люк М коментарі:

Отож слід створити знак для такої відповіді. Вітаємо!

Спасибі, Люку! Але насправді, оскільки я відповів на цю відповідь більше чотирьох разів (щоб додати останній приклад, але в основному для виправлення друкарських помилок та відшліфування її - набирати на крихітній клавіатурі нетбука важко), я не можу отримати більше балів за неї . Що дещо перешкоджає мені вкладати якомога більше зусиль у майбутні відповіді.

Дивіться тут мій коментар: /programming/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699


35

Ваш мозок підірвався через те, що він потрапив у нескінченну рекурсію. Це поширена помилка для початківців.

Вірите чи ні, ви вже розумієте рекурсію, вас просто тягне загальна, але несправна метафора функції: маленька скринька з речами, що надходять і виходять.

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

а) Прочитайте сторінку результатів Google для "рекурсії"
б) Після того, як ви прочитали його, перейдіть за першим посиланням на ньому та ...
a.1) Прочитайте нову сторінку про рекурсію 
b.1) Після того, як ви прочитали його, перейдіть за першим посиланням на ньому та ...
a.2) Прочитайте нову сторінку про рекурсію 
b.2) Після того, як ви прочитали його, перейдіть за першим посиланням на ньому та ...

Як бачите, ви давно займаєтесь рекурсивними речами без проблем.

Як довго ви б продовжували виконувати це завдання? Назавжди, поки ваш мозок не вибухне? Звичайно, ні, ви зупинитесь у заданій точці, коли ви повірите, що виконали завдання.

Не потрібно вказувати це, коли просите вас "дізнатися більше про рекурсію в мережі", оскільки ви людина і можете самі це зробити.

Комп'ютер не може підключити гніздо, тому ви повинні містити чітке закінчення: "дізнайтеся більше про рекурсію в мережі, доки ви її не зрозуміли, або ви прочитали максимум 10 сторінок ".

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

"дізнайтеся більше про рекурсію в мережі, доки ви її не зрозумієте, або ви прочитали максимум 10 сторінок і починаєте з www.google.com/search?q=recursion "

Щоб уникнути всього цього, пропоную спробувати будь-яку з цих книг:

  • Загальний Лисп: Ніжний вступ до символічних обчислень. Це симпатичне нематематичне пояснення рекурсії.
  • Маленький мошеник.

6
Метафора "функція = маленька коробка вводу-виводу" працює з рекурсією, доки ви також уявляєте, що там є фабрика, що робить нескінченні клони, і ваша маленька коробка може проковтнути інші невеликі коробки.
ефемія

2
Цікаво .. Отже, в майбутньому роботи щось будуть гуглювати і навчатись самостійно, використовуючи перші 10 посилань. :) :)
кумар

2
@kumar чи не Google це робить вже з Інтернетом?
TJ

1
чудові книги, дякую за рекомендацію
Макс Корецький

+1 за "Ваш мозок підірвався, тому що він потрапив у нескінченну рекурсію. Це звичайна помилка для початківців".
Стік Underflow

26

Щоб зрозуміти рекурсію, все, що вам потрібно зробити, - це подивитися на етикетці пляшки з шампунем:

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

Проблема в цьому полягає в тому, що не існує умови припинення, і рекурсія повторюватиметься нескінченно, або поки у вас не закінчиться шампунь або гаряча вода (зовнішні умови припинення, аналогічні видуванню вашої стеки).


6
Дякую, dar7yl - це ЗАВЖДИ дратувало мене на флаконах з шампунем. (Напевно, мені завжди судилося програмувати). Хоча я тримав парі , хлопець , який вирішив додати «Repeat» в кінці інструкції зроблені мільйони компанії.
kenj0418

5
Я сподіваюся, що rinse()за вамиlather()
CoderDennis

@JakeWilson, якщо використовується оптимізація хвостових викликів - обов'язково. як це зараз є, хоча - це цілком дійсна рекурсія.

1
@ dar7yl, тому моя пляшка шампуню завжди порожня ...
Брендон Лінг

11

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


А потім блукайте по решті книг Гофстадтера. Моя улюблена на даний момент - переклад поезії: Le Ton Beau do Marot . Не зовсім тематика CS, але вона викликає цікаві питання щодо того, що насправді і що означає переклад.
RBerteig

9

Це скоріше скарга, ніж питання. Чи є у вас конкретніше питання щодо рекурсії? Як і множення, люди не про що багато пишуть.

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

Питання:

Що таке * b?

Відповідь:

Якщо b дорівнює 1, це a. В іншому випадку це + a * (b-1).

Що таке * (b-1)? Дивіться вищезазначене питання для способу його розробки.


@Andrew Grimm: Добре запитання. Це визначення для натуральних чисел, а не цілих чисел.
S.Lott

9

Я думаю, що цей дуже простий метод повинен допомогти вам зрозуміти рекурсію. Метод буде викликати себе, поки певна умова не буде істинною, а потім повернеться:

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) тощо. butt не буде викликати writeNumbers (-1);

Цей код по суті такий же, як:

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

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

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 рівнів, тоді як ви пишете цикл, тоді потрібно буде вкласти його 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

Як бачите, рекурсивний метод набагато краще.


1
LOL - взяв мене вдруге, щоб зрозуміти, що ти використовуєш JavaScript! Я побачив "функцію" і подумав, що PHP потім зрозумів, що змінні не починаються з $. Тоді я подумав C # для використання слова var - але методи не називаються функціями!
ozzy432836

8

Насправді ви використовуєте рекурсію для зменшення складності вашої проблеми. Ви застосовуєте рекурсію, поки не досягнете простого базового випадку, який можна легко вирішити. За допомогою цього можна вирішити останній рекурсивний крок. І з цим усі інші рекурсивні кроки до вашої первинної проблеми.


1
Я згоден з цією відповіддю. Хитрість полягає у визначенні та вирішенні базового (найпростішого) випадку. А потім висловіть проблему в тому простому випадку (який ви вже вирішили).
Серхіо Акоста

6

Спробую пояснити це на прикладі.

Ви знаєте, що 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)

дорівнює 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

Я сподіваюся, що допомогли вам


Це не корисний спосіб думати про рекурсію. Поширена помилка початківців полягає в тому, щоб спробувати уявити, що відбувається всередині повторювального дзвінка, а не просто довіряти / доводити, що він поверне правильну відповідь - і ця відповідь, схоже, це заохочує.
ShreevatsaR

який би кращий спосіб зрозуміти рекурсію? я не кажу, що ви повинні дивитися на кожну рекурсивну функцію таким чином. Але це допомогло мені зрозуміти, як це працює.
Зоран Заріч

1
[Я не голосував -1, BTW.] Можна подумати так: довіряючи тому, що факториал (n-1) правильно дає (n-1)! = (N-1) * ... * 2 * 1, тоді н факторіал (n-1) дає n * (n-1) ... * 2 * 1, що є n !. Або що завгодно. [Якщо ви намагаєтеся навчитися самостійно писати рекурсивні функції, а не просто бачите, що робить якась функція.]
ShreevatsaR

Я використовував фактичні дані при поясненні рекурсії, і я думаю, що однією з найпоширеніших причин цього не вдалося зробити приклад, тому що пояснювач не любить математику і захоплюється цим. (Питання про те, чи не люблять люди, які не люблять математику, - інше питання) З цієї причини я, як правило, намагаюся використовувати нематематичний приклад, де це можливо.
Tony Meyer

5

Рекурсія

Метод A, виклики Метод A викликає метод А. Зрештою, один із цих методів A не дзвонить і не виходить, але це рекурсія, оскільки щось викликає саме себе.

Приклад рекурсії, де я хочу роздрукувати кожну назву папки на жорсткому диску: (в c #)

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...
    }
}

де базовий випадок у цьому прикладі?
Kunal Mukherjee

4

Яку книгу ви використовуєте?

Стандартний підручник з алгоритмів, який насправді хороший, - це Cormen & Rivest. Мій досвід полягає в тому, що він досить добре вчить рекурсії.

Рекурсія є однією з найскладніших частин програмування, яку можна зрозуміти, і хоча вона вимагає інстинкту, її можна вивчити. Але для цього потрібен хороший опис, хороші приклади та хороші ілюстрації.

Також 30 сторінок взагалі багато, 30 сторінок в одній мові програмування заплутано. Не намагайтеся вивчити рекурсію на мові C або Java, перш ніж ви зрозумієте рекурсію взагалі з загальної книги.


4

Рекурсивна функція - це просто функція, яка викликає себе стільки разів, скільки потрібно для цього. Це корисно, якщо вам потрібно щось обробити кілька разів, але ви не впевнені, скільки разів насправді буде потрібно. Певним чином, ви могли думати про рекурсивну функцію як про тип циклу. Як і цикл, однак, вам потрібно буде вказати умови для порушення процесу, інакше він стане нескінченним.


4

http://javabat.com - це цікаве та захоплююче місце для занять рекурсією. Їх приклади починаються досить легкими і проробляються через багато (якщо ви хочете взяти це так далеко). Примітка: Їх підхід вивчається практикуючи. Ось рекурсивна функція, яку я написав, щоб просто замінити цикл.

Цикл for:

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

Ось рекурсія зробити те саме. (зауважте, ми перевантажуємо перший метод, щоб переконатися, що він використовується так само, як вище). Також у нас є інший метод підтримання нашого індексу (подібний до того, як це робить для вас оператор вище). Рекурсивна функція повинна підтримувати власний індекс.

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), я отримаю ********. Я сподіваюся, що на прикладі простої функції, яка робить те саме, що і для циклу, можливо, це допоможе. Ти можеш це більше попрактикувати на Java Bat.


javabat.com - надзвичайно корисний веб-сайт, який допоможе думати рекурсивно. Я настійно пропоную піти туди і спробувати самостійно вирішити рекурсивні проблеми.
Парадиус

3

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

1: Уявіть, що у вас є функція, правильна для f (n-1), побудуйте f такою, що f (n) є правильною. 2: Побудуйте f таким чином, що f (1) є правильним.

Ось як можна довести, що функція є математичною правильною, і вона називається індукцією . Еквівалентно мати різні базові випадки або більш складні функції на кількох змінних). Також еквівалентно уявити, що f (x) є правильним для всіх x

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

Тепер уявіть, що у вас є функція, яка підказує, чи можна створити х центів, якщо x <n. Викликайте цю функцію 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.

Щоб скористатися цією інформацією для вирішення вашої проблеми в Таїланді Ханої, я думаю, що фокус полягає в тому, щоб припустити, що у вас є функція переміщення n-1 планшетів від a до b (для будь-якого a / b), намагаючись перемістити n таблиць від a до b .


3

Простий рекурсивний приклад у 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)

ЩО З ВСІМ КАПАМИ? Однак, якщо це серйозно, вони вийшли зі стилю в LISP десь 20 років тому.
Себастьян Крог

Ну, я це написав на моделі машини Lisp Machine, якій зараз 17 років. Насправді я записав функцію без форматування у слухачі, зробив кілька редагувань, а потім використав PPRINT для її форматування. Це перетворило код на CAPS.
Rainer Joswig

3

Щоб пояснити рекурсію шестирічному, спочатку поясніть це п’ятирічному, а потім зачекайте рік.

Насправді це корисний зустрічний приклад, оскільки ваш рекурсивний дзвінок повинен бути простішим, а не складнішим. Було б ще важче пояснити рекурсію до п'ятирічного віку, і хоча ви могли зупинити рекурсію при 0, у вас немає простого рішення для пояснення рекурсії нульовому віку.

Щоб вирішити проблему за допомогою рекурсії, спочатку поділіть її на одну або кілька більш простих проблем, які ви можете вирішити таким же чином, а потім, коли проблему досить просто вирішити без подальшої рекурсії, ви можете повернутися назад до більш високих рівнів.

Фактично, це було рекурсивним визначенням способу вирішення проблеми з рекурсією.


3

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

Дорожня подорож у світ Діснея

Ми ще там? (Ні)

Ми там ще? (Незабаром)

Ми там ще? (Майже ...)

Ми там ще? (SHHHH)

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

У цей момент дитина засинає ...

Ця функція зворотного відліку - простий приклад:

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

Закон Хофштадтера, застосований до програмних проектів, також є актуальним.

Суть людської мови полягає, на думку Хомського, у здатності кінцевих мізків виробляти те, що він вважає нескінченними граматиками. Під цим він має на увазі не тільки те, що немає верхньої межі у тому, що ми можемо сказати, але й немає верхньої межі кількості речень, яку має наша мова, і немає верхньої межі щодо розміру якогось конкретного речення. Хомський стверджував, що основним інструментом, який лежить в основі всієї цієї творчості людської мови, є рекурсія: здатність однієї фрази повторюватися всередині іншої однотипної фрази. Якщо я кажу "будинок брата Івана", у мене є іменник "будинок", який зустрічається в іменниковій фразі "будинок брата", і ця фраза іменника зустрічається в іншій фразі іменника "будинок брата Івана". Це має багато сенсу, і це

Список літератури


2

Працюючи з рекурсивними рішеннями, я завжди намагаюся:

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

Також існують різні типи рекурсивних рішень, є підхід «ділити і перемагати», який корисний для фракталів та багатьох інших.

Це також допоможе, якщо ви могли б попрацювати над більш простими проблемами спочатку просто розвісити його. Деякі приклади вирішують для факторного і генерують число n-го рівня.

Для довідки настійно рекомендую алгоритми Роберта Седжевіка.

Сподіваюся, що це допомагає. Удачі.


Цікаво, чи не краще спочатку придумати загальне правило - рекурсивний дзвінок, який "простіше", ніж те, з чого ви почали. Тоді базовий випадок повинен стати очевидним, виходячи з того, що є найпростішим. Саме тому я схильний думати про вирішення проблеми рекурсивно.
dlaliberte

2

Ой. Я намагався розібратися у Вежах Ханоя минулого року. Найважливіша річ у TOH - це не простий приклад рекурсії - у вас вкладені рекурсії, які також змінюють ролі веж на кожному дзвінку. Єдиний спосіб, коли я міг би зробити це сенсом, - це буквально візуалізувати рух кілець в моїх очах, і вербалізувати, яким буде рекурсивний дзвінок. Я б почав з одного кільця, потім два, потім три. Я фактично замовив гру в Інтернеті. На це мені знадобилося, може, два-три дні, щоб зламати мізки.


1

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

Не впевнений, що ця метафора ефективна ... :-)

У будь-якому разі, за винятком класичних прикладів (факторіал, який є найгіршим прикладом, оскільки він неефективний і легко сплющений, Фібоначчі, Ханой ...), які трохи штучні (я рідко, якщо взагалі, використовую їх у реальних випадках програмування), це цікаво подивитися, де він насправді використовується.

Дуже поширений випадок - ходити по дереву (або графіку, але дерева звичайніші).
Наприклад, ієрархія папок: щоб перелічити файли, ви повторите їх. Якщо ви знайдете підкаталог, функція, що перераховує файли, викликає себе з новою папкою як аргумент. Повертаючись із переліку цієї нової папки (та її підпапок!), Вона поновлює її контекст, передуючи наступний файл (або папку).
Інший конкретний випадок - це малювання ієрархії компонентів графічного інтерфейсу: зазвичай є контейнери, як панелі, для вміщення компонентів, які також можуть бути панелями, або складених компонентів і т.д. називає функцію фарби всіх компонентів, які вона містить, і т.д.

Не впевнений, чи я дуже чіткий, але мені подобається показувати реальне використання навчального матеріалу, оскільки це було щось, в чому я спіткнувся в минулому.


1

Подумайте, робоча бджола. Намагається зробити мед. Це робить свою роботу і очікує, що інші бджоли-робітники зроблять відпочинок з медом. А коли соти наповнені, вона зупиняється.

Подумайте це як магію. У вас є функція, яка має однакове ім’я з тією, яку ви намагаєтеся реалізувати, і коли ви надаєте їй підпроблему, вона вирішує її для вас, і єдине, що вам потрібно зробити, це інтегрувати рішення вашої частини з рішенням дав тобі.

Наприклад, ми хочемо обчислити довжину списку. Давайте назвемо нашу функцію 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.