递归算法的特性
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了递归算法的特性相关的知识,希望对你有一定的参考价值。
参考技术A 递归算法是一种直接或者间接地调用自身算法的过程。在计算机编写程序中,递归算法对解决一大类问题是十分有效的,它往往使算法的描述简洁而且易于理解。
递归算法解决问题的特点:
(1) 递归就是在过程或函数里调用自身。
(2) 在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。
(3) 递归算法解题通常显得很简洁,但递归算法解题的运行效率较低。所以一般不提倡用递归算法设计程序。
(4) 在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等。所以一般不提倡用递归算法设计程序。 递归算法所体现的“重复”一般有三个要求:
一是每次调用在规模上都有所缩小(通常是减半);
二是相邻两次重复之间有紧密的联系,前一次要为后一次做准备(通常前一次的输出就作为后一次的输入);
三是在问题的规模极小时必须用直接给出解答而不再进行递归调用,因而每次递归调用都是有条件的(以规模未达到直接解答的大小为条件),无条件递归调用将会成为死循环而不能正常结束。
关于递归
递归算法一般用于解决三类问题:
(1)数据的定义是按递归定义的。(Fibonacci函数)
(2)问题解法按递归算法实现。
这类问题虽则本身没有明显的递归结构,但用递归求解比迭代求解更简单,如Hanoi问题。
(3)数据的结构形式是按递归定义的。
如二叉树、广义表等,由于结构本身固有的递归特性,则它们的操作可递归地描述。
下面来看看斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...,数列中从第三项起,每一项都是前两项之和。
再看看这段使用递归求斐波那契数的代码(代码来自网上),然后分析这样做有什么问题:
#include<iostream> using namespace std; int F(int n)//函数返回一个数对应的Fibonacci数 { if(n==0 || n==1)//递归边界 return 1; return F(n-1) + F(n-2);//递归公式 } int main() { //测试 int n; while(cin >> n) cout << F(n) << endl; return 0; }
编译运行,输入10,看看结果,输出89,和我们预计的一样。看起来无懈可击,递归的边界判断有了,递归逻辑也没什么问题,完美。
然而,我们再输入大一点的数,比如40,然后再看呢,能感觉到明显的运行时间了,在我i5的机器上,大概花了1秒钟。再大一点的数,时间花费就更明显了,计算第45个数,花了8秒钟,第47个数用了23秒,第50个数用了88秒。看起来不太妙,时间增长速度看来是指数级别的。可以证明事实确实是这样。
既然调用代码不能再简单,那就看看递归本身。加个计数器看看递归到底在干什么,运行了多少次:
#include<iostream> using namespace std; unsigned long total = 0; int F(int n) { total++; if (n == 0 || n == 1) return 1; return F(n - 1) + F(n - 2);//递归公式 } int main() { int n = 40; cout << F(n) << endl; cout << "total:" << total << endl; return 0; }
运行结果如下,可以看到,为计算第40个斐波那契数,递归被调用了3亿多次,怪不得这么耗时:
165580141
total:331160281
计算第50个数试试:
-1109825406
total:40730022147
结果计算了400多亿次,而且结果是负数,显然是错的。
换成unsigned long型,计算出了正确结果20365011074,因为超过了int类型能表示的最大整数2^32次(约为40亿)。即便是unsigned long型,使用更高效的算法后,依然面临着超过数据表示范围的问题,在计算第92个数时,即便以long型表示,依然出现了负数-6246583658587674878。大整数问题暂且放下,继续看递归问题。
上述递归,计算第一个数和第二个数时调用fib各一次,因为直接返回了:
F(0)=1,
F(1)=1
在计算第三个数时,F(2),F(1),F(0)各被调用一次,总共是3次;
计算第4个数时,F(3)本身一次,再加F(2)一次,F(1)一次,总共是1+3+1=5次
计算第5个数时,F(4)本身一次,再加F(3)一次,F(2)一次,总共1+5+3=9次
计算第6个数时,F(5)本身一次,再加F(4)一次,F(3)一次,总共1+9+5=15次
……
可以看出,和斐波那切数列很像,不过每次都会多加1,因为会调用自身一次。写代码验证一下总的调用次数:
long total = 0; long a = 1; long b = 1; for (int i = 0; i <= 50; i++) { if (i < 2) { total += 1; continue; } long tmp = b; b = a + b + 1; a = tmp; cout << b << endl; }
结果正是上面的40730022147。
嗯,单纯从递归公式来看,算法很完美,然而并不适合直接套用到代码里,因为计算机在处理递归的时候,需要堆栈存储前一次计算结果,所需要的堆栈数量和上面计算次数相同。斐波那切数列本身递推公式是无理数表达式,这里不详述了,感兴趣的自己搜一下,只需知道是按指数增长的,而这里这个总的调用次数计算也和斐波那切数的计算完全类似,不同的是每次多加了个1,然而还是指数级增长的。
根本原因是在递归函数中调用递归函数本身超过了1次,从而使堆栈使用量呈指数增长。要解决这个问题,只需避免多余的调用,类似下面做法:
#include<iostream> using namespace std; long a = 0; long b = 1; long fibs(int n) { if (n == 0 || n == 1){ return b; } long tmp = b; b = b + a; a = tmp; return fibs(n - 1); } int main() { cout << fibs(90) << endl; return 0; }
可以看到,使用全局变量的方法,免去了多余的堆栈使用次数,计算第90个斐波那切数也是非常快的。
自然,还是有整数溢出问题,比如上面参数传递93,会得出一个负数(我的机器是64位,具体long型表达范围结果根据使用的操作系统位数、编译器不同而不同)。当数据超过了基本数据类型的表达范围,就要想其他办法了,这个后续有机会再慢慢讨论吧,例如用字符串存储、计算大整数,等等。有关递归今天就先到这里了:)
以上是关于递归算法的特性的主要内容,如果未能解决你的问题,请参考以下文章