一.递归定义
递归就是,从前有个山,山里有个庙,庙里有个老和尚给小和尚讲故事,讲的故事是,从前有个山,山里有个庙,庙里有个老和尚给小和尚讲故事,讲的故事是。。。
就像套娃一样,在自己内部重复自己的过程,专业地讲叫:在定义自身的同时,又出现对自身的引用,
* 如果一个函数在其定义体内直接调用自己,则称为直接递归函数
* 如果一个函数经过一系列的中间调用语句,通过其他函数间接调用自己,则称为间接递归函数
许多数学函数都是递归定义的,如二阶斐波那契(Fibonacci)数列定义为
二.栈与递归的联系
递归进层(i-->i+1层)系统所要进行的三个操作:
①保留本层参数与返回地址。
②为被调用函数的局部变量分配存储区,给下层参数赋值。
③将程序转移到被调用函数的入口。
而从被调用函数返回至调用函数之前,递归退层(i<--i+1层)系统也需要完成三个操作:
①保存被调用函数的结果。
②释放被调用函数的数据区,恢复上层参数。
③依照被调用函数保存的返回地址,将控制转移回调用函数。
可以发现,这三个操作就是个相逆的过程.
当递归函数调用时,应按照“后调用先返回”的原则处理调用过程(有没有触发关键词,直接栈起来了(滑稽)),因此上述函数之间的信息传递和控制转移必须通过栈来实现。系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区,而每当从一个函数退出时,就释放它的存储区。显然,当前正在运行的函数的数据区必须在栈顶。
一个递归函数的运行过程中,调用函数与被调用函数是同一个函数,因此,与每次调用时相关的一个重要概念就是递归函数运行的“层次”。假设主函数是第0层,从主函数调用递归函数则被称作第1层,同样,当从第i层调用函数进入下一层,也就是i+1层。反之,退出,i-1层。
那么这些信息该如何存储传递呢?
为保证递归函数的正确执行,系统需要设立一个递归工作栈作为整个递归函数运行期间使用的数据存储区。每层递归所需信息构成一个工作记录,其中包括所有的实在参数,所有的局部变量以及上一层的返回地址。每进入一层递归就会产生一个新的工作记录压入栈顶。每退出一层递归,就从栈顶弹出一个工作记录。因此当前执行层的工作记录必然是递归工作栈栈顶的工作记录,称此纪录为活动记录,并称指示活动记录的栈顶指针为当前环境指针。这么麻烦,如何实现?只能说多谢系统自己管理递归工作栈,无需我们费心,不过我们可以去模仿制作理解其原理。
三.递归求解问题
许多问题的求解过程可以用递归分解方法描述,一个典型例子就是著名的汉诺(Hanoi)塔问题。
n阶Hanoi塔问题:假设有三个分别命名为A,B,C,的三个塔座,在塔座A上插有n个直径大小各不相同,从小到大编号为1,2,...,n的圆盘。现要求将塔座A上的n个圆盘移动到塔座C上,并且仍然按照原来顺序叠放。圆盘移动时须遵守下列规则:
① 每次只能移动一个圆盘。
② 圆盘可以插在A,B,C,中任何一个塔座上。
③ 任何时刻都不能将一个较大的圆盘压在较小的圆盘之上。
算法思想:
这里规定f(x)表示X个按照一定规则叠放的圆盘,1,2,3等数字表示圆盘编号,A,B,C表示三个塔座。
当n=1时,问题就很简单,只需将1从塔座A移动到塔座C上即可。
当n>1时,就无法一次性完成此工作,需要借助B塔座来完成。
当n=3时,所要进行的操作如下:
要将f(3)从A挪到C上,就得先把f(2)从A挪到B上,然后将3从A挪到C上,再将f(2)从B挪到C上;
如何将f(2)从B挪到C上呢? 自然先将f(1)从B挪到A上,然后将2从B挪到C上,再将f(1)从A挪到C上;
至此,已成艺术(Bushi).
至此,问题已经解决,只需将1挪到C上即可。
推广至f(n)
如果要将f(n)从A挪到C上,就得先把f(n-1)从A挪到B上,然后将n从A挪到C上,再将f(n-1)从B挪到C上;
如何将f(n-1)从B挪到C上呢? 自然先将f((n-1)-1)从B挪到A上,然后将n-1从B挪到C上,再将f((n-1)-1)从A挪到C上;
......
如何将f(2)从B挪到C上呢? 自然先将f(1)从B挪到A上,然后将2从B挪到C上,再将f(1)从A挪到C上;
当n=1时,只需将1移动到塔座C上即可.
可以发现,整个过程就是将f(n)挪动步骤不断重复,直至n=1结束。
代码表示如下:
// n 个数
void Hanoi(int n, char A, char B, char C) { // A -起始塔座 B -中间辅助塔座 C -目标塔座
if (n == 1) {
Move(1, A, C); // 将1号从A塔座挪到C塔座
}
else {
Hanoi(n - 1, A, C, B); // 将f(n-1)从A挪到B上
Move(n, A, C); // 将n从A挪到C上
Hanoi(n - 1, B, A, C); // 将f(n-1)从B挪到C上
}
}
本文主要侧重递归思路引导,Hanoi的具体实现,可以利用链栈,将三个栈的头结点看作ABC三个塔座,顺序圆盘就可以作为它的元素,最后通过不断地出栈压栈,实现移动。这里就可以用到多栈共享的思路。
关于链栈,多栈共享思路,可见上篇文章
C语言数据结构基础 --限定性线性表之---栈-优快云博客;
四.递归思想
递归算法就是在算法中直接或间接调用算法本身的算法。这使得递归算法的前提有以下两个:
① 原问题可以层层分解为类似的子问题,且子问题的规模小于原问题。
② 规模最小的子问题可以直接求解。
设计递归算法的原则是用自身的简单情况定义自身,设计方法如下:
① 寻找分解方法:将原问题化为子问题求解。 例如:n!=n*(n-1)!
② 设计递归出口:将规模最小的子问题确定递归终止条件。 例如:求解n! 当n=1,n!=1;
递归的特点就是对问题描述简单,结构清晰,容易证明程序正确性。
五.递归应用
实际上掌握了递归思想后,可以解决许多问题。
1.求解n!
首先,寻找分解方法,这一步可谓是核心:
如果想到,n!=n*(n-1)!,那么问题一迎刃而解了。
if(n!=1) return n*f(n-1);
下一步就是设计递归出口,当然,上一步其实越界了,实际上两步都走完才可以很好地设计出完整的递归函数。
我们知道,n一直分下去最后会到1,而1!=1,是个确切地结果,也就是我们找寻的出口,
所以 if(n==1) return 1;
其最终代码如下:
// 求解N!
int F_N(int n) {
if (n == 1) return 1; // 递归出口: 1!=1
else return n * F_N(n - 1); // 分解方法: n!=n*(n-1)!
}
2.斐波那契(Fibonacci)数列求解
其实,这个函数的表示就将我们的递归问题写明白了,出口在0,1,分解在n>1,
只需用代码复述一遍函数即可:
// Fib
int Fib(int n) {
if (n == 0 || n == 1) return n;
else if (n > 1) {
return Fib(n - 1) + Fib(n - 2);
}
}
从这也可以看出,C语言函数就是加上语句规则的描述,与数学函数的描述非常接近。
六.递归转化为非递归
众所周知,没有什么事务是绝对完美的,递归也如此。
它虽然有着将复杂问题分解为简单问题,简洁清晰的特点,但控制权的频繁转移,以及自身对于隐式栈的调用,使其效率较低。且并非所有语言支持递归。
消除递归的原因总结为如下三点:
① 有利于提高算法时空性能,因为递归执行时需要系统提供隐式栈来实现递归,所以效率较低。
② 无应用递归语句的语言设施环境条件,有些计算机语言不支持递归功能。如FORTRAN
③ 递归算法是以此执行完成的,中间过程对用户不可见,某些问题的处理上不合适。
理解递归是消除递归的前提,消除递归就要基于问题本身进行分析,常见的有以下两种消除递归的方法:
一,简单递归的转化,对于尾递归和单向递归算法,可以使用循环结构算法替代。
二,基于栈的方式,即将递归中隐含的栈机制转换为由用户直接控制的显式的栈。利用栈来保存参数。
简单递归的转换:
1.单向递归
单向递归是指递归函数中虽然有一处以上的递归调用语句,但各次递归的语句的参数只与主调用函数有关,相互之间的参数无关,并且这些递归调用语句处于算法最后。
计算斐波那契数列就是典型的单向递归,其递归调用参数n只与传入的n有关,后续不过是在n的基础上减小。
其递归写法在上面,这里展示转换代码:
int Fib_C(int n) {
if (n == 0 || n == 1) return n;
else {
int x = 0, y = 1, z; // x==Fib(0) ,y==Fib(1) ,z 为所求值
for (int i = 2; i <= n; ++i) {
z = x + y;
x = y; // x为Fib(n-2)
y = z; // y为Fib(n-1)
}
return z;
}
}
2.尾递归
尾递归是指递归调用语句只有一个,而且是处于算法的最后,尾递归是单向递归的特例。
求n!就是典型的尾递归
同上,只展示非递归方式:
int F_N_C(int n) {
if (n == 0) return 1;
int x = 1;
for (int i = 1; i <= n; ++i) {
x *= i;
}
return x;
}