对于递归思想的一点理解

对于递归的理解很早已经就开始了,但是一直都云里雾里的,今天参照网上的众多资料,自己捋了捋,大概的想法如下:

一、递归的基本思想

  • (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

  • 5
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方寸间沧海桑田

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值