关于递归

Posted sevencd

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于递归相关的知识,希望对你有一定的参考价值。

递归算法一般用于解决三类问题:

(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型表达范围结果根据使用的操作系统位数、编译器不同而不同)。当数据超过了基本数据类型的表达范围,就要想其他办法了,这个后续有机会再慢慢讨论吧,例如用字符串存储、计算大整数,等等。有关递归今天就先到这里了:)







以上是关于关于递归的主要内容,如果未能解决你的问题,请参考以下文章

CSP核心代码片段记录

executePendingTransactions 的递归入口

nodejs常用代码片段

关于片段生命周期

JavaScript 代码片段

关于js----------------分享前端开发常用代码片段