Eu usei bastante a recursão nos meus muitos anos de programação para resolver problemas simples, mas tenho plena consciência de que às vezes você precisa de iteração devido a problemas de memória / velocidade.
Então, em algum momento no passado, tentei descobrir se havia alguma maneira "padrão" ou de livro didático de transformar uma abordagem de recursão comum à iteração e não encontrei nada. Ou pelo menos nada do que me lembre ajudaria.
- Existem regras gerais?
- Existe um "padrão"?
recursion
computer-science
theory
iteration
Gustavo Carreno
fonte
fonte
Respostas:
Normalmente, substituo um algoritmo recursivo por um algoritmo iterativo, pressionando os parâmetros que normalmente seriam passados para a função recursiva em uma pilha. Na verdade, você está substituindo a pilha de programas por uma de sua preferência.
Nota: se você tiver mais de uma chamada recursiva e deseja preservar a ordem das chamadas, adicione-as na ordem inversa à pilha:
deve ser substituído por
Edit: O artigo Eliminação de pilhas e recursões (ou link de backup do artigo ) entra em mais detalhes sobre este assunto.
fonte
(node)->()
por(node)->[actions]
onde está a ação() -> [actions]
. Então, do lado de fora, você apenas retira uma ação / continuação da pilha, aplica / executa, empurra as ações retornadas na pilha na ordem inversa e repetida. Travessias contingentes / complexas, você apenas captura o que teriam sido variáveis de pilha local em ponteiros contados por referência que você fecha nos seus thunks, depois os thunks subsequentes podem depender dos resultados das sub-travessias anteriores, etc.new
, podemos criar um objeto na pilha em vez da pilha. Diferentemente da pilha, o heap não possui restrições de memória. Veja gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.htmlRealmente, a maneira mais comum de fazer isso é manter sua própria pilha. Aqui está uma função quicksort recursiva em C:
Veja como podemos torná-lo iterativo, mantendo nossa própria pilha:
Obviamente, este exemplo não verifica os limites da pilha ... e realmente você pode dimensionar a pilha com base no pior caso, com os valores esquerdo e direito. Mas você entendeu.
fonte
O(N) = O(R*L)
, ondeL
está a soma da complexidade "para a camada r"; por exemplo, nesse caso, vocêO(N)
trabalha em cada etapa da particionamento, a profundidade recursiva éO(R)
, ou seja, no pior casoO(N)
, o caso médioO(logN)
aqui.Parece que ninguém abordou onde a função recursiva se chama mais de uma vez no corpo e lida com o retorno a um ponto específico na recursão (ou seja, não primitivo-recursivo). Dizem que toda recursão pode ser transformada em iteração , portanto parece que isso deve ser possível.
Eu apenas vim com um exemplo de C # de como fazer isso. Suponha que você tenha a seguinte função recursiva, que age como uma passagem pós-ordem, e que AbcTreeNode é uma árvore de 3 árias com ponteiros a, b, c.
A solução iterativa:
fonte
Esforce-se para fazer sua chamada recursiva Tail Recursion (recursão em que a última instrução é a chamada recursiva). Depois disso, a conversão para iteração geralmente é bastante fácil.
fonte
Bem, em geral, a recursão pode ser imitada como iteração simplesmente usando uma variável de armazenamento. Observe que recursão e iteração são geralmente equivalentes; um quase sempre pode ser convertido no outro. Uma função recursiva da cauda é facilmente convertida em uma iterativa. Apenas torne a variável acumuladora local e itere em vez de recursar. Aqui está um exemplo em C ++ (C não fosse pelo uso de um argumento padrão):
Conhecendo-me, provavelmente cometi um erro no código, mas a ideia está aí.
fonte
Mesmo usando a pilha não converterá um algoritmo recursivo em iterativo. Recursão normal é recursão baseada em função e, se usarmos pilha, ela se tornará recursão baseada em pilha. Mas ainda é recursão.
Para algoritmos recursivos, a complexidade do espaço é O (N) e a complexidade do tempo é O (N). Para algoritmos iterativos, a complexidade do espaço é O (1) e a complexidade do tempo é O (N).
Mas se usarmos as coisas da pilha em termos de complexidade, permaneceremos as mesmas. Eu acho que apenas a recursão da cauda pode ser convertida em iteração.
fonte
copy = new int[size]; for(int i=0; i<size; ++i) copy[i] = source[i];
espaço da memória e da complexidade do tempo, ambos O (N) com base no tamanho dos dados, mas é claramente um algoritmo iterativo.O artigo sobre eliminação de pilhas e recursões captura a idéia de externalizar o quadro da pilha no heap, mas não fornece uma maneira simples e repetível de converter. Abaixo está um.
Ao converter para código iterativo, é preciso estar ciente de que a chamada recursiva pode ocorrer a partir de um bloco de código arbitrariamente profundo. Não são apenas os parâmetros, mas também o ponto de retornar à lógica que resta a ser executada e ao estado das variáveis que participam das condicionais subsequentes, que são importantes. Abaixo está uma maneira muito simples de converter em código iterativo com menos alterações.
Considere este código recursivo:
Código iterativo:
Observe como a estrutura do código ainda permanece fiel à lógica recursiva e as modificações são mínimas, resultando em menor número de bugs. Para comparação, marquei as alterações com ++ e -. A maioria dos novos blocos inseridos, exceto v.push_back, é comum a qualquer lógica iterativa convertida
+++++++++++++++++++++++++
------------------------
+++++++++++++++++++++++++
-------------------------
+++++++++++++++++++++++++
-------------------------
+++++++++++++++++++++++++
-------------------------
fonte
stackitem
objetos são alocados com um valor de lixo parara
. Tudo ainda funciona no caso mais parecido, mas,ra
por coincidência, é 1 ou 2, você terá um comportamento incorreto. A solução é inicializarra
em 0.stackitem
não deve ser pressionado sem inicializar. Mas sim, inicializar com 0 detectaria erros.v.pop_back()
declaração?Pesquise no google por "Estilo de passagem contínua". Existe um procedimento geral para converter para um estilo recursivo de cauda; também existe um procedimento geral para transformar funções recursivas da cauda em loops.
fonte
Apenas matando o tempo ... Uma função recursiva
pode ser convertido para
fonte
Geralmente, a técnica para evitar o estouro de pilha é para funções recursivas, chamada técnica de trampolim, amplamente adotada pelos desenvolvedores Java.
No entanto, para C #, existe um pequeno método auxiliar aqui que transforma seu função recursiva para iterativo sem exigir a lógica mudança ou tornar o código in-compreensível. C # é uma linguagem tão agradável que coisas incríveis são possíveis com ela.
Ele funciona agrupando partes do método por um método auxiliar. Por exemplo, a seguinte função recursiva:
Torna-se em:
fonte
Pensando em coisas que realmente precisam de uma pilha:
Se considerarmos o padrão de recursão como:
Por exemplo, a clássica torre de Hanói
Isso pode ser traduzido em um loop que trabalha em uma pilha explícita, atualizando-o como:
Para a Torre de Hanói, isso se torna:
Aqui há uma flexibilidade considerável sobre como você define sua pilha. Você pode fazer da sua pilha uma lista de
Command
objetos que fazem coisas sofisticadas. Ou você pode ir na direção oposta e fazer uma lista de tipos mais simples (por exemplo, uma "tarefa" pode ser de 4 elementos em uma pilha deint
, em vez de um elemento em uma pilha deTask
).Tudo isso significa que a memória da pilha está na pilha e não na pilha de execução Java, mas isso pode ser útil, pois você tem mais controle sobre ela.
fonte
Um padrão a ser procurado é uma chamada de recursão no final da função (chamada recursão de cauda). Isso pode ser facilmente substituído por um tempo. Por exemplo, a função foo:
termina com uma chamada para foo. Isso pode ser substituído por:
o que elimina a segunda chamada recursiva.
fonte
Uma pergunta que foi encerrada como duplicata desta tinha uma estrutura de dados muito específica:
O nó tinha a seguinte estrutura:
A função de exclusão recursiva parecia com:
Em geral, nem sempre é possível evitar uma pilha para funções recursivas que se chamam mais de uma vez (ou mesmo uma vez). No entanto, para essa estrutura específica, é possível. A idéia é achatar todos os nós em uma única lista. Isso é feito colocando os nós atuais
child
no final da lista da linha superior.Essa técnica pode ser aplicada a qualquer estrutura vinculada a dados que possa ser reduzida a um DAG com uma ordem topológica determinística. Os nós dos filhos atuais são reorganizados para que o último filho adote todos os outros filhos. Em seguida, o nó atual pode ser excluído e a travessia pode iterar para o filho restante.
fonte
A recursão nada mais é do que o processo de chamar uma função da outra, somente esse processo é feito chamando uma função por si só. Como sabemos quando uma função chama a outra, a primeira função salva seu estado (suas variáveis) e passa o controle para a função chamada. A função chamada pode ser chamada usando o mesmo nome de variáveis ex fun1 (a) pode chamar fun2 (a). Quando fazemos chamadas recursivas, nada de novo acontece. Uma função chama a si mesma passando o mesmo tipo e similar nas variáveis de nome (mas, obviamente, os valores armazenados nas variáveis são diferentes, apenas o nome permanece o mesmo.) Para si mesmo. Porém, antes de cada chamada, a função salva seu estado e esse processo de economia continua. A ECONOMIA É FEITO EM UMA PILHA.
Agora a pilha entra em jogo.
Portanto, se você escrever um programa iterativo e salvar o estado em uma pilha de cada vez e depois exibir os valores da pilha quando necessário, você converteu com êxito um programa recursivo em um iterativo!
A prova é simples e analítica.
Na recursão, o computador mantém uma pilha e, na versão iterativa, você precisará manter a pilha manualmente.
Pense bem, basta converter um programa recursivo de pesquisa em profundidade (em gráficos) em um programa iterativo dfs.
Muito bem sucedida!
fonte
Outro exemplo simples e completo de transformar a função recursiva em iterativa usando a pilha.
fonte
Uma descrição aproximada de como um sistema pega qualquer função recursiva e a executa usando uma pilha:
Isso pretendia mostrar a ideia sem detalhes. Considere esta função que imprimiria nós de um gráfico:
Por exemplo, gráfico: A-> B A-> C mostra (A) imprimiria B, A, C
As chamadas de função significam salvar o estado local e o ponto de continuação para que você possa voltar e pular a função que deseja chamar.
Por exemplo, suponha que o show (A) comece a ser executado. A chamada de função na linha 3. show (B) significa - Adicione um item à pilha, significando "você precisará continuar na linha 2 com o estado variável local node = A" - Vá para a linha 0 com o nó = B.
Para executar o código, o sistema executa as instruções. Quando uma chamada de função é encontrada, o sistema envia as informações necessárias para voltar para onde estava, executa o código da função e, quando a função é concluída, exibe as informações sobre para onde precisa continuar.
fonte
Este link fornece algumas explicações e propõe a idéia de manter o "local" para poder chegar ao local exato entre várias chamadas recursivas:
No entanto, todos esses exemplos descrevem cenários em que uma chamada recursiva é feita uma quantidade fixa de vezes. As coisas ficam mais complicadas quando você tem algo como:
fonte
Existe uma maneira geral de converter a passagem recursiva em iterador usando um iterador lento que concatena vários fornecedores de iteradores (expressão lambda que retorna um iterador). Veja meu Convertendo Recursivo Traversal para Iterator .
fonte
Meus exemplos estão no Clojure, mas deve ser bastante fácil de traduzir para qualquer idioma.
Dada esta função que
StackOverflow
é para grandes valores de n:podemos definir uma versão que usa sua própria pilha da seguinte maneira:
onde
return
é definido como:Isso também funciona para funções mais complexas, por exemplo, a função ackermann :
pode ser transformado em:
fonte