深入理解递归

来源:互联网 发布:仙女网络语言什么意思 编辑:程序博客网 时间:2024/06/09 20:38

内容会持续更新,有错误的地方欢迎指正,谢谢!

什么是迭代和递归

迭代的是人,递归的是神。 –L. Peter Deutsch

简单的定义: 当函数直接或者间接调用自己时,则发生了递归。但递归并不直观, 也不符合我们的思维习惯,所以,我们更加容易理解迭代。
例子:计算一个字符串的长度—迭代实现

//迭代的描述: 从第一个字符开始,只要字符不为0,则+1。int Length(const char* str){    int size=0;    while(*str)    {        ++size;        ++str;    }    return size;}

计算一个字符串的长度—递归实现

//递归的描述: 当前字符串的长度等于剩下的字符串长度+X,X为递归函数已执行的次数。int Length(const char* str){    if(*str==0)        return 0;    return Length(++str)+1;}

递归的原理

例子:
n的阶乘

int func(int n){    if(n<=1)    {        return 1;    }    return n*func(--n);}

Paul Graham提到一种方法, 算是递归正确的思考方式,该方法如下:
1. 当n=0, 1的时候,结果正确。
2. 假设函数对于n是正确的,函数对n+1结果也正确。
如果这两点是成立的,我们知道这个函数对于所有可能的n都是正确的。

其实就是数学归纳法。最重要的是第1点,如果我们去掉if(n<=1)这个基本用例后, 代码会进入死循环,永远不会结束。

递归的实现

既然递归比迭代更难理解,为啥我们还要用递归呢?因为有些问题用递归来解决要省时省力很多!
例子:斐波拉契数列:f(0)=0 f(1)=1 f(n)=f(n-1)+f(n-2)
迭代实现:

int Fibo(int n){    int f2=0 , f0=0 , f1=1;    for(int i=0;i<n-1;++i)    {        f2=f0+f1;        f0=f1;        f1=f2;    }    return f2;}

递归实现:

int Fibo(int n){    if(n==0)        return 0;    if(n==1)        return 1;    return Fibo(n-1)+Fibo(n-2);}

递归来实现,再简单不过了,但时间复杂度很高~

有上面的例子可得,有递归算法描述后,程序很容易写,那么关键问题就是怎么得到递归算法描述?
Paul Graham方法,只需要做两件事情:

  1. 如何解决问题的一般情况, 通过将问题切分成有限小并更小的子问题。
  2. 如何通过有限的步骤, 来解决最小的问题(基本用例)。

如果这两件事完成了, 那问题就解决了。这个过程还是数学归纳法的思想。

Paul Graham方法的应用

汉诺塔问题:

一般情况:
当有N个圆盘在A杠上,我们已经找到办法将其移到C杠上了,我们怎么移动N+1个圆盘到C杠上呢?很简单,我们首先用将N个圆盘移动到C杠上的方法将N个圆盘都移动到B杠上,然后再把第N+1个圆盘(最后一个)移动到C杠上,再用同样的方法将在B杠上的N个圆盘移动到C杠上。问题解决。

最小的问题(基本用例):
当有1个圆盘在A上, 我们直接把圆盘移动到C上即可。

算法描述大概就是上面这样,其实也可以看作思维的过程。

A为存放盘子的塔,B为辅助塔,C为目标塔。
算法分为三步:
一、将A上n-1个盘子全部放到B塔上
二、将A上剩下的一个盘子放到C塔上
三、将B塔上的n-1个盘子全部放到C塔上
注:不需要考虑如何移动n-1个盘子,考虑得越多,越谜,还是把人生活得快乐些吧~

void HanoTower(int n,char from,char temp,char to){    if(n==1)        cout<<"From "<<from<<" To "<<to<<endl;    else    {        HanoTower(n-1,from,to,temp);        HanoTower(1,from,temp,to);        HanoTower(n-1,temp,from,to);    }}int main(){    HanoTower(3,'A','B','C');    return 0;}

当n=3时的输出:
From A To C
From A To B
From C To B
From A To C
From B To A
From B To C
From A To C

看起来这么复杂的问题,用递归这么容易,没有想到吧。要是用迭代来解决这个问题呢?你试试吧,试完就能体会到递归的好处了。

写递归函数的注意项

除了使用Paul Graham方法,还有几点需要注意:

1. 要分析清楚满足递归的条件,并全部列出:
写之前就一定要想清楚什么时候这个函数会调用自己,为了防止疏漏条件,最好把所有满足递归的条件都列在纸上或者文档上,一定要尽可能的全面。因为我们经常容易漏掉某一种满足条件,那么结果自然就会不正确。
2. 要分析不满足条件时的处理方式:
就是正确的情景考虑到了后还要考虑错误的情景。
3. 要分析递归函数的返回值:
如果递归函数有返回值,那么每执行完一次递归函数后,如何接收、处理该递归函数的返回值。
4. 写完递归函数后一定要进行单元测试:
可以将循环的次数和递归后的结果打印出来,看看打印后的结果是否符合自己的预期,如果某一递归出现问题,可以根据循环次数的记录在调试的时候直接定位,这样效率会高很多。测试的时候一定要涉及到所有满足递归的条件,每一条件分支都要检查一遍。

递归总结

优点:结构清晰,可读性强,为设计算法、调试程序带来很大方便。
缺点:在递归调用中,系统为每一层的返回点、局部量等开辟了栈来存储,易造成栈溢出。

递归算法一般用于解决三类问题:
1. 数据的定义是按递归定义的。(Fibonacci函数)
2. 问题解法按递归算法实现。(汉诺塔问题)
3. 数据的结构形式是按递归定义的。(树的遍历,图的搜索)

递归算法可以分为三种类型:基于递归策略的分治算法、基于递归策略的自顶向下的动态规划算法、基于递归策略的回溯算法。

a.基于递归策略的分治法(先拆,再解,后合),当问题能分解为独立的子问题,就可以使用分治法。
举例:
1. 阶乘
2. 斐波拉契数列(又叫 爬楼梯问题)
3. 汉诺塔问题
4. 矩阵乘法Strassen’s算法
5. 最近点对问题

b.基于递归策略的动态规划(自顶向下类型):
当问题不能分解为独立的子问题,却又符合最优化原理时,就可以使用动态规划法。
举例:
1. 装配线排程问题
2. 最长共同子序列问题
3. 背包问题

c.基于递归策略的回溯:
当问题找不到数据间的相互关系、也不能将问题分解为独立的子问题,就只有把全部解都列出来,才能推断出问题的解。遍历问题各个可能解的通路,当发现此路不通时,回溯到上一步继续尝试别的通路。

参考

【1】写递归函数的正确思维方法
http://blog.csdn.net/vagrxie/article/details/8470798
【2】如何编写递归程序(分治法)
http://blog.csdn.net/xgf415/article/details/52026961
【3】算法策略的总结
http://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741482.html

原创粉丝点击