<<O algoritmo é lindo>>——(2) Explicação detalhada do pensamento recursivo

contente

prefácio

Conceitos básicos de recursão

Três elementos de recursão

Pensamento de corte de bolo: exercícios simples recursivos

Usando a Fórmula de Recursão para Resolver o Máximo Divisor Comum

Não há nada melhor: ordenação por inserção recursivamente

Solução recursiva para pesquisa binária

Recursão Multibranch: A Sequência de Fibonacci 

Falando sobre algumas ideias de otimização de recursão

combate tópico

resumo final


prefácio

Quando entrei em contato com a recursão pela primeira vez, acredito que a maioria dos meus amigos deve estar tão confusa quanto eu, e eles suspiraram que era incrível, tantas coisas foram feitas em uma linha tão curta de código.

Na verdade, não tenha medo, é fácil de aprender. Nada mais é do que pensar em como usar os conceitos de recursão limite e recursão . Decomponha o problema original em vários sub -problemas, só precisamos escrever a recursão e limite, e deixar o resto para o computador. , Nós somos o chefe, é para nós. Tentamos desenhar um diagrama recursivo para ajudar a entender o código que não entendemos, e tentamos não pensar no processo de empilhamento de pilhas em nossas mentes. Nossos cérebros podem empurrar várias pilhas! ! Em vez disso, ele se confunde.

Recentemente, li muito conhecimento sobre recursão e gostaria de compartilhar com vocês minhas opiniões, esperando trazer uma pequena ajuda.

Conceitos básicos de recursão

Existe uma definição de recursão aparentemente brincalhona: "Para entender a recursão, você deve primeiro entender a recursão até poder entender a recursão", mas essa explicação da recursão é muito intuitiva. A recursão é chamar repetidamente sua própria função, mas sempre O problema é reduzido até que o intervalo seja reduzido ao resultado de que os dados de limite podem ser obtidos diretamente e, em seguida, a solução correspondente é obtida no caminho de volta . Deste ponto de vista, a recursão é muito adequada para implementar a ideia de dividir e conquistar.

Três elementos de recursão

Para melhor elicitar os três elementos, vamos dar um exemplo simples: use recursão para resolver o fatorial de n

1. Encontre duplicatas (subproblemas) - recursiva

Primeiro dê a fórmula de cálculo de n!: n!=1*2*3*...*n; esta forma recursiva n!=n*(n-1)!, então a escala é n O problema é transformado em um problema de tamanho n-1. Se n! for representado por f(n), ele pode ser escrito como f(n)=f(n-1)*n ( recursivo ), de modo que alcancemos nosso objetivo de fazer o escala menor - subproblemas. (Nota: Encontrar a recursão é o passo mais difícil na recursão. Precisamos entrar em contato com mais perguntas para resumir nossa experiência. Em seguida, praticarei algumas perguntas com você. Devemos praticar mais e tentar alterar o loop encontrado para recursão)

int f(n)
{
   return f(n-1)*n
}

2. Encontre a quantidade de mudança na repetição - parâmetro

A quantidade de mudança deve ser usada como parâmetro . Este problema tem apenas uma variável n, que obviamente é usada como parâmetro. O papel deste ponto não é tão importante neste problema. Explicarei um problema de soma de matriz abaixo, refletindo o seu valor.

3. Encontre a tendência de mudança de parâmetro - exportação de design

O código acima tem uma situação em que ele chama a si mesmo, então isso é chamado de recursão, mas o código acima não é padronizado o suficiente e, mesmo que haja um problema, ele continuará chamando a si mesmo, caindo em um poço sem fundo e, finalmente, em um estouro de pilha ocorreu um erro.

Então precisamos encontrar uma "saída" para ele, ou seja, precisamos descobrir qual é o parâmetro, a recursão termina, e então retornar o resultado diretamente.

Obviamente no exemplo acima, quando n==1, f(1)=1 for export; vamos melhorar o código abaixo

int f(int n)
{
	if (n == 1)
	{
		return 1;
	}
	return f(n - 1) * n;
} 

Neste ponto, o código dentro de nossa função f(n) está completo.

Como dissemos acima, se você não entender o código, você precisa desenhar um diagrama recursivo para ajudá-lo a entender. Vamos usar isso como exemplo para desenhar um diagrama com você.

Para entender melhor os três elementos da recursão, deixe-me fazer alguns exercícios com você.

Pensamento de corte de bolo: exercícios simples recursivos

Soma da matriz:

#include<iostream>
int main()
{
   int a[]={1,2,3,4,5,6,7};
   //递归函数sum
   return 0;
}

1. Encontre repetições - recursivas

Para encontrar uma fórmula recursiva, seguiremos o problema original em vários subproblemas, assim como cortamos um bolo, primeiro cortamos um pedaço e comemos, e usamos a recursão para dar o resto ao próximo passo para comer, este problema nós apenas extraímos o primeiro elemento, o resto é entregue ao processamento recursivo

int sum(int a[])
{
   return a[0]+sum(a);
}

 Foram realizadas

2. Encontre a quantidade de mudança na repetição - parâmetro

Neste processo de repetição, descobriremos que o intervalo do array está constantemente diminuindo, o ponto inicial do array está mudando e descobrimos que algo está constantemente indo para a direita. Mas a fórmula recursiva escrita acima não tem intervalo variável , sempre foi Recursão desde o início. Depois de analisar isso, descobriremos que devemos encontrar a quantidade de mudança na repetição para adicionar um parâmetro. Eu adiciono outro parâmetro n aqui para registrar o comprimento do array

int sum(int a[],int n,int begin)
{
   return a[begin]+sum(a,n,begin+1);
}

3. Encontre a tendência de mudança de parâmetro - exportação de design

Se begin for igual ao índice do último elemento do array, retornamos diretamente, que é a saída


int sum(int a[],int n,int i)
{
    if (i == n-1)
	{
		return a[i];
	}
	return a[i] + sum(a, n,i+1);
}

Isso está feito. Não é muito simples? Deve haver alguns chefões reclamando que este tópico é simples. No começo, vamos começar com simplicidade e usar os três elementos com proficiência. É a mesma rotina ao encontrar problemas. Não muito para digamos, outro

vire a corda

int main()
{
	string sa = "wewe89r";
	cout << f(sa);
	return 0;
}

1. Encontre repetições - recursivas

Para esta questão, ainda podemos usar o pensamento de corte de bolo para encontrar a fórmula recursiva. Para facilitar o entendimento, farei um desenho

int f(string str)
{
 
  return f(str.substr(1))+str.substr(0,1);   //substr(0,1)表示从下标0开始取一个字符形成的串                                    
                                             
                                             //substr(1)表示从下标1开始到结尾形成的串
}

2. Encontre a quantidade de mudança na repetição - parâmetro

O comprimento da string nesta questão está mudando constantemente, então usamos o comprimento da string como parâmetro, e aqui eu uso a função substr, não há necessidade de adicionar novos parâmetros, mas a essência é reduzir continuamente o comprimento da string seqüência.

3. Encontre a tendência de mudança de parâmetro - exportação de design

Se o comprimento da string for menor ou igual a 1, o programa está prestes a terminar, encontre esta saída

string  f(string str) {
	int len = str.length();
	if (len <= 1)
		return str;
	return f(str.substr(1)) + str.substr(0, 1); //substr(0,1)表示从下标0开始取一个字符形成的串
}                                               //substr(1)表示从下标1开始到结尾形成的串

Bom, essa pergunta acabou aqui. Acredito que todos aqui tenham uma compreensão maior dos três elementos. Depois das perguntas práticas, você pode pensar de acordo com esse modelo. Não é realista confiar em um blog para aprofundar um pensamento. Sim, então você tem que praticar por conta própria.

Este está aqui, e vou levá-lo para um estudo mais aprofundado abaixo.

Usando a Fórmula de Recursão para Resolver o Máximo Divisor Comum

O máximo divisor comum de inteiros positivos a e b refere-se ao máximo divisor comum de todos os divisores comuns de a e b. Por exemplo, o máximo divisor comum de 4 e 6 é 2, e o máximo divisor comum de 3 e 9 é 3 . Geralmente, gcd(a, b) é usado para representar o máximo divisor comum de a e b, e o algoritmo de Euclides é comumente usado para resolver o máximo divisor comum.

O algoritmo de Euclides é baseado no seguinte teorema:

Suponha que a e b sejam inteiros positivos, então gcd(a,b)=gcd(b,a%b); (a prova é ignorada)

Do teorema acima, pode ser encontrado que se a<b, então o resultado do teorema é que a e b são trocados; se a>b, então o tamanho dos dados sempre pode ser reduzido por este teorema, e a redução é muito rápido. Isso parece obter o resultado rapidamente, e o conhecimento precisa de mais uma coisa: o limite de recursão, ou seja, até que ponto o tamanho dos dados pode ser reduzido para que o resultado possa ser calculado. É muito simples, como todos sabemos: o maior divisor comum de 0 e qualquer inteiro a é a, Esta conclusão pode ser considerada como um limite recursivo. A partir disso, podemos facilmente pensar em escrevê-lo de forma recursiva, porque as duas chaves de recursão foram obtidas:

1. Fórmula recursiva: mdc(a,b)=mdc(b,a%b);

2. Limite recursivo: mdc(a,0)=a.

Então pegue o seguinte código:

int gcd(a,b)
{
if(a==0) return a;
else return gcd(b,a%b);
}

Não há nada melhor: ordenação por inserção recursivamente

 Para vários algoritmos, já os apresentei em detalhes. Para quem esqueceu, pode ler meu blog: Seven Common Sorting Algorithms

Vá diretamente para o código: (ou siga os três elementos acima)

#include<iostream>
using namespace std;
void InsertSort(int* a, int n)
{
	if (n == 0)
	{
		return;
	}
	InsertSort(a, n - 1);
		int x = a[n];
		int index = n - 1;
		while (index>-1&&x < a[index])
		{
			a[index + 1] = a[index];
			index--;
		}
		a[index+1] = x;
	}
int main()
{
	int a[] = { 1,3,4,2,6,5 };
	InsertSort(a, sizeof(a)/sizeof(int)-1);
	for (auto x : a)
	{
		cout << x << " ";
	}
	return 0;
}

Solução recursiva para pesquisa binária

 A pesquisa binária de intervalo completo é
 equivalente a três subproblemas
* pesquisa à esquerda (recursão)
* proporção do meio
* pesquisa à direita (recursão)

#include<iostream>
using namespace std;
int binarySearch(int* a, int left, int right, int key)
{
	while (left < right)
	{
		int mid = left + ((right - left) >> 1);
		int number = a[mid];
		if (number < key)
		{
			return binarySearch(a, mid + 1, right, key);
		}
		else if (number > key)
		{
			return binarySearch(a, left, mid - 1, key);
		}
		else
			return mid;
	}
}
int main()
{
	int a[] = { 1,2,3,4,5,6,7,8,9,10 };
	int find=binarySearch(a, 0, sizeof(a)/sizeof(int)-1, 5);
	cout << find << endl;
	return 0;
}

Recursão Multibranch: A Sequência de Fibonacci 

A sequência de Fibonacci satisfaz a sequência de f(0)=1, f(1)=1, f(n)=f(n-1)+f(n-2) (n>=2), a frente do sequência Vários itens são 1,1,2,3,5,8,,,. Como o limite recursivo já está familiarizado com f(0)=1 e f(1)=1 da definição, e a fórmula recursiva é f(n)=f(n-1)+f(n-2)(n >=2 ), então podemos escrever o programa para o n-ésimo item seguindo a maneira de resolver o fatorial de n:

int f(int n)
{ 
if(n==1||n==2)
 return 1;
else
 return f(n-1)+f(n-2);
}

Também sabemos que escrever código desta forma é conciso e fácil de entender, mas é muito ineficiente.Onde está a ineficiência? Assumindo n = 15, desenhe a árvore de recursão: 

Como entender essa árvore de recursão, quando você quer calcular f(15) , você precisa calcular f (14) ef (13 ), para calcular f(14) , você precisa calcular  f(13) e f(12 ) ), e assim por diante. Finalmente , quando f(1) é encontrado, o resultado é conhecido e   o resultado pode ser retornado diretamente, e a árvore recursiva não cresce mais para baixo.  f(2)

É óbvio que as equações f(13) e f(12) são calculadas repetidamente ao realizar cálculos recursivos. Quanto maior o número de cálculos, mais cálculos repetidos. Isso é uma coisa terrível, então temos que encontrar uma maneira de otimizar isso. .

Falando sobre algumas ideias de otimização de recursão

Uma vez que o problema é esclarecido, na verdade, metade do problema foi resolvido. Como o motivo do demorado é o cálculo repetido, podemos criar um "memorando". Depois de calcular a resposta a uma subquestão, não se apresse em retornar, primeiro anote no "memorando" e depois retorne ; cada vez que uma sub-questão for encontrada Vá até o "Memo" e verifique. Se você achar que este problema já foi resolvido antes, basta usar a resposta diretamente, e não perder tempo calculando-a.

Normalmente é usado um array como este "memorando", claro que você também pode usar uma tabela de hash (dicionário), a ideia é a mesma.


 int f(int n)
 { 
    if(n ==0||n==1)
    { 
      return 1; 
    } 
     //先判断有没计算过 
     if(arr[n] != -1)
    { 
       // 已经计算过,不用再计算了
       return arr[n];
    }
     else
     {
      // 没有计算过,递归计算,并且把结果保存到 arr数组里 
        arr[n] = f(n-1) + f(n-1); 
        reutrn arr[n]; 
      } 
 }

 Até agora, a solução recursiva com memoização é tão eficiente quanto a solução de programação dinâmica iterativa. Na verdade, esta solução é quase a mesma solução de programação dinâmica comum, exceto que esta solução é uma solução "recursiva" "de cima para baixo" Nosso código de programação dinâmica mais comum é uma solução "recursiva" "de baixo para cima". empurre" para resolver.

O que é " de cima para baixo "? Observe que a árvore de recursão (ou gráfico) que acabamos de desenhar se estende de cima para baixo, de um problema original de maior escala, por exemplo  f(20), decompõe gradualmente a escala para baixo até  f(1) corresponder  a f(2) esses dois casos base e depois camada por camada Retornando a resposta é chamado de "de cima para baixo".

O que é " de baixo para cima "? Por sua vez, começamos diretamente de baixo, o mais simples, o menor tamanho do problema, a soma dos resultados conhecidos  (caso base) f(1) e  f(2)vamos subindo até chegarmos à resposta que queremos  f(20). Essa é a ideia de "recursão", que também é a razão pela qual a programação dinâmica geralmente se livra da recursão, mas completa o cálculo por iteração de loop.

Auto-aperfeiçoamento:

public int f(int n)
 {
    if(n == 1||n==2)
     return 1; 
    int f1 = 1;
    int f2 = 2; 
    int sum = 0; 
    for (int i = 3; i <= n; i++)
     { 
     sum = f1 + f2;
      f1 = f2;
       f2 = sum; 
     } 
     return sum; 
}

Claro, também existem algumas operações de aceleração recursiva - poda , recursão e retrocesso para otimizar a recursão.

combate tópico

Os dois exercícios recursivos mais clássicos a seguir, você pode praticar suas mãos

passos pulando

Torre de Hanói

resumo final

O acima é o meu entendimento de recursão. Espero que possa desempenhar um papel para atrair outras pessoas. Se houver algo que você não entenda, você pode deixar uma mensagem na área de comentários e discutir juntos. Em uma palavra, em uma palavra , é necessário estar familiarizado com os três elementos de recursão que mencionei acima Pratique a sumarização e o domínio.

Acho que você gosta

Origin blog.csdn.net/m0_58367586/article/details/123752617
Recomendado
Clasificación