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 n∗f(n−1)n≤1n>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){...}
函数内部自己的事情,我们相信这个递归部分总是能给我们正确的后缀全排列(基于递归部分总能递归到基础部分)。程序输出的也验证了这个事实:
汉诺塔问题
汉诺塔问题源自大梵天创世的一个古老传说。目标是要把所有的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个盘子进行正确的移动(基于递归部分总能递归到基础部分)。程序的输出也验证了这个事实:
图解汉诺塔问题的知乎回答