Como você esvazia um vaso contendo cinco flores?
Resposta: se o vaso não estiver vazio, você retira uma flor e depois esvazia um vaso contendo quatro flores.
Como você esvazia um vaso contendo quatro flores?
Resposta: se o vaso não estiver vazio, você retira uma flor e depois esvazia um vaso contendo três flores.
Como você esvazia um vaso contendo três flores?
Resposta: se o vaso não estiver vazio, você retira uma flor e depois esvazia um vaso contendo duas flores.
Como você esvazia um vaso contendo duas flores?
Resposta: se o vaso não estiver vazio, você retira uma flor e depois esvazia um vaso contendo uma flor.
Como você esvazia um vaso contendo uma flor?
Resposta: se o vaso não estiver vazio, você retira uma flor e depois esvazia um vaso que não contém flores.
Como você esvazia um vaso que não contém flores?
Resposta: se o vaso não estiver vazio, retire uma flor, mas o vaso está vazio e pronto.
Isso é repetitivo. Vamos generalizar:
Como você esvazia um vaso contendo N flores?
Resposta: se o vaso não estiver vazio, retire uma flor e esvazie um vaso contendo flores N-1 .
Hmm, podemos ver isso no código?
void emptyVase( int flowersInVase ) {
if( flowersInVase > 0 ) {
// take one flower and
emptyVase( flowersInVase - 1 ) ;
} else {
// the vase is empty, nothing to do
}
}
Hmm, não poderíamos ter feito isso em um loop for?
Por que, sim, a recursão pode ser substituída pela iteração, mas geralmente a recursão é mais elegante.
Vamos conversar sobre árvores. Na ciência da computação, uma árvore é uma estrutura composta de nós , em que cada nó tem algum número de filhos que também são nós, ou nulos. Uma árvore binária é uma árvore feita de nós que possuem exatamente dois filhos, geralmente chamados "esquerdo" e "direito"; novamente os filhos podem ser nós ou nulos. Uma raiz é um nó que não é filho de nenhum outro nó.
Imagine que um nó, além de seus filhos, tenha um valor, um número e imagine que desejamos somar todos os valores em alguma árvore.
Para somar valor em qualquer nó, adicionaríamos o valor do próprio nó ao valor de seu filho esquerdo, se houver, e ao valor de seu filho direito, se houver. Agora lembre-se de que os filhos, se não forem nulos, também são nós.
Portanto, para somar o filho esquerdo, adicionamos o valor do nó filho ao valor do filho esquerdo, se houver, e ao valor do filho direito, se houver.
Portanto, para somar o valor do filho esquerdo do filho esquerdo, adicionaríamos o valor do nó filho ao valor do filho esquerdo, se houver, e ao valor do filho direito, se houver.
Talvez você tenha antecipado para onde estou indo com isso e gostaria de ver algum código? ESTÁ BEM:
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 ) ;
}
}
Observe que, em vez de testar explicitamente os filhos para ver se eles são nulos ou nós, apenas fazemos com que a função recursiva retorne zero para um nó nulo.
Digamos que tenhamos uma árvore que se parece com isso (os números são valores, as barras apontam para filhos e @ significa que o ponteiro aponta para nulo):
5
/ \
4 3
/\ /\
2 1 @ @
/\ /\
@@ @@
Se chamarmos sumNode na raiz (o nó com valor 5), retornaremos:
return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;
Vamos expandir isso no lugar. Em todo lugar que vemos sumNode, vamos substituí-lo pela expansão da declaração de retorno:
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 ;
Agora veja como conquistamos uma estrutura de profundidade arbitrária e "ramificação", considerando-a como a aplicação repetida de um modelo composto? a cada vez, através de nossa função sumNode, lidamos com apenas um único nó, usando uma única ramificação if / then e duas instruções simples de retorno que quase escreveram themsleves, diretamente de nossa especificação?
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
Esse é o poder da recursão.
O exemplo de vaso acima é um exemplo de recursão da cauda . Tudo o que significa recursão final é que, na função recursiva, se recorremos (ou seja, se chamamos a função novamente), essa foi a última coisa que fizemos.
O exemplo da árvore não era recursivo de cauda, porque mesmo que a última coisa que fizemos foi retribuir o filho certo, antes de fazê-lo, recursivamos o filho esquerdo.
De fato, a ordem na qual chamamos os filhos e adicionamos o valor do nó atual não importava, porque a adição é comutativa.
Agora vamos ver uma operação em que a ordem é importante. Usaremos uma árvore binária de nós, mas desta vez o valor mantido será um caractere, não um número.
Nossa árvore terá uma propriedade especial: para qualquer nó, seu caractere vem depois (em ordem alfabética) do caractere mantido por seu filho esquerdo e antes (em ordem alfabética) do caractere mantido por seu filho direito.
O que queremos fazer é imprimir a árvore em ordem alfabética. Isso é fácil, dada a propriedade especial da árvore. Nós apenas imprimimos o filho esquerdo, o caractere do nó e o filho direito.
Não queremos apenas imprimir à vontade, então passaremos à nossa função algo para imprimir. Este será um objeto com uma função print (char); não precisamos nos preocupar com o funcionamento, apenas quando a impressão é chamada, ela imprime algo em algum lugar.
Vamos ver isso no código:
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 );
Além da ordem das operações agora importantes, este exemplo ilustra que podemos passar as coisas para uma função recursiva. A única coisa que precisamos fazer é garantir que, a cada chamada recursiva, continuemos a repassá-la. Passamos um ponteiro de nó e uma impressora para a função e, em cada chamada recursiva, passamos "inativos".
Agora, se nossa árvore estiver assim:
k
/ \
h n
/\ /\
a j @ @
/\ /\
@@ i@
/\
@@
O que vamos imprimir?
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
Portanto, se apenas olharmos para as linhas, fomos impressos:
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
Vimos que imprimimos "ahijkn", que está de fato em ordem alfabética.
Conseguimos imprimir uma árvore inteira, em ordem alfabética, apenas sabendo como imprimir um único nó em ordem alfabética. O que era justo (porque nossa árvore tinha a propriedade especial de ordenar valores à esquerda dos valores em ordem alfabética mais tarde) saber imprimir o filho esquerdo antes de imprimir o valor do nó e imprimir o filho certo depois de imprimir o valor do nó.
E esse é o poder da recursão: ser capaz de fazer coisas inteiras, sabendo apenas como fazer uma parte do todo (e sabendo quando parar de se repetir).
Lembrando que, na maioria dos idiomas, operador || ("ou") curto-circuito quando seu primeiro operando for verdadeiro, a função recursiva geral é:
void recurse() { doWeStop() || recurse(); }
Luc M comenta:
O SO deve criar um crachá para esse tipo de resposta. Parabéns!
Obrigado Luc! Mas, na verdade, porque editei esta resposta mais de quatro vezes (para adicionar o último exemplo, mas principalmente para corrigir erros de digitação e aperfeiçoá-la - digitar um minúsculo teclado de netbook é difícil), não consigo mais obter pontos por isso . O que me desencoraja de colocar tanto esforço em respostas futuras.
Veja meu comentário aqui sobre isso: /programming/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699