Eu quase entendo como a recursão da cauda funciona e a diferença entre ela e uma recursão normal. I única não entendo por que ele não requer pilha para lembrar o seu endereço de retorno.
// tail recursion
int fac_times (int n, int acc) {
if (n == 0) return acc;
else return fac_times(n - 1, acc * n);
}
int factorial (int n) {
return fac_times (n, 1);
}
// normal recursion
int factorial (int n) {
if (n == 0) return 1;
else return n * factorial(n - 1);
}
Não há nada a fazer depois de chamar uma função em uma função de recursão de cauda, mas isso não faz sentido para mim.
c
algorithm
recursion
tail-recursion
Alan Coromano
fonte
fonte
-O3
. O link é para uma discussão anterior que aborda assuntos muito semelhantes e discute o que é necessário para implementar essa otimização.Respostas:
O compilador é simplesmente capaz de transformar isso
em algo como isto:
fonte
Você pergunta por que "ele não requer pilha para lembrar seu endereço de retorno".
Eu gostaria de mudar isso. Ele faz usar a pilha de lembrar o endereço de retorno. O truque é que a função na qual a recursão da cauda ocorre tem seu próprio endereço de retorno na pilha e, quando salta para a função chamada, o trata como seu próprio endereço de retorno.
Concretamente, sem otimização de chamada de cauda:
Nesse caso, quando
g
é chamado, a pilha terá a seguinte aparência:Por outro lado, com a otimização da chamada de cauda:
Nesse caso, quando
g
é chamado, a pilha terá a seguinte aparência:Claramente, quando
g
retornar, ele retornará ao local de ondef
foi chamado.EDIT : O exemplo acima usa o caso em que uma função chama outra função. O mecanismo é idêntico quando a função se chama.
fonte
A recursão da cauda geralmente pode ser transformada em loop pelo compilador, especialmente quando acumuladores são usados.
compilaria para algo como
fonte
Existem dois elementos que devem estar presentes em uma função recursiva:
Uma função recursiva "regular" mantém (2) no quadro da pilha.
Os valores retornados na função recursiva regular são compostos por dois tipos de valores:
Vejamos o seu exemplo:
O quadro f (5) "armazena" o resultado de sua própria computação (5) e o valor de f (4), por exemplo. Se eu chamar fatorial (5), logo antes das chamadas da pilha começarem a entrar em colapso, eu tenho:
Observe que cada pilha armazena, além dos valores mencionados, todo o escopo da função. Portanto, o uso de memória para uma função recursiva f é O (x), onde x é o número de chamadas recursivas que tenho que fazer. Portanto, se eu precisar de 1kb de RAM para calcular o fatorial (1) ou fatorial (2), preciso de ~ 100k para calcular o fatorial (100), e assim por diante.
Uma função Tail recursiva coloca (2) seus argumentos.
Em uma recursão de cauda, passo o resultado dos cálculos parciais em cada quadro recursivo para o próximo usando parâmetros. Vamos ver o nosso exemplo fatorial, Tail Recursive:
int fatorial (int n) {int helper (int num, int acumulado) {se num == 0 retorno acumulado senão return helper (num - 1, acumulado * num)} return helper (n, 1)
}
Vejamos seus quadros em fatorial (4):
Vê as diferenças? Nas chamadas recursivas "regulares", as funções de retorno compõem recursivamente o valor final. Na recursão da cauda, eles referenciam apenas o caso base (o último avaliado) . Chamamos acumulador de argumento que acompanha os valores mais antigos.
Modelos de recursão
A função recursiva regular é a seguinte:
Para transformá-lo em uma recursão de cauda, nós:
Veja:
Veja a diferença?
Otimização de chamada de cauda
Como nenhum estado está sendo armazenado nas pilhas Não-Fronteiras das Chamadas de Chamada, elas não são tão importantes. Alguns idiomas / intérpretes substituem a pilha antiga pela nova. Portanto, sem quadros de pilha restringindo o número de chamadas, as Chamadas Tail se comportam como um loop for nesses casos.
Cabe ao seu compilador otimizar, ou não.
fonte
Aqui está um exemplo simples que mostra como as funções recursivas funcionam:
A recursão de cauda é uma função recursiva simples, onde a recorrência é feita no final da função, portanto, nenhum código é feito em ascensão, o que ajuda a maioria dos compiladores de linguagens de programação de alto nível a fazer o que é conhecido como Otimização de recursão de cauda , também possui um otimização mais complexa, conhecida como módulo de recursão da cauda
fonte
A função recursiva é uma função que chama por si só
Ele permite que os programadores escrevam programas eficientes usando uma quantidade mínima de código .
A desvantagem é que eles podem causar loops infinitos e outros resultados inesperados se não forem escritos corretamente .
Explicarei as funções Recursiva Simples e Recursiva de Cauda
Para escrever uma função recursiva simples
Do exemplo dado:
Do exemplo acima
É o fator decisivo quando sair do loop
O processamento real deve ser feito
Deixe-me interromper a tarefa, uma a uma, para facilitar o entendimento.
Vamos ver o que acontece internamente se eu correr
fact(4)
If
loop falhar, então ele vai paraelse
loop, então ele retorna4 * fact(3)
Na memória da pilha, temos
4 * fact(3)
Substituindo n = 3
If
loop falhar, então ele vai paraelse
loopentão retorna
3 * fact(2)
Lembre-se de que chamamos `` `` 4 * fact (3) ``
A saída para
fact(3) = 3 * fact(2)
Até agora, a pilha tem
4 * fact(3) = 4 * 3 * fact(2)
Na memória da pilha, temos
4 * 3 * fact(2)
Substituindo n = 2
If
loop falhar, então ele vai paraelse
loopentão retorna
2 * fact(1)
Lembre-se de que ligamos
4 * 3 * fact(2)
A saída para
fact(2) = 2 * fact(1)
Até agora, a pilha tem
4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
Na memória da pilha, temos
4 * 3 * 2 * fact(1)
Substituindo n = 1
If
loop é verdadeiroentão retorna
1
Lembre-se de que ligamos
4 * 3 * 2 * fact(1)
A saída para
fact(1) = 1
Até agora, a pilha tem
4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
Finalmente, o resultado de fato (4) = 4 * 3 * 2 * 1 = 24
A recursão da cauda seria
If
loop falhar, então ele vai paraelse
loop, então ele retornafact(3, 4)
Na memória da pilha, temos
fact(3, 4)
Substituindo n = 3
If
loop falhar, então ele vai paraelse
loopentão retorna
fact(2, 12)
Na memória da pilha, temos
fact(2, 12)
Substituindo n = 2
If
loop falhar, então ele vai paraelse
loopentão retorna
fact(1, 24)
Na memória da pilha, temos
fact(1, 24)
Substituindo n = 1
If
loop é verdadeiroentão retorna
running_total
A saída para
running_total = 24
Finalmente, o resultado de fato (4,1) = 24
fonte
Minha resposta é mais um palpite, porque recursão é algo relacionado à implementação interna.
Na recursão da cauda, a função recursiva é chamada no final da mesma função. Provavelmente o compilador pode otimizar da seguinte maneira:
Como você pode ver, estamos encerrando a função original antes da próxima iteração da mesma função, portanto, na verdade, não estamos "usando" a pilha.
Mas acredito que, se houver destruidores a serem chamados dentro da função, essa otimização pode não se aplicar.
fonte
O compilador é inteligente o suficiente para entender a recursão da cauda.No caso, ao retornar de uma chamada recursiva, não há operação pendente e a chamada recursiva é a última instrução, se enquadra na categoria de recursão da cauda. O compilador basicamente executa a otimização da recursão da cauda, removendo a implementação da pilha.
Após executar a otimização, o código acima é convertido em abaixo de um.
É assim que o compilador faz a Otimização de recursão de cauda.
fonte