Как вы опорожняете вазу с пятью цветами?
Ответ: если ваза не пуста, вы вынимаете один цветок, а затем вы опорожняете вазу с четырьмя цветами.
Как вы опорожняете вазу с четырьмя цветами?
Ответ: если ваза не пуста, вы вынимаете один цветок, а затем вы опорожняете вазу с тремя цветами.
Как вы опорожняете вазу с тремя цветами?
Ответ: если ваза не пуста, вы вынимаете один цветок, а затем вы опорожняете вазу с двумя цветами.
Как вы опорожняете вазу с двумя цветами?
Ответ: если ваза не пуста, вы вынимаете один цветок, а затем вы опорожняете вазу с одним цветком.
Как вы опорожняете вазу с одним цветком?
Ответ: если ваза не пуста, вы вынимаете один цветок, а затем вы опорожняете вазу без цветов.
Как вы опорожняете вазу без цветов?
Ответ: если ваза не пуста, вы вынимаете один цветок, но ваза пуста, так что все готово.
Это повторяется. Давайте обобщим это:
Как вы опорожняете вазу, содержащую 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
}
}
Хм, разве мы не могли сделать это в цикле?
Почему, да, рекурсию можно заменить итерацией, но часто рекурсия более элегантна.
Давайте поговорим о деревьях. В информатике дерево - это структура, состоящая из узлов , где у каждого узла есть некоторое количество дочерних узлов, которые также являются узлами или равны нулю. Бинарное дерево представляет собой дерево из узлов, имеющих ровно два ребенка, как правило , называют «левой» и «правой»; снова дети могут быть узлами или нулевыми. корень является узлом , который не является потомком любого другого узла.
Представьте, что у узла, в дополнение к его дочерним элементам, есть значение, число, и представьте, что мы хотим суммировать все значения в некотором дереве.
Чтобы суммировать значение в любом одном узле, мы добавили бы значение самого узла к значению его левого дочернего элемента, если он есть, и значения его правого дочернего элемента, если он есть. Теперь вспомните, что дети, если они не равны нулю, также являются узлами.
Таким образом, чтобы суммировать левый дочерний элемент, мы добавили бы значение самого дочернего узла к значению его левого дочернего элемента, если он есть, и значения его правого дочернего элемента, если он есть.
Таким образом, чтобы суммировать значение левого дочернего элемента левого потомка, мы добавили бы значение самого дочернего узла к значению его левого дочернего элемента, если он есть, и значению его правого дочернего элемента, если он есть.
Возможно, вы предвидели, куда я пойду с этим, и хотели бы увидеть какой-нибудь код? ХОРОШО:
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 @ @
/\ /\
@@ 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», который действительно в алфавитном порядке.
Нам удается напечатать все дерево в алфавитном порядке, просто зная, как напечатать один узел в алфавитном порядке. Что было просто (потому что у нашего дерева было специальное свойство упорядочения значений слева от более поздних по алфавиту значений), зная, что нужно напечатать левый потомок перед печатью значения узла и распечатать правый потомок после печати значения узла.
И это сила рекурсии: способность делать целые вещи, зная только, как сделать часть целого (и зная, когда прекратить повторение).
Напоминая, что в большинстве языков оператор || ("или") при коротком замыкании, когда его первый операнд равен true, общая рекурсивная функция:
void recurse() { doWeStop() || recurse(); }
Люк М комментирует:
ТАК следует создать значок для такого рода ответа. Поздравляем!
Спасибо, Люк! Но на самом деле, поскольку я редактировал этот ответ более четырех раз (чтобы добавить последний пример, но в основном для исправления опечаток и полировки - набирать текст на крошечной клавиатуре нетбука сложно), я не могу получить больше очков за него , Что несколько отговаривает меня от того, чтобы вкладывать столько усилий в будущие ответы.
Смотрите мой комментарий здесь: /programming/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699