本文转自:http://blog.csdn.net/w453908766/article/details/50878154#comments
鉴于行业内对递归存在许多误解和疑惑,这里我想结合算法分析,写一个系列关于递归和算法分析的博客。
在这一章,我们要讲的是递归的形式和思维方法,在后面的章节里,我们会讲到时间复杂度和空间复杂度的分析,在那里我们就会看到,并不是什么递归函数都效率低,都会溢出。
概念背得再熟,你也不会写!
请写出这个函数
嗯,是这样写的:
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,在运行过程中永远不变
状态转移方程是,应用状态转移方程,函数就会从一个状态转换到下一个状态,不变量保持不变,问题规模减小
简单得说,写迭代函数就是要想办法把问题规模减小(原问题->缩小规模)
只要每一步能把问题规模减小,这个算法就是正确的
在后面的章节,我们将会用更多例子解释这章的内容
转载请附上原地址