Há momentos em que usar recursão é melhor do que usar um loop, e momentos em que usar um loop é melhor do que usar recursão. Escolher a opção "certa" pode economizar recursos e / ou resultar em menos linhas de código.
Existem casos em que uma tarefa só pode ser executada usando recursão, em vez de um loop?
INC (r)
,JZDEC (r, z)
pode implementar uma máquina de Turing. Não tem 'recursão' - isso é um salto se zero mais DECrement. Se a função Ackermann é computável (é), essa máquina de registro pode fazê-lo.Respostas:
Sim e não. Por fim, não há nada que a recursão possa calcular que o loop não pode, mas o loop requer muito mais encanamento. Portanto, a única coisa que a recursão pode fazer que os loops não podem é tornar algumas tarefas super fáceis.
Tome andando uma árvore. Andar em uma árvore com recursão é estúpido e fácil. É a coisa mais natural do mundo. Andar em uma árvore com laços é muito menos direto. É necessário manter uma pilha ou alguma outra estrutura de dados para acompanhar o que você fez.
Muitas vezes, a solução recursiva para um problema é mais bonita. Esse é um termo técnico e importa.
fonte
A
que encontra algo em uma árvore. SempreA
que encontra essa coisa, ela lança outra função recursivaB
que encontra uma coisa relacionada na subárvore na posição em que foi lançadaA
. Uma vezB
finalizada a recursão, ela retornaA
e a última continua sua própria recursão. Pode-se declarar uma pilha paraA
e uma paraB
, ou colocar aB
pilha dentro doA
loop. Se alguém insistir em usar uma única pilha, as coisas ficam realmente complicadas.Therefore, the one thing recursion can do that loops can't is make some tasks super easy.
E a única coisa que os loops podem fazer e não a recursão é tornar algumas tarefas super fáceis. Você já viu as coisas feias e não intuitivas que você precisa fazer para transformar os problemas iterativos mais naturais de recursão ingênua em recursão posterior, para que eles não explodam?map
oufold
(na verdade, se você optar por considerá-los primitivos, eu acho que você pode usarfold
/unfold
como uma terceira alternativa para laços ou recursão). A menos que você esteja escrevendo o código da biblioteca, não há muitos casos em que você deveria se preocupar com a implementação da iteração, e não com a tarefa que ela deveria estar realizando - na prática, isso significa que loops explícitos e recursão explícita são igualmente pobres. abstrações que devem ser evitadas no nível superior.Não.
Descer até os próprios fundamentos dos mínimos necessários, a fim de calcular, você só precisa ser capaz de loop (isso por si só não é suficiente, mas é um componente necessário). Não importa como .
Qualquer linguagem de programação que possa implementar uma máquina de Turing é chamada de Turing complete . E há muitos idiomas que estão completos.
Minha língua favorita no caminho de "isso realmente funciona?" A completude de Turing é a do FRACTRAN , que é Turing completo . Ele possui uma estrutura de loop e você pode implementar uma máquina de Turing. Assim, qualquer coisa que seja computável, pode ser implementada em uma linguagem que não tenha recursão. Portanto, não há nada que a recursão possa fornecer em termos de computabilidade que um loop simples não pode.
Isso realmente se resume a alguns pontos:
Isso não quer dizer que existem algumas classes de problemas que são mais facilmente pensadas com recursão do que com loop ou com loop ao invés de com recursão. No entanto, essas ferramentas também são igualmente poderosas.
E embora eu tenha levado isso ao extremo 'esolang' (principalmente porque você pode encontrar coisas que Turing estão completas e implementadas de maneiras bastante estranhas), isso não significa que os esolangs sejam de qualquer forma opcionais. Existe uma lista completa de coisas que são acidentalmente concluídas por Turing, incluindo os modelos Magic the Gathering, Sendmail, MediaWiki e o sistema de tipos Scala. Muitos deles estão longe de ser ótimos quando se trata de realmente fazer algo prático, mas você pode calcular qualquer coisa que seja computável usando essas ferramentas.
Essa equivalência pode se tornar particularmente interessante quando você entra em um tipo específico de recursão conhecido como chamada final .
Se você tem, digamos, um método fatorial escrito como:
Esse tipo de recursão será reescrito como um loop - nenhuma pilha usada. Tais abordagens são realmente mais elegantes e fáceis de entender do que o loop equivalente sendo gravado, mas, novamente, para cada chamada recursiva pode haver um loop equivalente gravado e para cada loop pode ser gravada uma chamada recursiva.
Há também momentos em que a conversão do loop simples em uma chamada recursiva de chamada de cauda pode ser complicada e mais difícil de entender.
Se você quiser entrar no lado da teoria, consulte a tese de Church Turing . Você também pode achar útil a tese de revisão da igreja no CS.SE.
fonte
Você sempre pode transformar o algoritmo recursivo em um loop, que usa uma estrutura de dados Last-In-First-Out (pilha AKA) para armazenar o estado temporário, porque a chamada recursiva é exatamente isso, armazenando o estado atual em uma pilha, prosseguindo com o algoritmo, depois, restaurando o estado. A resposta é tão curta: Não, não existem casos assim .
No entanto, um argumento pode ser feito para "sim". Vamos dar um exemplo concreto e fácil: mesclar classificação. Você precisa dividir os dados em duas partes, mesclar as partes e combiná-las. Mesmo se você não fizer uma chamada de função da linguagem de programação real para mesclar a classificação, a fim de mesclar as partes, será necessário implementar uma funcionalidade idêntica à real de uma chamada de função (pressione o estado em sua própria pilha, vá para início do loop com diferentes parâmetros de partida e depois pop o estado da sua pilha).
É recursão, se você implementar a chamada de recursão, como etapas separadas de "push state" e "jump to begin" e "pop state"? E a resposta para isso é: Não, ainda não é chamado de recursão, é chamado de iteração com pilha explícita (se você deseja usar a terminologia estabelecida).
Observe que isso também depende da definição de "tarefa". Se a tarefa é classificar, você pode fazê-lo com muitos algoritmos, muitos dos quais não precisam de nenhum tipo de recursão. Se a tarefa é implementar um algoritmo específico, como a classificação de mesclagem, aplica-se a ambiguidade acima.
Então, vamos considerar a questão: existem tarefas gerais, para as quais existem apenas algoritmos semelhantes à recursão. Do comentário de @WizardOfMenlo sob a questão, a função Ackermann é um exemplo simples disso. Portanto, o conceito de recursão é autônomo, mesmo que possa ser implementado com uma construção de programa de computador diferente (iteração com pilha explícita).
fonte
Depende de quão estritamente você define "recursão".
Se exigirmos estritamente que envolva a pilha de chamadas (ou qualquer mecanismo para manter o estado do programa usado), sempre poderemos substituí-lo por algo que não o faça. De fato, as linguagens que levam naturalmente ao uso pesado de recursão tendem a ter compiladores que fazem uso intenso da otimização de chamada de cauda; portanto, o que você escreve é recursivo, mas o que você executa é iterativo.
Mas vamos considerar um caso em que fazemos uma chamada recursiva e usamos o resultado de uma chamada recursiva para essa chamada recursiva.
É fácil fazer a primeira chamada recursiva iterativa:
Em seguida, podemos limpar o dispositivo
goto
para afastar os velociraptores e a sombra de Dijkstra:Mas, para remover as outras chamadas recursivas, teremos que armazenar os valores de algumas chamadas em uma pilha:
Agora, quando consideramos o código fonte, certamente transformamos nosso método recursivo em um método iterativo.
Considerando o que isso foi compilado, transformamos o código que usa a pilha de chamadas para implementar recursão em código que não o faz (e, ao fazer isso, transformou o código que lançará uma exceção de excesso de pilha para valores muito pequenos em código que apenas demore muito tempo para retornar [consulte Como posso impedir que minha função Ackerman transborde a pilha? para algumas otimizações adicionais que fazem com que ela realmente retorne para muitas outras entradas possíveis]).
Considerando como a recursão é implementada geralmente, transformamos o código que usa a pilha de chamadas em código que usa uma pilha diferente para manter operações pendentes. Poderíamos, portanto, argumentar que ainda é recursivo, quando considerado nesse nível baixo.
E nesse nível, de fato não há outras maneiras de contornar isso. Portanto, se você considera esse método recursivo, há realmente coisas que não podemos fazer sem ele. Geralmente, embora não rotulemos esse código como recursivo. O termo recursão é útil porque cobre um certo conjunto de abordagens e nos dá uma maneira de falar sobre elas, e não estamos mais usando uma delas.
Claro, tudo isso pressupõe que você tem uma escolha. Existem dois idiomas que proíbem chamadas recursivas e idiomas que não possuem as estruturas de loop necessárias para a iteração.
fonte
A resposta clássica é "não", mas permita-me explicar por que acho que "sim" é uma resposta melhor.
Antes de prosseguir, vamos desviar o caminho: do ponto de vista da computabilidade e da complexidade:
Ok, agora, vamos colocar um pé na área de prática, mantendo o outro pé na terra da teoria.
A pilha de chamadas é uma estrutura de controle , enquanto uma pilha manual é uma estrutura de dados . Controle e dados não são conceitos iguais, mas são equivalentes no sentido de que podem ser reduzidos um ao outro (ou "emulados" um pelo outro) do ponto de vista da computabilidade ou da complexidade.
Quando essa distinção é importante? Quando você trabalha com ferramentas do mundo real. Aqui está um exemplo:
Digamos que você esteja implementando o N-way
mergesort
. Você pode ter umfor
loop que percorra cada um dosN
segmentos, os chamamergesort
separadamente e depois mescla os resultados.Como você pode paralelizar isso com o OpenMP?
No domínio recursivo, é extremamente simples: basta colocar
#pragma omp parallel for
seu loop que vai de 1 a N e pronto. No domínio iterativo, você não pode fazer isso. Você precisa gerar threads manualmente e passar os dados apropriados manualmente para que eles saibam o que fazer.Por outro lado, existem outras ferramentas (como vetorizadores automáticos, por exemplo
#pragma vector
) que funcionam com loops, mas são totalmente inúteis com recursão.A propósito, apenas porque você pode provar que os dois paradigmas são equivalentes matematicamente, isso não significa que eles sejam iguais na prática. Um problema que pode ser trivial de automatizar em um paradigma (por exemplo, paralelização de loop) pode ser muito mais difícil de resolver no outro paradigma.
ou seja: as ferramentas para um paradigma não se traduzem automaticamente para outros paradigmas.
Conseqüentemente, se você precisar de uma ferramenta para resolver um problema, é provável que a ferramenta funcione apenas com um tipo específico de abordagem e, consequentemente, você não conseguirá resolver o problema com uma abordagem diferente, mesmo se puder provar matematicamente que o problema pode ser resolvido de qualquer maneira.
fonte
Deixando de lado o raciocínio teórico, vamos dar uma olhada em como são a recursão e os loops do ponto de vista da máquina (hardware ou virtual). Recursão é uma combinação de fluxo de controle que permite iniciar a execução de algum código e retornar após a conclusão (em uma visão simplista quando sinais e exceções são ignorados) e de dados que são passados para esse outro código (argumentos) e retornados de (resultado). Geralmente, nenhum gerenciamento de memória explícita está envolvido; no entanto, há alocação implícita de memória da pilha para salvar endereços de retorno, argumentos, resultados e dados locais intermediários.
Um loop é uma combinação de fluxo de controle e dados locais. Comparando isso com recursão, podemos ver que a quantidade de dados nesse caso é fixa. A única maneira de quebrar essa limitação é usar a memória dinâmica (também conhecida como heap ) que pode ser alocada (e liberada) sempre que necessário.
Para resumir:
Supondo que a parte do fluxo de controle seja razoavelmente poderosa, a única diferença está nos tipos de memória disponíveis. Então, ficamos com 4 casos (o poder da expressividade está listado entre parênteses):
Se as regras do jogo forem um pouco mais rígidas e a implementação recursiva não permitir o uso de loops, obtemos o seguinte:
A principal diferença no cenário anterior é que a falta de memória da pilha não permite que a recursão sem loops execute mais etapas durante a execução do que as linhas de código.
fonte
Sim. Existem várias tarefas comuns que são fáceis de executar usando recursão, mas impossíveis com apenas loops:
fonte
Há uma diferença entre funções recursivas e funções recursivas primitivas. Funções recursivas primitivas são aquelas calculadas usando loops, em que a contagem máxima de iterações de cada loop é calculada antes do início da execução do loop. (E "recursivo" aqui não tem nada a ver com o uso de recursão).
As funções recursivas primitivas são estritamente menos poderosas que as funções recursivas. Você obteria o mesmo resultado se assumisse funções que usam recursão, em que a profundidade máxima da recursão deve ser calculada previamente.
fonte
Se você está programando em c ++ e usa o c ++ 11, há uma coisa a ser feita usando recursões: funções constexpr. Mas o padrão limita isso a 512, conforme explicado nesta resposta . O uso de loops nesse caso não é possível, pois, nesse caso, a função não pode ser constexpr, mas isso é alterado no c ++ 14.
fonte
fonte
Eu concordo com as outras perguntas. Não há nada que você possa fazer com recursão que não possa fazer com um loop.
MAS , na minha opinião, a recursão pode ser muito perigosa. Primeiro, para alguns, é mais difícil entender o que realmente está acontecendo no código. Segundo, pelo menos para C ++ (Java, não tenho certeza), cada etapa da recursão afeta a memória porque cada chamada de método causa acúmulo de memória e inicialização do cabeçalho dos métodos. Dessa forma, você pode aumentar sua pilha. Simplesmente tente a recursão dos números de Fibonacci com um alto valor de entrada.
fonte