您的位置:首页 > 其它

递归与算法分析(一)递归总论

2016-03-13 15:29 316 查看
鉴于行业内对递归存在许多误解和疑惑,这里我想结合算法分析,写一个系列关于递归和算法分析的博客。

在这一章,我们要讲的是递归的形式和思维方法,在后面的章节里,我们会讲到时间复杂度和空间复杂度的分析,在那里我们就会看到,并不是什么递归函数都效率低,都会溢出。

概念背得再熟,你也不会写!

请写出这个函数



嗯,是这样写的:

int f(int n){

if(n==0)return 1;

else return 5*n;

}

(这是在逗我吗?)

那如果把函数改成

呢?

那么简单一改,就写成这样:

int f(int n){

if(n==0)return 1;

else return n*f(n-1);

}

这就是我们常说的阶乘了。

之所以要你写上面那个傻逼函数,绝不是在逗你,因为我从几个同学身上发现,他们总不知道“return”要放在哪里。递归的一般形式是这样的:

int f(int a,int b,int c){ //不一定是int

if(___)return ____;

else if(____)return ____;

else if(____)return ____f(__,___,___)____;

else return ____f(__,___,___)____;

}

前面若干句是基础条件,后面若干句是递归调用,每一句都有return,都没有赋值语句,而且在每一个分支,我们一般不需要用大括号框住语句块,因为每个分支都只有一句话。

代换模型

在我们试图理解递归的运行过程的时候,经常用大脑来记忆程序的断点和现场数据,结果才调用了两三次,就把前面的弄乱了,这种理解方式是错误的。

既然脑子记不住,那就笔来写嘛!

以阶乘作为例子,我们来看看阶乘的运行过程。



嗯,这就是递归阶乘的运算过程,以后要用笔来写,不许再用脑子记了!

这个运行过程的高度,也就是这个函数的计算次数,就是它的时间复杂度,

可以看出,大概运算了2n次,也就是T(n)=O(n)。

运行过程的最大宽度,就是它在运算时,需要占用的栈空间的大小,也就是它的空间复杂度,

可以看出,它的空间复杂度S(n)=O(n)。当n比较大时(大概是3万的时候),我们有限的栈空间就装不下了(这里不考虑整数溢出),当然,如果问题规模本身就不大,这个空间复杂度是可以接受的。

下面,我们写另一种形式的阶乘。



用C语言写出来,就是:

int f(int a,int n){

if(n==0)return 1;

else return f(a*n,n-1);

}

我们来看看这个函数的运行过程:



同样,它的时间复杂度T(n)=O(n),

不同的是,它占用的栈空间,不管n是多少,永远是一个单位的空间,所以,它的空间复杂度S(n)=O(1)。不严谨得说就是,它不占用栈空间,当然也不会造成栈溢出。

我们把这种运算过程成为“迭代”,把这种递归形式称为“尾递归”。

在介绍尾递归之前,我先来介绍一下“尾调用”。

尾调用就是这样:

int f(int n){

.

.

.

return g(_);

}

当程序运行完g(_)并得到返回值后,没有对这个返回值做任何运算,而是直接作为f(n)函数的返回值。所以,在调用g(_)之前,不需要保存现场,不需要压栈。

如果g(_)就是f(_),这个递归就是尾递归。

尾递归的调用需要编译器的支持,支持尾递归的编译器在优化模式下,会识别出这个递归是尾递归,就不需要压栈了。

如果编译器不支持尾递归优化,对于尾递归,它还是会压栈的。

它明明是递归,为什么不需要栈空间呢?

因为我们平常所说的“递归”,是两个不同的概念。

按照我的理解,“递归”,“迭代”,“循环”的关系是这样的:



写法是在具体形式上的表现,而运行过程是算法内在的本质。比如说快速排序在本质上就是一个递归过程,不管你是用递归写还是用循环写

⓪用循环写法来写迭代过程,就是我们最常用的方式

①用循环写法来写递归过程,就是自己压栈,自己出栈

②用递归写法来写迭代过程,例如上面的第2个阶乘

③用递归写法来写递归过程,例如上面的第1个阶乘

②是我最推荐的方式,既不会造成栈溢出(当然要有编译器的支持),也减少了循环带来的思维压力,还能令代码简洁明了。

下面的图标分别显示了用递归,尾递归,循环计算从1加到所花时间(时间单位别管,看比例就行了)(为了不扰乱大家的思绪,这里不严谨得不给出测试环境和具体代码)



可以看出,递归的确会慢许多,而尾递归和循环差异不大。

思维方式与函数构造方法

我们可以用上面的代换模型来理解函数的运行过程,但不必这样,我们可以用数学归纳法的思维理解递归,在理解的同时,我们也做了一次数学证明。

递归的思维方式

用数学归纳法证明递归阶乘算法的正确性:

证:

当n=0时,f(0)=1=0!,显然成立

假设f(n-1)的确等于(n-1)!,那么f(n)=n*f(n-1)=n*(n-1)!=n!

证毕

如何写递归函数?

第1步,先写基础情况

第2步,计算子问题,得出子问题的解,并确信它是正确的(不允许怀疑,也别去思考它为什么正确,它就是正确的)

第3步,用子问题的解构造原问题的解,并返回

简单的说,写递归就是思考怎样用子问题的解构造原问题的解(子问题->原问题)

迭代的思维方式

引入三个词,一个叫“状态”,一个叫“不变量”,一个叫“状态转移方程”。

以迭代阶乘函数为例




f(1,5),f(60,2),f(120,1)都是运行过程中的一个状态

这里的不变量是f(a,k)=n!

例如这里f(1,5),f(60,2),f(120,1)都等于120,在运行过程中永远不变

状态转移方程是,应用状态转移方程,函数就会从一个状态转换到下一个状态,不变量保持不变,问题规模减小

简单得说,写迭代函数就是要想办法把问题规模减小(原问题->缩小规模)

只要每一步能把问题规模减小,这个算法就是正确的

在后面的章节,我们将会用更多例子解释这章的内容

转载请附上原地址
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: