Як ви спустошите вазу, що містить п’ять квіток?
Відповідь: якщо ваза не порожня, ви виймаєте одну квітку, а потім порожню вазу, що містить чотири квітки.
Як ви спустошите вазу, що містить чотири квітки?
Відповідь: якщо ваза не порожня, ви виймаєте одну квітку, а потім порожню вазу, яка містить три квітки.
Як ви спустошите вазу, що містить три квітки?
Відповідь: якщо ваза не порожня, ви виймаєте одну квітку, а потім порожню вазу, що містить дві квітки.
Як ви спустошите вазу, що містить дві квітки?
Відповідь: якщо ваза не порожня, ви виймаєте одну квітку, а потім порожню вазу, що містить одну квітку.
Як ви спустошите вазу, що містить одну квітку?
Відповідь: якщо ваза не порожня, ви виймаєте одну квітку, а потім порожню вазу, яка не містить квітів.
Як ви спустошите вазу, що не містить квітів?
Відповідь: якщо ваза не порожня, ви виймаєте одну квітку, але ваза порожня, так що ви готові.
Це повторюється. Давайте узагальнимо це:
Як ви спустошите вазу, що містить 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