对于递归的理解很早已经就开始了,但是一直都云里雾里的,今天参照网上的众多资料,自己捋了捋,大概的想法如下:
一、递归的基本思想
- (1)递归含义:递归就是递和归,递到最基本的递归基之后根据之前的路归回去;而循环往往就直接是归回去这条路,但是没有递的路标就往往很难走;
- (2)递归与循环的区别:递归适合从大往小推,而循环适合从小往大推,而递归难以实现从小往大,但是循环却可以实现从大往小;递归就是把整个计算的路直接走完了,然后根据递归基走回去一遍把坑填了;循环就是不知道自己走多远的路,直到限制条件出现挡住了
- (3)总结:递归就是从计算的终点走到起点再走到终点,循环就是从起点走到终点,或从终点走到起点。
二、递归的实际分类
- (1)线性递归:单向的递归,一条路走到死;
- (2)多向递归:在递归的过程中可能出现多种不同的方向,但是最终也只能选择一个方向,属于线性递归的一种;
- (3)多路递归:具体就是递归分成了f(n)+f(n-1)之类的不同道路,其每一个子递归都可能再做多次递归(通常都是一分为二,故也称二分地轨)。这种递归方式可以直接用多叉树结构来理解,递归基就是叶节点;
- (4)尾递归:递归调用在递归实例中恰好是最后一步操作,这种递归都能易于变为迭代版本
三、递归的必须(非必须)具有的四个步骤:
- (1)明确递归的终止条件;
- (2)给出递归终止时的处理方法;
- (3)明确当前问题的特定解决方案(有些情况下不用解决,只用返回);
- (4)提取重复的逻辑,缩小问题的规模;
四、递归基本类型简单描述
- (1)在递的过程中解决问题
function recursion(大规模)
{
if(end_condition) //1.明确的停止条件
end; //2.在停止时所执行的解决方案
else
{
solve; //3.在递的过程中,即划分问题前的解决方案
recursion(小规模); //4.缩小运行规模
}
}
- (2)在归的过程中解决问题
function recursion(大规模)
{
if(end_condition) //1.明确的停止条件
end; //2.在停止时所执行的解决方案
else
{
recursion(小规模); //3.缩小运行规模
solve; //4.在归的过程中,即划分问题后的解决方案
}
}
- (3)多路递归时(如二叉树的迭代遍历),对于不同的路来说,解决方案可处于递或者归的位置
function recursion(大规模)
{
if(end_condition) //1.明确的停止条件
end; //2.在停止时所执行的解决方案
else
{
recursion1(小规模); //3.缩小运行规模,子规模1
solve; //4.对于不同的子递归,其所处的位置不同
recursion2(小规模); //5.缩小运行规模,子规模2
}
}
- (4)特例,solve的位置总是飘忽不定,可能在递过程或归过程,也可能在停止解决方案中,也可能不出现,所以其不是必须的。
五、一些经典问题的解决方案
1.阶乘实现(线性递归问题/尾递归问题)
#ifndef JIECHENGSHIXIAN
#define JIECHENGSHIXIAN
#include <iostream>
using namespace std;
//阶乘的递归实现是一种线性递归,solve过程是在递的过程中(在归时才进行结果计算)
long factorial1(int n)
{
if (n == 1)
return 1;
return n*factorial1(n - 1);
}
//阶乘的迭代实现
long factorial2(int n)
{
long sum = 1;
while (n)
sum *= n--;
return sum;
}
int main_factorial()
{
cout << "阶乘递归:" << factorial1(10) << endl;
cout << "迭代递归:" << factorial2(10) << endl;
return 0;
}
#endif
2.Fibonacci数(多路递归问题/线性递归问题/尾递归问题)
- 如果要说Fibonacci最好的实现,我认为是动态规划,而且是用lambda表达式实现的动态规划,虽然我没有测这些函数的具体实现,但我真的认为lambda表达式这样的匿名函数可以省去许多不必要的资源,并且,真的很优雅。
#ifndef FIBSHIXIAN
#define FIBSHIXIAN
#include <iostream>
using namespace std;
//1.最经典无脑的斐波拉切实现,属于二分递归,将原递归策略不断地进行二分处理
long Fibonacci1(int n)
{
if (n == 1 || n == 2)
return 1;
return Fibonacci1(n - 1) + Fibonacci1(n - 2);
}
//2.优化后的斐波拉切实现,不需要循环地计算F(n-1)和F(n-2),接受数组的前两位f与s,从而代码更易用
long Fibonacci2(int f, int s, int n)
{
//2.1递归基的选择相对较长,直接对应了输入的首位和二位
if (n == 1)
return f;
if (n == 2)
return s;
if (n == 3)
return f + s;
//2.2线性递归的实现,思想为从门的尽头向最开始的门思考(不是从1往n算,是从n往0算,而规律在1往n算找)
//2.3这里的思想理解关键是,将solve过程放在递的过程中,每次开门都会执行 f0 = s; s0 = f+s; 在n的减小过程中不断执行f+s
return Fibonacci2(s, f + s, n - 1);
}
//3.普通的循环语句实现
long Fibonacci3(int n)
{
if (n == 1 || n == 2)
return 1;
int f = 1;
int s = 1;
long sum = 0;
while (n-- > 3)
{
s = f + s;
f = s - f;
}
return f + s;
}
//4.极致精简的动态规划法,是计算效率最高的算法
long Fibonacci4(int n)
{
//4.1此方法思想为:f(-1) = 1; f(0) = 0; f(1) = 1; f(2) = 1; 减少了大量的if判断
long f = 1, s = 0;
while (n--)
{
s = f + s;
f = s - f;
}
return s;
}
int main_fibonacci()
{
int n = 30;
cout << "1.经典斐波拉切实现:" << Fibonacci1(n) << endl;
cout << "2.优化斐波拉切实现:" << Fibonacci2(1, 1, n) << endl;
cout << "3.循环斐波拉切实现:" << Fibonacci3(n) << endl;
cout << "4.动态规划斐波拉切实现:" << Fibonacci4(n) << endl;
//5.最最优雅的写法,直接在while(cin)循环里加个匿名函数(匿名函数最后加了个(),表示调用了此函数)
while (cin >> n)
cout << "lambda表达式斐波拉切实现:" << [&n]() {long f = 1, s = 0; while (n--) { s = f + s; f = s - f; } return s; }() << endl;
return 0;
}
#endif
3.杨辉三角形取值(多路递归问题/尾递归问题)
- 对于杨辉三角形,我的理解是以第一行和第一列开始算,即x与y的初始值都应该是1,所以代码中相关数值的选取也是按照此思想来的
#include <iostream>
using namespace std;
//杨辉三角形的特征就是,一个数等于两肩数之和
long PascalsTriangle(int x, int y)
{
//1.首先判断数据的输入正确
if( x > 0 && y > 0)
{
//2.然后判断递归基,取三角形的边均为1
if (x == y || x == 1)
return 1;
//3.如果都可以则执行二分递归,按照两肩的位置进行递归(属于在归时完成结果计算)
return PascalsTriangle(x - 1, y - 1) + PascalsTriangle(x, y - 1);
}
return -1;
}
int main_PascalsTriangle()
{
cout << "杨辉三角形的(10,5)位置为:" << PascalsTriangle(3, 5) << endl;
return 0;
}
4.回文字符串(线性递归问题/尾递归问题)
- 回文字符串的判断,迭代和递归几乎一毛一样,这就是我在最开始说的,递归适用于实现从大到小的过程,但是迭代可以实现大到小或小到大的任意过程,只是代价不同。而此递归又刚好属于尾递归,所以非常易于在迭代/递归版本中转换
#include <iostream>
#include <string>
using namespace std;
//一个字符串的回文数判断(递归都是从大到小,只是有的规律在最小时找,有的在最大时找)
bool isPalindromeString1(string s)
{
//1.递归基
if (s.size() > 1)
{
if (s[0] != s[s.size() - 1])
return false;
else
//线性递归
return isPalindromeString1(s.substr(1, s.size() - 2));
}
return true;
}
//一个字符串的回文数循环,和递归基本一毛一样,都是从大到小
bool isPalindromeString2(string s)
{
while (s.size() > 1)
{
if (s[0] != s[s.size() - 1])
return false;
else
s = s.substr(1, s.size() - 2);
}
return true;
}
int main_isPalindromeString()
{
string s;
while (cin >> s)
{
if (isPalindromeString1(s))
cout << "true" << endl;
else
cout << "false" << endl;
if (isPalindromeString2(s))
cout << "true" << endl;
else
cout << "false" << endl;
}
return 0;
}
5.全排列问题(多路递归问题)
- 到了全排列的递归实现,就是标准的多路递归问题了。其核心在于两点:
- (1)对于全排列的定义实现:“固定前i,全排列i+1后所有节点,所有结果成多叉树结构,整体运行操作就是按序访问所有叶节点(每访问完一个叶节点就要复位)”;
- (2)当发现这是一个多叉树结构的问题后,三步走:确定递归基,确定循环实现每条分支的方式,确定循环中的迭代方式。即可将问题快速简化
- 也要注意到,这不是一个尾递归问题,所以其变为迭代操作会相当复杂
#include <iostream>
#include <list>
using namespace std;
/*
abc
a bc b ac c ba 其上的枝节点都在过循环
abc acb bac bca cba cab 到达递归基的多叉树叶节点
全排列递归的意义:固定前i,全排列i+1后所有,所有结果成多叉树结构,整体运行操作就是按序访问所有叶节点(每访问完一个叶节点就要复位)
*/
//全排列问题,相当于固定前i位,对第i+1位之后的数据再进行全排列
//list数组存放排列的数,k表示为当前的第几个数,m表示数组的长度
void Perm(int list[], int k, int m)
{
//1.递归基为当前所处的位数达到了最后一位
if (k == m - 1)
{
//1.1每当访问到叶节点时,依照当前数组内部的结构,输出结果,表示该分支遍历完成
for (int i = 0; i < m; i++)
cout << list[i];
cout << endl;
}
else
{
//2.全排列算法的核心:固定前i位,对i+1后的数据再进行全排列
for (int i = k; i < m; i++) //每一层循环就是多叉树的一层,而每层的尽头就是底下的各个分支结束(也就是说,能到这个循环的,都是倒数第二层叶节点)
{
//2.1 for循环通过交换数字来实现全排列,这里从k开始,每个k之后的值都会被交换一遍
swap(list[i], list[k]);
//2.2 线性递归,逐渐缩小范围(每个大循环下面都包含有大量的小循环,每完成一个最底层的小循环则将该小循环所换的数换回来,用于下一次小循环)
Perm(list, k + 1, m);
//2.3 这里是还原数组,保证每一步的计算都能按循环初始时实现(最后计算完成后数组仍为原型)
swap(list[i], list[k]);
}
}
}
int main_Perm()
{
int a[] = { 1,2,3,4 };
Perm(a, 0, 4);
for (int i = 0; i < 4; i++)
cout << "a[" << i << "]:" << a[i] << endl;
return 0;
}
6.前n位依次被n整除(多路递归问题)
- 这道题来源于《程序员面试宝典》P112的面试例题2,也正是这道题给了我重看一遍递归的原因。
- 这是一道少见的递归从小到大解决问题的案例,当然,这种方案实际上应该叫试探回溯法,也就是不断的尝试一个分支上的所有结果,直到尝试完成后才切换到另一个分支。关于试探回溯法的理解可以参考我的博客试探回溯法(N皇后问题)。
- 这道题的核心思想在于,通过在循环中实现递归,通过循环实现了回溯的过程,从而不需要人为地去记录所需回溯的结果之类的。这里的循环+递归的实现时为了找出123456789到987654321之间的所有符合要求的数,所以没有在递归基的时候跳出,请注意
#include <iostream>
#include <vector>
using namespace std;
//1.递归解法:这是一个不断地试探回溯的过程,所以这是一个标准的试探回溯递归法(类似深度优先搜索算法)
//从最普通的个位数1开始试探,不断地使用未使用的值来*10扩充,尝试是否有符合条件的数,若没有则回溯到最开始,乃至个数为1时,使其递增为2再继续试探
//很有意思的一点是,递归的试探回溯是建立在循环的基础上的,而迭代的试探回溯也是循环为蓝本
int used[10] = {0}; //记录对应位数是否使用的数组,0未使用,1使用
vector<long long> save; //记录所符合条件的所有数组
//k初始化为0,表示从数字0开始找整除,a初始化为0,表达从0开始找到987654321
void findthenumber_DFS(int k, long long a)
{
//1.递归基,前面的k为在k=a=0时的一个保护,后面的为不满足当前数a整除k的情况
if (k && a%k != 0)
return;
//2.当此时满足k==9时,表示此时的a已经满足了题目条件,将其存储并return结束此次遍历
if (k == 9)
{
save.push_back(a);
return;
}
//3.线性递归,实现功能的核心,思想为试探回溯法,不过不需要做回溯标记,其会跟着循环自己回来
//循环中的多个线性递归,在某种意义上,直接就是组合成了一种多向递归,最后的结果仍然是多叉树结构
for (int i = 1; i < 10; i++)
{
//3.1首先判断当前数字是否被使用过,被使用过则跳过到下一个数字
if (!used[i])
{
used[i] = 1;
//3.2多路递归的实现,具体为将下一个需要整除的数k,与整合当前能使用数字后的数字a带入递归
findthenumber_DFS(k + 1, a * 10 + i);
//3.3每当前面的一个递归结束,无论其是否找到,均将当前数据复位,便于继续查找
used[i] = 0;
}
}
}
int main()
{
findthenumber_DFS(0, 0);
for (auto&s : save)
cout << "找到的数有:" << s;
cout << endl;
return 0;
}
六、总结
- 线性递归问题往往易于解决,其往往与尾递归问题挂钩,从而便于在迭代与循环两方面解决;
- 如果分析出问题属于多叉树结构,也就是需要多路递归来解决时,最好就使用多路递归(递归基,解决方案,小规模方法)来考虑(复杂情况下,解决方案solve往往和小规模方法recursion耦合性很强),这种时候使用迭代实现往往会非常麻烦。
参考博客: [1] https://blog.csdn.net/justloveyou_/article/details/71787149