深入浅出重新理解递归思想,并重新思考经典递归算法(全排列以及汉诺塔)问题。

本文深入探讨了递归的概念,通过数学分段函数和数学归纳法阐述递归思想。递归算法的关键在于基础部分和递归部分,理解这两部分有助于解决如全排列和汉诺塔等经典问题。全排列问题的C++实现通过递归生成所有排列,而汉诺塔问题的递归解法通过移动n-1个盘子实现。文章强调,避免过度跟踪递归过程,而是信任递归部分会到达基础部分,从而简化问题解决思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.理解递归

通过数学的分段函数
f ( n ) = { 1 n ≤ 1   n ∗ f ( n − 1 ) n > 1 f(n) = \begin{cases} 1 & n \leq1 \\ \ n*f(n-1) & n \gt 1 \end{cases} f(n)={1 nf(n1)n1n>1
或者数学归纳法,都能很好理解到递归的思想。因此,所有的递归函数都必须包含两个部分:
1.基础部分。它包含n的一个或多个值,对于这些值,f(n)是直接定义的(即不用递归函数就能求解)。
2.递归部分。右侧f有一个参数小于n,因此重复应用递归部分可以把右侧f的表达式转变为基础部分。
而理解递归最重要的是放弃企图展开并跟踪递归全过程的思想。我们要相信上一步条件的递归总是正确的,这是建立在递归部分总能递归到基础部分的事实上。
只要理解了递归的思想,所有的问题将会迎刃而解。比如说我们常见的求阶乘的递归函数定义如下:

int factorial(int n) {
	if (n <= 1) return 1;		//基础部分
	return n * factorial(n-1);	//递归部分
}

如何去展开递归部分呢? 这时候我们就必须跳出这种想法,放弃理解递归部分的展开和跟踪。我们选择相信程序factorial(n-1)已经为我们求解出了1到n-1的阶乘,我们要做的就只有一件事,就是将这部分的阶乘结果乘上我们的n得到1到n的阶乘,也就是factorial(n)。当然,阶乘的递归部分是容易进行展开跟踪的,对它进行展开跟踪也能从侧面验证我们这种放弃思想的正确性。

2.理解经典递归算法

有了这种思想,我们重新回顾两个递归的经典算法:求全排列和汉诺塔问题。

全排列问题

给定一个集合E={e1,e2,e3,…,en}是n个元素的集合,求E的元素的所有排列。
例如E={a,b},则全排列有perm=({a,b},{b,a})。例如E={1,2,3},则全排列有perm=({1,2,3},{1,3,2},{2,1,3},{2,3,1},{3,1,2},{3,2,1})。
其C++算法描述为:

template <typename T>
//生成给定序列(数组)list[k:m]的全排列
void permutations(T list[],int k,int m) {
	if (k == m) {//只有一个排列,输出它
		std::copy(list,list+m+1,std::ostream_iterator<T>(cout,""));
		cout << endl;
	}
	else {//list[k:m]有多个排列,递归生成这些排列
		for (int i = k; i <= m;++i) {
			std::swap(list[k],list[i]);
			permutations(list,k+1,m);
			std::swap(list[k],list[i]);
		}
	}
}

算法求解给定序列(数组)list[k]到list[m]的所有排列。它的基础部分是if(k==m)的部分,当下标k==下标m的时候代表只有一个排列,输出这个排列。它的递归部分是else的部分,这个时候k!=m,也就是不止一个排列,在for循环中,将list[m]前的每一个元素list[i]与list[k]进行交换,然后递归得到以元素list[0]到list[k] (也就是交换后的list[i])为前缀的所有排列,最后再交换回去保证序列的原始顺序。
倘若我们选择展开跟踪这个递归算法的全过程,那么其递归过程变得极其复杂,难以理解。因此我们跳出这个想法。观察for循环的递归部分permutations(list,k+1,m);,它只做了一件事,那就是生成以元素list[0]到list[k] (也就是交换后的list[i])为前缀的全排列,至于其过程是怎么生成的,完全是permutations(list,k+1,m){...}函数内部自己的事情,我们相信这个递归部分总是能给我们正确的后缀全排列(基于递归部分总能递归到基础部分)。程序输出的也验证了这个事实:
对数组[1,2,3,4,5]后三位数生成全排列

汉诺塔问题

汉诺塔问题源自大梵天创世的一个古老传说。目标是要把所有的n个盘子从最左边的柱子(left)移动到中间的柱子(middle),可以以最右边的柱子(right)作为中转。条件是:
1)每次只能移动一个盘子
2)小盘子只能放在大盘子之上
我们用递归算法进行模拟。
其C++描述为:

//汉诺塔问题的递归算法
void towerOfHanoi(int n,char left,char middle,char right) {
	if (n>0) {
		//1
		towerOfHanoi(n-1,left, right, middle);
		 //2
		cout << "Move top disk from tower " << left
			<< "to top of tower " << middle << endl;
		//3
		towerOfHanoi(n-1, right, middle, left);
	}
}

算法描述并输出了汉诺塔问题的全过程。它的基础部分是if(n<=0),也就是当前只有一个盘子的时候,直接将左边柱子(left)的盘子移动到中间盘子(middle),前后两条递归调用语句失效。递归部分是if(n>0),也就是左边柱子(left)初始有大于1个盘子。把左边柱子(left)n-1个柱子先移动到右边柱子(right),以中间柱子(middle)为中转;然后把最大的盘子从左边柱子(left)移动到中间柱子(middle);最后把右边柱子(right)的n-1个盘子再移动到中间柱子(middle),以左边柱子(left)为中转,完成全过程。
同样的,如果要将其递归过程跟踪展开是极其复杂和困难的,我们跳出这种想法。观察其递归部分towerOfHanoi(n-1,left,right,middle);towerOfHanoi(n-1,right, middle,left);,它们分别只做了一件事,那就是把n-1个盘子先从左边柱子(left)移动到右边柱子(right),然再把n-1个盘子从右边柱子(right)移动到中间柱子(middle)。至于把这n-1个盘子移动的过程如何,完全是towerOfHanoi(n-1,left,right,middle){...}towerOfHanoi(n-1,right,middle,left){...}函数内部的事情,我们相信这两个递归过程总是能为我们把n-1个盘子进行正确的移动(基于递归部分总能递归到基础部分)。程序的输出也验证了这个事实:
对三个盘子的汉诺塔问题进行模拟移动
图解汉诺塔问题的知乎回答

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值