contente
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
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
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.