O que é recursão e quando devo usá-lo?

121

Um dos tópicos que parece surgir regularmente nas listas de discussão e discussões on-line é o mérito (ou a falta dele) de se formar em Ciência da Computação. Um argumento que parece surgir repetidamente para a parte negativa é que eles estão codificando há alguns anos e nunca usaram recursão.

Então a questão é:

  1. O que é recursão?
  2. Quando eu usaria recursão?
  3. Por que as pessoas não usam recursão?
Mike Minutillo
fonte
9
E talvez isso ajude: stackoverflow.com/questions/126756/...
kennytm
3
Isso pode ajudar a entender o conceito: navegue até o link fornecido no segundo comentário da pergunta nesta página e faça o que os comentários dizem para fazer: stackoverflow.com/questions/3021/…
dtmland

Respostas:

86

Existem várias boas explicações sobre recursão neste tópico, esta resposta é sobre por que você não deve usá-lo na maioria dos idiomas. , ruby, Java e C #) iteração é muito preferível a recursividade.

Para ver o porquê, siga as etapas que os idiomas acima usam para chamar uma função:

  1. espaço é esculpido na pilha para os argumentos da função e variáveis ​​locais
  2. os argumentos da função são copiados para este novo espaço
  3. controle salta para a função
  4. o código da função é executado
  5. o resultado da função é copiado para um valor de retorno
  6. a pilha é rebobinada para sua posição anterior
  7. controle salta de volta para onde a função foi chamada

A execução de todas essas etapas leva tempo, geralmente um pouco mais do que é necessário para percorrer um loop. No entanto, o verdadeiro problema está na etapa 1. Quando muitos programas são iniciados, eles alocam um único pedaço de memória para sua pilha e, quando ficam sem essa memória (geralmente, mas nem sempre devido à recursão), o programa trava devido a um estouro de pilha .

Portanto, nesses idiomas, a recursão é mais lenta e o torna vulnerável a falhas. Ainda existem alguns argumentos para usá-lo. Em geral, o código escrito recursivamente é mais curto e um pouco mais elegante, depois que você souber lê-lo.

Existe uma técnica que os implementadores de linguagem podem usar chamada otimização de chamada de cauda que pode eliminar algumas classes de estouro de pilha. De maneira sucinta: se a expressão de retorno de uma função é simplesmente o resultado de uma chamada de função, você não precisa adicionar um novo nível à pilha, pode reutilizar o atual para a função que está sendo chamada. Lamentavelmente, poucas implementações de linguagem imperativas têm otimização de chamada de cauda incorporada.

* Eu amo recursão. Minha linguagem estática favorita não usa loops, a recursão é a única maneira de fazer algo repetidamente. Eu simplesmente não acho que a recursão seja geralmente uma boa idéia em idiomas que não são ajustados para isso.

** A propósito, Mario, o nome típico para a sua função ArrangeString é "join", e eu ficaria surpreso se o seu idioma de escolha ainda não tiver uma implementação.

Peter Burns
fonte
1
É bom ver uma explicação da sobrecarga inerente da recursão. Também toquei nisso na minha resposta. Mas para mim, a grande força da recursão é o que você pode fazer com a pilha de chamadas. Você pode escrever um algoritmo sucinto com recursão que se ramifica repetidamente, permitindo que você faça coisas como hierarquias de rastreamento (relacionamentos pai / filho) com facilidade. Veja minha resposta para um exemplo.
precisa
7
Muito desapontado ao encontrar a resposta principal para uma pergunta intitulada "O que é recursão e quando devo usá-lo?" na verdade, não responda a nenhuma dessas perguntas, não importa o aviso extremamente tendencioso contra a recursão, apesar do uso generalizado na maioria dos idiomas que você mencionou (não há nada especificamente errado sobre o que você disse, mas você parece estar exagerando o problema e exagerando demais a utilidade).
Bernhard Barker
2
Você provavelmente está certo @Dukeling. Por contexto, quando escrevi esta resposta, já havia muitas explicações excelentes sobre recursão e escrevi com a intenção de ser um complemento dessa informação, não a resposta principal. Na prática, quando preciso percorrer uma árvore ou manipular qualquer outra estrutura de dados aninhada, geralmente reciro à recursão e ainda não atingi o estouro de uma pilha de minha própria criação na natureza.
Peter Burns,
63

Exemplo simples de recursão em inglês.

A child couldn't sleep, so her mother told her a story about a little frog,
    who couldn't sleep, so the frog's mother told her a story about a little bear,
         who couldn't sleep, so the bear's mother told her a story about a little weasel... 
            who fell asleep.
         ...and the little bear fell asleep;
    ...and the little frog fell asleep;
...and the child fell asleep.
DMin
fonte
1
-se + para tocar o coração :)
Suhail Mumtaz Awan
Há uma história semelhante como esta para crianças pequenas que não dormem nos contos folclóricos chineses, eu acabei de me lembrar dessa e me lembra como funciona a recursão no mundo real.
Harvey Lin
49

No sentido mais básico da ciência da computação, a recursão é uma função que se autodenomina. Digamos que você tenha uma estrutura de lista vinculada:

struct Node {
    Node* next;
};

E você deseja descobrir quanto tempo uma lista vinculada pode fazer isso com recursão:

int length(const Node* list) {
    if (!list->next) {
        return 1;
    } else {
        return 1 + length(list->next);
    }
}

(Isso também pode ser feito com um loop for, mas é útil como ilustração do conceito)

Andreas Brinck
fonte
@ Christopher: Este é um exemplo simples e agradável de recursão. Especificamente, este é um exemplo de recursão da cauda. No entanto, como Andreas afirmou, ele pode ser reescrito facilmente (com mais eficiência) com um loop for. Como explico na minha resposta, há melhores usos para recursão.
Steve Wortham
2
você realmente precisa de uma declaração else aqui?
Adrien Seja
1
Não, está lá apenas para maior clareza.
Andreas Brinck
@SteveWortham: Isso não é recursivo da cauda, ​​como está escrito; length(list->next)ainda precisa retornar para length(list)que o último possa adicionar 1 ao resultado. Fomos escritos para passar o comprimento até agora, só então poderíamos esquecer que o chamador existia. Like int length(const Node* list, int count=0) { return (!list) ? count : length(list->next, count + 1); }.
cHao 02/12
46

Sempre que uma função se chama, criando um loop, isso é recursão. Como em qualquer coisa, existem bons usos e maus usos para recursão.

O exemplo mais simples é a recursão final, onde a última linha da função é uma chamada para si mesma:

int FloorByTen(int num)
{
    if (num % 10 == 0)
        return num;
    else
        return FloorByTen(num-1);
}

No entanto, este é um exemplo inútil, quase inútil, porque pode ser facilmente substituído por uma iteração mais eficiente. Afinal, a recursão sofre de sobrecarga de chamada de função, que no exemplo acima pode ser substancial em comparação com a operação dentro da própria função.

Portanto, todo o motivo para fazer recursão, em vez de iteração, deve ser aproveitar a pilha de chamadas para fazer algumas coisas inteligentes. Por exemplo, se você chamar uma função várias vezes com parâmetros diferentes dentro do mesmo loop, é uma maneira de realizar ramificações . Um exemplo clássico é o triângulo de Sierpinski .

insira a descrição da imagem aqui

Você pode desenhar um desses de maneira muito simples com recursão, onde a pilha de chamadas se ramifica em 3 direções:

private void BuildVertices(double x, double y, double len)
{
    if (len > 0.002)
    {
        mesh.Positions.Add(new Point3D(x, y + len, -len));
        mesh.Positions.Add(new Point3D(x - len, y - len, -len));
        mesh.Positions.Add(new Point3D(x + len, y - len, -len));
        len *= 0.5;
        BuildVertices(x, y + len, len);
        BuildVertices(x - len, y - len, len);
        BuildVertices(x + len, y - len, len);
    }
}

Se você tentar fazer a mesma coisa com a iteração, acho que precisará de muito mais código para realizar.

Outros casos de uso comuns podem incluir a passagem de hierarquias, por exemplo, rastreadores de sites, comparações de diretórios etc.

Conclusão

Em termos práticos, a recursão faz mais sentido sempre que você precisar de ramificações iterativas.

Steve Wortham
fonte
27

A recursão é um método de resolver problemas com base na divisão e conquista da mentalidade. A idéia básica é que você pegue o problema original e o divida em instâncias menores (resolvidas com mais facilidade), resolva essas instâncias menores (geralmente usando o mesmo algoritmo novamente) e depois remonte-as na solução final.

O exemplo canônico é uma rotina para gerar o fatorial de n. O fatorial de n é calculado multiplicando todos os números entre 1 e n. Uma solução iterativa em C # é assim:

public int Fact(int n)
{
  int fact = 1;

  for( int i = 2; i <= n; i++)
  {
    fact = fact * i;
  }

  return fact;
}

Não há nada de surpreendente na solução iterativa e deve fazer sentido para qualquer pessoa familiarizada com C #.

A solução recursiva é encontrada reconhecendo que o enésimo fator é n * Fato (n-1). Ou, em outras palavras, se você souber qual é um número fatorial específico, poderá calcular o próximo. Aqui está a solução recursiva em C #:

public int FactRec(int n)
{
  if( n < 2 )
  {
    return 1;
  }

  return n * FactRec( n - 1 );
}

A primeira parte dessa função é conhecida como Caso Base (ou, às vezes, Cláusula Guard) e é o que impede que o algoritmo seja executado para sempre. Ele apenas retorna o valor 1 sempre que a função é chamada com um valor de 1 ou menos. A segunda parte é mais interessante e é conhecida como Etapa Recursiva . Aqui, chamamos o mesmo método com um parâmetro ligeiramente modificado (o diminuímos por 1) e, em seguida, multiplicamos o resultado com nossa cópia de n.

Quando encontrado pela primeira vez, isso pode ser um pouco confuso, por isso é instrutivo examinar como funciona quando executado. Imagine que chamamos FactRec (5). Entramos na rotina, não somos apanhados pelo caso base e, assim, acabamos assim:

// In FactRec(5)
return 5 * FactRec( 5 - 1 );

// which is
return 5 * FactRec(4);

Se digitarmos novamente o método com o parâmetro 4, novamente não seremos interrompidos pela cláusula guard e, portanto, terminaremos em:

// In FactRec(4)
return 4 * FactRec(3);

Se substituirmos esse valor de retorno pelo valor de retorno acima, obtemos

// In FactRec(5)
return 5 * (4 * FactRec(3));

Isso deve lhe dar uma pista de como a solução final é alcançada, para acelerar o processo e mostrar cada etapa no caminho:

return 5 * (4 * FactRec(3));
return 5 * (4 * (3 * FactRec(2)));
return 5 * (4 * (3 * (2 * FactRec(1))));
return 5 * (4 * (3 * (2 * (1))));

Essa substituição final acontece quando o caso base é acionado. Neste ponto, temos uma fórmula algrebraica simples para resolver, que equivale diretamente à definição de fatoriais em primeiro lugar.

É instrutivo observar que toda chamada no método resulta em um caso base sendo acionado ou em uma chamada para o mesmo método em que os parâmetros estão mais próximos de um caso base (geralmente chamado de chamada recursiva). Se não for esse o caso, o método será executado para sempre.

Wolfbyte
fonte
2
Boa explicação, mas acho importante observar que isso é simplesmente recursão de cauda e não oferece vantagem sobre a solução iterativa. É aproximadamente a mesma quantidade de código e será executado mais lentamente devido à sobrecarga da chamada de função.
precisa
1
@SteveWortham: Isso não é recursão de cauda. Na etapa recursiva, o resultado de FactRec()deve ser multiplicado nantes de retornar.
rvighne
12

A recursão está resolvendo um problema com uma função que se chama. Um bom exemplo disso é uma função fatorial. Fatorial é um problema de matemática em que fatorial de 5, por exemplo, é 5 * 4 * 3 * 2 * 1. Essa função resolve isso em C # para números inteiros positivos (não testados - pode haver um erro).

public int Factorial(int n)
{
    if (n <= 1)
        return 1;

    return n * Factorial(n - 1);
}
jkohlhepp
fonte
9

Recursão refere-se a um método que resolve um problema, resolvendo uma versão menor do problema e, em seguida, usando esse resultado mais algum outro cálculo para formular a resposta ao problema original. Muitas vezes, no processo de solução da versão menor, o método resolve uma versão ainda menor do problema, e assim por diante, até chegar a um "caso base" que é trivial de resolver.

Por exemplo, para calcular um fatorial para o número X, pode-se representá-lo como X times the factorial of X-1. Assim, o método "se repete" para encontrar o fatorial de X-1, e depois multiplica o que quer que seja, Xpara dar uma resposta final. Obviamente, para encontrar o fatorial de X-1, ele primeiro calculará o fatorial de X-2, e assim por diante. O caso base seria quando Xé 0 ou 1; nesse caso, ele sabe retornar 1desde então 0! = 1! = 1.

Âmbar
fonte
1
Acho que o que você está se referindo não é recursão, mas o princípio de design do algoritmo <a href=" en.wikipedia.org/wiki/… e Conquer</a> Veja, por exemplo, o <a href = " en.wikipedia. org / wiki / Ackermann_function "> Função Ackermans </a>.
Gabriel Ščerbák
2
Não, não estou me referindo a D&C. D&C implica que existem 2 ou mais subproblemas, a recursão por si só não existe (por exemplo, o exemplo fatorial dado aqui não é D&C - é completamente linear). D&C é essencialmente um subconjunto de recursão.
Amber
3
Citado no artigo exato que você vinculou: "Um algoritmo de dividir e conquistar funciona dividindo recursivamente um problema em dois ou mais subproblemas do mesmo tipo (ou relacionados)"
Amber
Não acho que seja uma ótima explicação, pois a recursão, a rigor, não precisa resolver o problema. Você poderia simplesmente se chamar (e transbordar).
UK-AL
Estou usando sua explicação em um artigo que estou escrevendo para o PHP Master, embora não possa atribuí-la a você. Espero que você não se importe.
Frostymarvelous 30/05
9

Considere um problema antigo e bem conhecido :

Em matemática, o maior divisor comum (MDC) ... de dois ou mais números inteiros diferentes de zero, é o maior número inteiro positivo que divide os números sem deixar resto.

A definição de gcd é surpreendentemente simples:

definição de gcd

onde mod é o operador modulo (ou seja, o restante após a divisão inteira).

Em inglês, essa definição diz que o maior divisor comum de qualquer número e zero é esse número, e o maior divisor comum de dois números m e n é o maior divisor comum de n e o restante depois de dividir m por n .

Se você quiser saber por que isso funciona, consulte o artigo da Wikipedia sobre o algoritmo euclidiano .

Vamos calcular o gcd (10, 8) como um exemplo. Cada passo é igual ao anterior:

  1. mcd (10, 8)
  2. mcd (10, 10 mod 8)
  3. mcd (8, 2)
  4. mcd (8, 8 mod 2)
  5. mcd (2, 0)
  6. 2

Na primeira etapa, 8 não é igual a zero; portanto, a segunda parte da definição se aplica. 10 mod 8 = 2 porque 8 entra em 10 uma vez com o restante 2. Na etapa 3, a segunda parte se aplica novamente, mas desta vez 8 mod 2 = 0 porque 2 divide 8 sem o restante. Na etapa 5, o segundo argumento é 0, então a resposta é 2.

Você notou que o mcd aparece nos lados esquerdo e direito do sinal de igual? Um matemático diria que essa definição é recursiva porque a expressão que você está definindo se repete dentro da definição.

Definições recursivas tendem a ser elegantes. Por exemplo, uma definição recursiva para a soma de uma lista é

sum l =
    if empty(l)
        return 0
    else
        return head(l) + sum(tail(l))

onde headé o primeiro elemento de uma lista e tailo restante da lista. Observe que se sumrepete dentro de sua definição no final.

Talvez você prefira o valor máximo em uma lista:

max l =
    if empty(l)
        error
    elsif length(l) = 1
        return head(l)
    else
        tailmax = max(tail(l))
        if head(l) > tailmax
            return head(l)
        else
            return tailmax

Você pode definir a multiplicação de números inteiros não negativos recursivamente para transformá-lo em uma série de adições:

a * b =
    if b = 0
        return 0
    else
        return a + (a * (b - 1))

Se esse pouco sobre transformar a multiplicação em uma série de adições não fizer sentido, tente expandir alguns exemplos simples para ver como funciona.

A classificação de mesclagem tem uma definição recursiva adorável:

sort(l) =
    if empty(l) or length(l) = 1
        return l
    else
        (left,right) = split l
        return merge(sort(left), sort(right))

Definições recursivas estão por toda parte, se você souber o que procurar. Observe como todas essas definições têm casos base muito simples, por exemplo , gcd (m, 0) = m. Os casos recursivos diminuem o problema para chegar às respostas fáceis.

Com esse entendimento, agora você pode apreciar os outros algoritmos no artigo da Wikipedia sobre recursão !

gbacon
fonte
8
  1. Uma função que se chama
  2. Quando uma função pode ser (facilmente) decomposta em uma operação simples mais a mesma função em uma parte menor do problema. Antes, devo dizer que isso o torna um bom candidato à recursão.
  3. Eles fazem!

O exemplo canônico é o fatorial que se parece com:

int fact(int a) 
{
  if(a==1)
    return 1;

  return a*fact(a-1);
}

Em geral, a recursão não é necessariamente rápida (a sobrecarga de chamada de função tende a ser alta porque as funções recursivas tendem a ser pequenas, veja acima) e podem sofrer de alguns problemas (a pilha excede alguém?). Alguns dizem que tendem a ser difíceis de acertar em casos não triviais, mas eu realmente não acredito nisso. Em algumas situações, a recursão faz mais sentido e é a maneira mais elegante e clara de escrever uma função específica. Note-se que alguns idiomas favorecem soluções recursivas e as otimizam muito mais (LISP vem à mente).

Louis Brandy
fonte
6

Uma função recursiva é aquela que se chama. O motivo mais comum que encontrei para usá-lo é atravessar uma estrutura em árvore. Por exemplo, se eu tiver um TreeView com caixas de seleção (pense na instalação de um novo programa, na página "escolha recursos para instalar"), talvez eu queira um botão "verificar tudo", que seria algo como isto (pseudocódigo):

function cmdCheckAllClick {
    checkRecursively(TreeView1.RootNode);
}

function checkRecursively(Node n) {
    n.Checked = True;
    foreach ( n.Children as child ) {
        checkRecursively(child);
    }
}

Portanto, você pode ver que o checkRecursively primeiro verifica o nó pelo qual ele é passado e depois se chama para cada um dos filhos desse nó.

Você precisa ter um pouco de cuidado com a recursão. Se você entrar em um loop recursivo infinito, receberá uma exceção de estouro de pilha :)

Não consigo pensar em uma razão pela qual as pessoas não devem usá-lo, quando apropriado. É útil em algumas circunstâncias, e não em outras.

Eu acho que, por ser uma técnica interessante, alguns codificadores talvez acabem usando-a com mais frequência do que deveriam, sem justificativa real. Isso deu à recursão um nome ruim em alguns círculos.

Blorgbeard está fora
fonte
5

Recursão é uma expressão que se refere direta ou indiretamente a si mesma.

Considere acrônimos recursivos como um exemplo simples:

  • GNU significa Not Unix do GNU
  • PHP significa PHP: pré-processador de hipertexto
  • YAML significa YAML Ain't Markup Language
  • WINE significa Wine Is Not a Emulator
  • VISA significa Visa International Service Association

Mais exemplos na Wikipedia

Konstantin
fonte
4

A recursão funciona melhor com o que eu gosto de chamar de "problemas fractais", em que você está lidando com uma coisa grande feita de versões menores dessa coisa grande, cada uma das quais é uma versão ainda menor da coisa grande, e assim por diante. Se você precisar percorrer ou pesquisar algo como uma árvore ou estruturas idênticas aninhadas, terá um problema que pode ser um bom candidato à recursão.

As pessoas evitam a recursão por vários motivos:

  1. A maioria das pessoas (inclusive eu) se preocupa com a programação procedural ou orientada a objetos, em oposição à programação funcional. Para essas pessoas, a abordagem iterativa (normalmente usando loops) parece mais natural.

  2. Aqueles de nós que cortam nossos dentes de programação em programação procedural ou orientada a objetos costumam ser instruídos a evitar recursões porque são propensas a erros.

  3. Muitas vezes nos dizem que a recursão é lenta. Chamar e retornar de uma rotina repetidamente envolve muito empilhar e estourar, o que é mais lento que o loop. Eu acho que algumas linguagens lidam com isso melhor do que outras, e essas linguagens provavelmente não são aquelas em que o paradigma dominante é processual ou orientado a objetos.

  4. Para pelo menos algumas linguagens de programação que eu usei, lembro-me de ouvir recomendações para não usar recursão se ela ultrapassar uma certa profundidade, porque sua pilha não é tão profunda.

Joey deVilla
fonte
4

Uma declaração recursiva é aquela em que você define o processo do que fazer em seguida como uma combinação das entradas e o que você já fez.

Por exemplo, considere fatorial:

factorial(6) = 6*5*4*3*2*1

Mas é fácil ver o fatorial (6) também é:

6 * factorial(5) = 6*(5*4*3*2*1).

Então geralmente:

factorial(n) = n*factorial(n-1)

Obviamente, o mais complicado da recursão é que, se você deseja definir as coisas em termos do que já fez, é preciso que haja um lugar para começar.

Neste exemplo, apenas criamos um caso especial definindo fatorial (1) = 1.

Agora vemos de baixo para cima:

factorial(6) = 6*factorial(5)
                   = 6*5*factorial(4)
                   = 6*5*4*factorial(3) = 6*5*4*3*factorial(2) = 6*5*4*3*2*factorial(1) = 6*5*4*3*2*1

Como definimos fatorial (1) = 1, chegamos ao "fundo".

De um modo geral, os procedimentos recursivos têm duas partes:

1) A parte recursiva, que define algum procedimento em termos de novas entradas combinadas com o que você "já fez" através do mesmo procedimento. (iefactorial(n) = n*factorial(n-1) )

2) Uma peça base, que garante que o processo não se repita para sempre, dando a ele um lugar para começar (ou seja factorial(1) = 1)

No começo, pode ser um pouco confuso, mas veja alguns exemplos e tudo deve se encaixar. Se você deseja uma compreensão muito mais profunda do conceito, estude a indução matemática. Além disso, esteja ciente de que alguns idiomas otimizam para chamadas recursivas, enquanto outros não. É muito fácil criar funções recursivas incrivelmente lentas, se você não tomar cuidado, mas também existem técnicas para torná-las com desempenho na maioria dos casos.

Espero que isto ajude...

Gregory Brown
fonte
4

Eu gosto desta definição:
na recursão, uma rotina resolve uma pequena parte de um problema, divide o problema em partes menores e, em seguida, se autodenomina para resolver cada uma das partes menores.

Também gosto da discussão de Steve McConnells sobre recursão no Code Complete, onde ele critica os exemplos usados ​​nos livros de ciência da computação sobre recursão.

Não use recursão para fatoriais ou números de Fibonacci

Um problema com os livros didáticos de ciência da computação é que eles apresentam exemplos tolos de recursão. Os exemplos típicos são computar um fatorial ou computar uma sequência de Fibonacci. A recursão é uma ferramenta poderosa, e é realmente idiota usá-la em qualquer um desses casos. Se um programador que trabalhou para mim usasse recursão para calcular um fatorial, eu contrataria outra pessoa.

Eu pensei que este era um ponto muito interessante a ser levantado e pode ser uma razão pela qual a recursão é muitas vezes incompreendida.

Edição: Esta não foi uma escavação na resposta de Dav - eu não tinha visto essa resposta quando publiquei este

desconto
fonte
6
A maior parte da razão pela qual fatoriais ou seqüências de fibonacci são usadas como exemplos é porque são itens comuns que são definidos de maneira recursiva e, portanto, eles se prestam naturalmente a exemplos de recursão para calculá-los - mesmo que esse não seja o melhor método. do ponto de vista do CS.
Amber
Eu concordo - eu acabei de descobrir, quando estava lendo o livro, que era um ponto interessante a ser levantado no meio de uma seção sobre recursão.
Robben_Ford_Fan_boy
4

1.) Um método é recursivo se puder se chamar; diretamente:

void f() {
   ... f() ... 
}

ou indiretamente:

void f() {
    ... g() ...
}

void g() {
   ... f() ...
}

2.) Quando usar recursão

Q: Does using recursion usually make your code faster? 
A: No.
Q: Does using recursion usually use less memory? 
A: No.
Q: Then why use recursion? 
A: It sometimes makes your code much simpler!

3.) As pessoas usam recursão somente quando é muito complexo escrever código iterativo. Por exemplo, técnicas de passagem em árvore, como pré-encomenda e pós-encomenda, podem ser tornadas iterativas e recursivas. Mas geralmente usamos recursivos devido à sua simplicidade.

Vinisha Vyasa
fonte
Que tal reduzir a complexidade quando dividir e conquistar em relação aos perfs?
Mfrachet
4

Aqui está um exemplo simples: quantos elementos em um conjunto. (existem maneiras melhores de contar as coisas, mas este é um bom exemplo recursivo simples.)

Primeiro, precisamos de duas regras:

  1. se o conjunto estiver vazio, a contagem de itens no conjunto será zero (duh!).
  2. se o conjunto não estiver vazio, a contagem será um mais o número de itens no conjunto após a remoção de um item.

Suponha que você tenha um conjunto como este: [xxx]. vamos contar quantos itens existem.

  1. o conjunto é [xxx] que não está vazio, por isso aplicamos a regra 2. o número de itens é um mais o número de itens em [xx] (ou seja, removemos um item).
  2. como o conjunto é [xx], aplicamos a regra 2 novamente: um + número de itens em [x].
  3. o conjunto é [x], que ainda corresponde à regra 2: um + número de itens em [].
  4. Agora o conjunto é [], que corresponde à regra 1: a contagem é zero!
  5. Agora que sabemos a resposta na etapa 4 (0), podemos resolver a etapa 3 (1 + 0)
  6. Da mesma forma, agora que sabemos a resposta na etapa 3 (1), podemos resolver a etapa 2 (1 + 1)
  7. E finalmente, agora que sabemos a resposta na etapa 2 (2), podemos resolver a etapa 1 (1 + 2) e obter a contagem de itens em [xxx], que é 3. Hooray!

Podemos representar isso como:

count of [x x x] = 1 + count of [x x]
                 = 1 + (1 + count of [x])
                 = 1 + (1 + (1 + count of []))
                 = 1 + (1 + (1 + 0)))
                 = 1 + (1 + (1))
                 = 1 + (2)
                 = 3

Ao aplicar uma solução recursiva, você geralmente tem pelo menos 2 regras:

  • como base, o caso simples que indica o que acontece quando você "esgotou" todos os seus dados. Geralmente, essa é uma variação de "se você não tiver dados para processar, sua resposta será X"
  • a regra recursiva, que indica o que acontece se você ainda tiver dados. Geralmente, esse é um tipo de regra que diz "faça algo para diminuir o conjunto de dados e aplique novamente as regras ao conjunto de dados menor".

Se traduzirmos o acima em pseudocódigo, obteremos:

numberOfItems(set)
    if set is empty
        return 0
    else
        remove 1 item from set
        return 1 + numberOfItems(set)

Existem exemplos muito mais úteis (atravessando uma árvore, por exemplo) que eu tenho certeza que outras pessoas abordarão.

Mark Harrison
fonte
3

Bem, essa é uma definição bastante decente que você tem. E a Wikipedia também tem uma boa definição. Então, adicionarei outra definição (provavelmente pior) para você.

Quando as pessoas se referem à "recursão", geralmente estão falando sobre uma função que escreveram, que se chama repetidamente até terminar o trabalho. A recursão pode ser útil ao percorrer hierarquias nas estruturas de dados.

Dave Markle
fonte
3

Um exemplo: Uma definição recursiva de uma escada é: Uma escada consiste em: - uma única etapa e uma escada (recursão) - ou apenas uma única etapa (terminação)

Ralph
fonte
2

Para recorrer a um problema resolvido: não faça nada, está feito.
Para se recuperar em um problema em aberto: execute o próximo passo e depois o restante.

Peter Burns
fonte
2

Em inglês simples: suponha que você possa fazer 3 coisas:

  1. Pegue uma maçã
  2. Anote marcas de registro
  3. Contar marcas de contagem

Você tem muitas maçãs à sua frente em uma mesa e deseja saber quantas maçãs existem.

start
  Is the table empty?
  yes: Count the tally marks and cheer like it's your birthday!
  no:  Take 1 apple and put it aside
       Write down a tally mark
       goto start

O processo de repetir a mesma coisa até você terminar é chamado de recursão.

Espero que esta seja a resposta "em inglês simples" que você está procurando!

Bastiaan Linders
fonte
1
Espera, tenho muitas marcas de registro na minha frente em uma mesa e agora quero saber quantas marcas de registro existem. De alguma forma, posso usar as maçãs para isso?
Christoffer Hammarström
Se você pegar uma maçã do chão (quando você a colocou lá durante o processo) e colocá-la na mesa toda vez que riscar uma marca da lista até que não haja mais marcas, tenho certeza de que você termine com uma quantidade de maçãs na mesa igual ao número de marcas registradas que você teve. Agora apenas conte essas maçãs para obter sucesso instantâneo! (nota: este processo não é mais recursão, mas num ciclo infinito)
Bastiaan Linders
2

Uma função recursiva é uma função que contém uma chamada para si mesma. Uma estrutura recursiva é uma estrutura que contém uma instância de si mesma. Você pode combinar os dois como uma classe recursiva. A parte principal de um item recursivo é que ele contém uma instância / chamada de si mesmo.

Considere dois espelhos um de frente para o outro. Vimos o puro efeito infinito que eles produzem. Cada reflexão é uma instância de um espelho, que está contida em outra instância de um espelho, etc. O espelho que contém um reflexo de si mesmo é recursão.

Uma árvore de pesquisa binária é um bom exemplo de programação de recursão. A estrutura é recursiva com cada nó contendo 2 instâncias de um nó. As funções para trabalhar em uma árvore de pesquisa binária também são recursivas.

Mcdon
fonte
2

Esta é uma pergunta antiga, mas quero adicionar uma resposta do ponto de vista logístico (ou seja, não do ponto de vista de correção do algoritmo ou do ponto de vista do desempenho).

Eu uso Java para o trabalho, e Java não suporta função aninhada. Como tal, se eu quiser fazer recursão, talvez seja necessário definir uma função externa (que existe apenas porque meu código colide com a regra burocrática de Java) ou talvez precise refatorar o código completamente (o que eu realmente odeio).

Portanto, evito frequentemente a recursão e uso a operação de pilha, porque a recursão em si é essencialmente uma operação de pilha.

fajrian
fonte
1

Você deseja usá-lo sempre que tiver uma estrutura em árvore. É muito útil na leitura de XML.

Nick Berardi
fonte
1

A recursão aplicada à programação basicamente chama uma função de dentro de sua própria definição (dentro de si), com parâmetros diferentes para realizar uma tarefa.

bennybdbc
fonte
1

"Se eu tenho um martelo, faça tudo parecer um prego."

A recursão é uma estratégia de solução de problemas para grandes problemas, onde a cada passo apenas "transforma duas pequenas coisas em uma coisa maior", cada vez com o mesmo martelo.

Exemplo

Suponha que sua mesa esteja coberta com uma bagunça desorganizada de 1024 papéis. Como você cria uma pilha de papéis organizada e limpa usando a recursão?

  1. Dividir: espalhe todas as folhas, para que você tenha apenas uma folha em cada "pilha".
  2. Conquistar:
    1. Vá ao redor, colocando cada folha em cima de outra folha. Agora você tem pilhas de 2.
    2. Vá ao redor, colocando cada pilha 2 em cima de outra pilha 2. Agora você tem pilhas de 4.
    3. Vá ao redor, colocando cada pilha de 4 em cima de outra pilha de 4. Agora você tem pilhas de 8.
    4. ... e assim por diante ...
    5. Agora você tem uma enorme pilha de 1024 folhas!

Observe que isso é bastante intuitivo, além de contar tudo (o que não é estritamente necessário). Na verdade, talvez você não vá até as pilhas de 1 folha, mas poderia e ainda funcionaria. A parte importante é o martelo: com os braços, você sempre pode colocar uma pilha em cima da outra para criar uma pilha maior, e não importa (dentro da razão) o tamanho de qualquer pilha.

Andres Jaan Tack
fonte
6
Você está descrevendo dividir e conquistar. Embora este seja um exemplo de recursão, não é de forma alguma o único.
Konrad Rudolph
Isso é bom. Não estou tentando capturar [o mundo da recursão] [1] em uma frase, aqui. Eu quero uma explicação intuitiva. [1]: facebook.com/pages/Recursion-Fairy/269711978049
Andres Jaan Tack
1

Recursão é o processo em que uma chamada de método é capaz de executar uma determinada tarefa. Reduz a redundância de código. A maioria das funções ou métodos recursivos deve ter uma condição para interromper a chamada recussiva, ou seja, interromper a chamada própria se uma condição for atendida - isso impede a criação de um loop infinito. Nem todas as funções são adequadas para serem usadas recursivamente.

Shivam
fonte
1

ei, desculpe se minha opinião concorda com alguém, só estou tentando explicar a recursão em inglês puro.

suponha que você tenha três gerentes - Jack, John e Morgan. Jack gerencia 2 programadores, John-3 e Morgan-5., você dará a cada gerente 300 $ e quer saber quanto custaria. A resposta é óbvia - mas e se dois funcionários da Morgan também forem gerentes?

Aí vem a recursão. você começa do topo da hierarquia. o custo de verão é 0 $. você começa com Jack. Depois, verifique se ele tem algum gerente como funcionário. se você encontrar algum deles, verifique se eles têm gerentes como funcionários e assim por diante. Adicione 300 $ ao custo de verão sempre que encontrar um gerente. Quando você terminar com Jack, vá até John, seus funcionários e depois para Morgan.

Você nunca saberá quantos ciclos realizará antes de obter uma resposta, apesar de saber quantos gerentes possui e quantos Budget pode gastar.

A recursão é uma árvore, com galhos e folhas, chamadas pais e filhos, respectivamente. Quando você usa um algoritmo de recursão, mais ou menos conscientemente está construindo uma árvore a partir dos dados.

Luka Ramishvili
fonte
1

Em inglês simples, recursão significa repetir algumas vezes.

Na programação, um exemplo é chamar a função em si.

Veja o seguinte exemplo de cálculo de fatorial de um número:

public int fact(int n)
{
    if (n==0) return 1;
    else return n*fact(n-1)
}
Indigo Praveen
fonte
1
Em inglês simples, repetir algo repetidamente é chamado de iteração.
26612 toon81
1

Qualquer algoritmo exibe recursão estrutural em um tipo de dados se basicamente consiste em uma instrução switch com um caso para cada caso do tipo de dados.

por exemplo, quando você está trabalhando em um tipo

  tree = null 
       | leaf(value:integer) 
       | node(left: tree, right:tree)

um algoritmo recursivo estrutural teria a forma

 function computeSomething(x : tree) =
   if x is null: base case
   if x is leaf: do something with x.value
   if x is node: do something with x.left,
                 do something with x.right,
                 combine the results

Essa é realmente a maneira mais óbvia de escrever qualquer algoritmo que funcione em uma estrutura de dados.

agora, quando você olha para os números inteiros (bem, os números naturais) conforme definidos usando os axiomas do Peano

 integer = 0 | succ(integer)

você vê que um algoritmo recursivo estrutural em números inteiros se parece com isso

 function computeSomething(x : integer) =
   if x is 0 : base case
   if x is succ(prev) : do something with prev

a função fatorial muito conhecida é sobre o exemplo mais trivial dessa forma.

mfx
fonte
1

A função chama a si mesma ou usa sua própria definição.

Syed Tayyab Ali
fonte