Tenho visto em todo estouro de pilha, por exemplo, aqui , aqui , aqui , aqui , aqui e alguns outros eu não me importo de mencionar, que "qualquer programa que usa recursão pode ser convertido em um programa usando apenas iteração".
Havia até mesmo um altamente upvoted fio com um altamente upvoted resposta que disse sim, é possível.
Agora não estou dizendo que eles estão errados. É que essa resposta contraria meu escasso conhecimento e compreensão sobre computação.
Acredito que todas as funções iterativas podem ser expressas como recursão, e a Wikipedia tem uma declaração nesse sentido. No entanto, duvido que o inverso seja verdadeiro. Por um lado, duvido que funções recursivas não primitivas possam ser expressas iterativamente.
Também duvido que as hiperoperações possam ser expressas iterativamente.
Em sua resposta (que não entendo a propósito) à minha pergunta, o YuvalFIlmus disse que não é possível converter nenhuma sequência de operações matemáticas em uma sequência de adições.
Se a resposta de YF estiver realmente correta (acho que está, mas o raciocínio dele estava acima da minha cabeça), isso não significa que nem toda recursão pode ser convertida em iteração? Como se fosse possível converter todas as recursões em iterações, eu seria capaz de expressar todas as operações como uma sequência de adições.
Minha pergunta é esta:
Toda recursão pode ser convertida em iteração e por quê?
Por favor, responda que um colegial brilhante ou um aluno do primeiro ano do ensino médio entenderão. Obrigado.
PS: Eu não sei o que é recursiva primitiva (eu sei sobre a função Ackermann, e que ela não é recursiva primitiva, mas ainda é computável. AL1 meu conhecimento sobre isso vem da página da Wikipedia na função Ackermann).
PPS: Se a resposta for sim, você poderia, por exemplo, escrever uma versão iterativa de uma função não primitiva-recursiva. Por exemplo, Ackermann na resposta. Isso vai me ajudar a entender.
fonte
Respostas:
É possível substituir a recursão por iteração mais memória ilimitada .
Se você possui apenas iteração (digamos, enquanto loops) e uma quantidade finita de memória, tudo o que você tem é um autômato finito. Com uma quantidade finita de memória, o cálculo possui um número finito de etapas possíveis, portanto é possível simular todas elas com um autômato finito.
Ter memória ilimitada muda o negócio. Essa memória ilimitada pode assumir várias formas que acabam tendo um poder expressivo equivalente. Por exemplo, uma máquina de Turing mantém as coisas simples: há uma única fita e o computador pode avançar ou retroceder a fita um passo de cada vez - mas isso é suficiente para fazer qualquer coisa que você possa fazer com funções recursivas.
Uma máquina de Turing pode ser vista como um modelo idealizado de um computador (máquina de estado finito) com algum armazenamento extra que cresce sob demanda. Observe que é crucial que não apenas não exista um limite finito na fita, mas mesmo com a entrada, você não pode prever com segurança a quantidade de fita necessária. Se você pudesse prever (ou seja, computar) quanta fita é necessária a partir da entrada, poderia decidir se o cálculo seria interrompido calculando o tamanho máximo da fita e tratando todo o sistema, incluindo a fita agora finita, como uma máquina de estado finito .
Outra maneira de simular uma máquina de Turing com computadores é a seguinte. Simule a máquina de Turing com um programa de computador que armazena o início da fita na memória. Se o cálculo chegar ao final da parte da fita que cabe na memória, substitua o computador por um computador maior e execute o cálculo novamente.
Agora, suponha que você queira simular uma computação recursiva com um computador. As técnicas para executar funções recursivas são bem conhecidas: cada chamada de função possui um pedaço de memória, chamado de quadro de pilha . Fundamentalmente, funções recursivas podem propagar informações através de várias chamadas, passando variáveis por aí. Em termos de implementação em um computador, isso significa que uma chamada de função pode acessar o quadro de pilha de uma chamada principal (grande) * .
Um computador é um processador - uma máquina de estados finitos (com um grande número de estados, mas estamos fazendo a teoria da computação aqui, então tudo o que importa é que é finito) - juntamente com uma memória finita. O microprocessador executa um loop while gigante: “enquanto a energia estiver ligada, leia uma instrução da memória e execute-a”. (Processadores reais são muito mais complexos que isso, mas isso não afeta o que eles podem calcular, apenas a rapidez e conveniência de fazê-lo.) Um computador pode executar funções recursivas com esse loop while para fornecer iteração, além do mecanismo para acessar a memória, incluindo a capacidade de aumentar o tamanho da memória à vontade.
Se você restringir a recursão à recursividade primitiva, poderá restringir a iteração à iteração limitada . Ou seja, em vez de usar loops while com um tempo de execução imprevisível, você pode usar loops nos quais o número de iterações é conhecido no início do loop¹. O número de iterações pode não ser conhecido no início do programa: ele próprio pode ter sido calculado por loops anteriores.
Não vou nem esboçar uma prova aqui, mas há uma relação intuitiva entre ir da recursão primitiva à recursão total e passar de loops para loops enquanto: em ambos os casos, envolve não saber antecipadamente quando você vai Pare. Com recursão total, isso é feito com o operador de minimização, onde você continua até encontrar um parâmetro que satisfaça a condição. Com os loops while, isso é feito continuando até que a condição do loop seja satisfeita.
for
while
fonte
Toda recursão pode ser convertida em iteração, conforme testemunhado por sua CPU, que executa programas arbitrários usando uma iteração infinita de busca e execução. Essa é uma forma do teorema de Böhm-Jacopini . Além disso, muitos modelos de computação completos de Turing não têm recursão, por exemplo, máquinas de Turing e contra-máquinas .
Funções recursivas primitivas correspondem a programas usando iteração limitada , ou seja, você precisa especificar o número de iterações que um loop é executado com antecedência. A iteração limitada não pode simular recursão em geral, pois a função Ackermann não é recursiva primitiva. Mas a iteração ilimitada pode simular qualquer função parcialmente computável.
fonte
Eu implementei isso no Ceilão, você pode executá-lo no WebIDE , se quiser. (Ele gera a pilha no início de cada iteração do loop.)
Obviamente, isso acabou de mover a pilha de chamadas implícita da recursão para uma pilha explícita com os parâmetros e valores de retorno.
fonte
Já existem ótimas respostas (com as quais não posso nem competir), mas eu gostaria de apresentar essa explicação simples.
Recursão é apenas a manipulação da pilha de tempo de execução. A recursão adiciona um novo quadro de pilha (para a nova chamada da função recursiva) e o retorno remove um quadro de pilha (para a inovação recém-concluída da função recursiva). A recursão fará com que algum número de quadros de pilha seja adicionado / removido, até que eventualmente todos saiam (espero!) E o resultado seja retornado ao chamador.
Agora, o que aconteceria se você fizesse sua própria pilha, substituísse as chamadas de função recursiva por empurrar para a pilha, substituísse o retorno das funções recursivas por estourar a pilha e tivesse um loop while que funcionasse até que a pilha estivesse vazia?
fonte
Tanto quanto eu sei, e na minha própria experiência, você pode implementar qualquer recursão como uma iteração. Como mencionado acima, a recursão usa a pilha, que é conceitualmente ilimitada, mas praticamente limitada (você já recebeu uma mensagem de estouro de pilha?). Nos meus primeiros dias de programação (no terceiro trimestre do século passado do último milênio) eu estava usando linguagens não recursivas implementando algoritmos recursivos e não tive problemas. Eu não tenho certeza de como alguém poderia provar isso, no entanto.
fonte