每日一题12:用数组加速递归

许多程序设计教学书上都用斐波那契数列(数列中第一二项都是1,其它任意一项都是其前两项之和)作为讲解递归的例子,作为教学例子,它确实十分合适,但是如果用在实际计算中,那么递归实现的斐波那契数列求值实在是太慢了,其中主要的原因是重复计算太多,这样的递归算法不仅速度效率低下,还容易造成栈溢出。如果能够保留下已经计算过的值,但需要时直接取用而不是重复计算,那么必然会提高程序性能。

对于斐波那契数列求解使用一个一维数组来保存之前的计算值,是十分合适的:

#include "stdafx.h"

#include <iostream>

using namespace std;
//递归解法
double Fibonacci(int n)
{
    if(n < 0) return -1;
    if(n == 0 || n == 1) return 1;
    //重复计算:比如计算Fibonacci(n - 1)时,就会计算
    //Fibonacci(n - 2)
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}
//利用一维数组的解法
double Fibonacci_a(int n)
{
    if(n < 0) return -1;
    if(n == 0 || n == 1) return 1;
    int m = n + 1;
    double *fibo = new double[m];
    fibo[0] = fibo[1] = 1;
    for (int i = 2; i < m; ++i)
    {
        fibo[i] = fibo[i - 1] + fibo[i - 2];
    }
    return fibo[n];
}

上述程序中,当n为50的时候程序运行递归版就已经十分缓慢了,而数组版当n达到我的系统能接受的最大值1475时,在觉察不到的等待时间内就得出了答案。而内存占用无非就是n加一些必须的字节而已。

这个解法让我想起了刚开始接触编程那会学院里组织的一次编程比赛,其中有一个就是需要利用数组加速递归的,那个题目已经忘记了,但是凭记得的大概也可以构造出一个来(不确定是不是原题目,但是思路是一样的),当时我提交的是一段投机取巧的解法,也许对于给定的输入可以给出答案,但是其他的就没有保证了。题目大概如下:

输入m、n、p三个非负整数,计算f(m,n,p)的值,其中,当m、n、p都不为0时,f(m,n,p) = f(m - 1,n,p) + f(m,n - 1,p) + f(m,n,p - 1),如果m、n、p中任一个为0时,f(m,n,p) = 1。按照定义使用递归可以很容易地得出解法,但是当m、n、p中任意一个数达到10的时候,程序运行起来就十分缓慢,原因就是重复计算实在太多,递归分支以指数级别增加(以3为底),所以必然不能用递归的方式求值。比赛完了之后,出题人(同班同学)问我用什么方式实现的,我说是假的,他提示我应该用一个三维数组,当时也没反应过来,不知道怎么用,这段时间开始学算法(之前都学了一些花架子,悔不当初啊),总算反应过来,所以实现了一个利用三维数组替换递归的方法,解题思路和斐波那契数列是一摸一样的,只是在计算数组元素位置、初始化记录数组的时候有一点点不一样而已(主要是因为在c或C++中三维数组与一维数组的对应关系是由程序员编写造成的,否则哪那么多事),多余的就不说了,直接上代码:

//递归版
double f(int i,int j, int k)
{
    if(i < 0 || j < 0 || k < 0) return -1;
    if(i == 0 || j == 0 || k == 0) return 1;
    return f(i - 1,j,k) + f(i,j - 1,k) + f(i,j,k - 1);
}

//三维数组版
double f_a(int m,int n, int p)
{
    if(m < 0 || n < 0 || p < 0) return -1;
    if(m == 0 || n == 0 || p == 0) return 1;
    m++;
    n++;
    p++;
    double *f_array = new double[m*n*p];
    memset(f_array,0,m*n*p*sizeof(double));
    for (int j = 0; j < n; ++j)
    {
        for (int k = 0; k < p; ++k)
        {
            f_array[j*p + k] = 1;
        }
    }
    for (int i = 0; i < m; ++i)
    {
        for (int k = 0; k < p; ++k)
        {
            f_array[i*n*p + k] = 1;
        }
    }
    for (int i = 0; i < m; ++i)
    {
        for (int j = 0; j < p; ++j)
        {
            f_array[(i*n + j)*p] = 1;
        }
    }
    //这个三重循环不是按行、列、“厚度”的顺序,而是倒过来的
    for (int k = 1; k < p; ++k)
    {
        for (int j = 1; j < n; ++j)
        {
            for (int i = 1; i < m; ++i)
            {
                f_array[(i*n + j)*p + k] = f_array[((i-1)*n + j)*p + k] + f_array[(i*n + j - 1)*p + k] + f_array[(i*n + j)*p + k - 1];
            }
        }
    }
    return f_array[m* n * p - 1];
}

int _tmain(int argc, _TCHAR* argv[])
{
    int n = 100;
    cout<<f_a(n,n,n)<<endl;
    return 0;
}

当n取100时基本上也是立马出结果,当n取到200时,需要等待1s左右,这时候大量的计算不是花在真正的加法上,而是花在了数据的读取(包括位置计算、指针移动),计算出来的数也是快到10的300次方了(当输入较小时,我对比了两个版本的结果,所以数组版的程序还是比较可信,我只保证理论上是完全正确的,数比较大时,我没有提供任何看得到的证据)。

用《数据结构与算法分析:C语言描述》第一章里的对于递归算法设计的准则作为结语吧:

1)基准情形。必须总有某些基准情形,它无需递归就能解出。

2)不断推进。对于那些需要递归求解的情形,每一次递归调用都必须要使求解状况朝接近基准情形的方向推进。

3)设计法则。假设所有的递归调用都能运行。

4)合成效益法则。在求解一个问题的同一实例时,切勿在不同的递归调用中做重复的工作。

很明显上面两个问题的递归解法都违背了第四条准则。

时间: 2024-08-04 22:21:31

每日一题12:用数组加速递归的相关文章

每日一题14:数组与链表组合方案下的Josephus问题

愚人节与自己开了个很大的玩笑,几天没写程序,今天继续!Josephus问题是说N个人围成一个圈传热土豆,先约定一个数M,当传递了M次的时候拿着土豆的人出局,然后将土豆给出局人的下一个人,游戏继续,直到最后只剩下一个人,求出局人的序列(按出局顺序排列). 这个问题可以用数组实现,但是需要标记代表出局人的元素,并且没遍历一个元素就要检查该元素是否已被标记为出局,这样程序运行时间必然会变慢.另一种方式是使用一个链表,每次把出局的节点删除掉.这样的解决方案非常直观,只需要关注链表中的节点,因为在链表中的

经典算法题每日演练——第十题 树状数组

原文:经典算法题每日演练--第十题 树状数组 有一种数据结构是神奇的,神秘的,它展现了位运算与数组结合的神奇魅力,太牛逼的,它就是树状数组,这种数据结构不是神人是发现不了的. 一:概序 假如我现在有个需求,就是要频繁的求数组的前n项和,并且存在着数组中某些数字的频繁修改,那么我们该如何实现这样的需求?当然大家可以往 真实项目上靠一靠. ① 传统方法:根据索引修改为O(1),但是求前n项和为O(n). ②空间换时间方法:我开一个数组sum[],sum[i]=a[1]+....+a[i],那么有点意

老男孩教育每日一题-2017年5月12日-磁盘知识点:linux系统中LVM配置实现方法?

1.题目 2.参考答案 01:将一个或多个物理分区创建为一个PV # pvcreate /dev/sdb{1,2} Physical volume "/dev/sdb1" successfully created Physical volume "/dev/sdb2" successfully created # pvs          #<- 查看系统中的PV信息 PV                 VG   Fmt      Attr     PSiz

C语言每日一题之No.1

鉴于在学校弱弱的接触过C,基本上很少编程,C语言基础太薄弱.刚好目前从事的是软件编程,难度可想而知.严重影响工作效率,已无法再拖下去了.为此,痛下决心恶补C语言.此前只停留在看书,光看好像也记不住,C这东西毕竟是练出来的,所以从今天开始,每日一道C语言题目,从题目入手来补知识漏洞.题目比较基础,如不堪入目,还请见谅. 题目:输入三个整数,输出最大的数 思路:定义三个变量用来存储输入的整数 比较三个变量的大小,找到最大的数 定义一个变量存储来存储最大的数 程序: 1 #include <stdio

c#新手_每日一题(七)

进击c#的小白一枚,望大神指点. 每日一题:第7题请编写函数int[] GetPrime(int m),其功能是:将所有大于1小于整数m的素数存入prime[]数组中,并传回. 所谓素数,就是除了1和此整数自身外,没法被其他自然数整除的数. static void Main(string[] args) { int m = 12; GetPrime(m); Console.ReadLine(); } static int[] GetPrime(int m) { int[] prime = new

C语言每日一题之No.8

正式面对自己第二天,突然一种强烈的要放弃的冲动,在害怕什么?害怕很难赶上步伐?害怕这样坚持到底是对还是错?估计是今天那个来了,所以身体激素有变化导致情绪起伏比较大比较神经质吧(☆_☆)~矮油,女人每个月总有这么几天的....晚上闺蜜打电话来,共同探讨了作为单身女性身在一线城市的生活,互相安慰互相关心,心里一下子就温暖了许多.总在这个时候,你会觉得,这个冷静的城市里你不是一个人在行走,还有另一颗心牵挂着你.嘿嘿,回来该学习还学习.现在不管坚持是对的还是错的,你都踏上了研发这条不归路,那就一条黑走到

老男孩教育每日一题-第126天-通过shell脚本打印乘法口诀表

问题背景: 生成9*9乘法表 [[email protected] ~]# seq 9 | sed 'H;g' | awk -v RS='' '{for(i=1;i<=NF;i++)printf("%dx%d=%d%s", i, NR, i*NR, i==NR?"\n":"\t")}' 1x1=1 1x2=2   2x2=4 1x3=3   2x3=6   3x3=9 1x4=4   2x4=8   3x4=12  4x4=16 1x5=5

算法题:求数组中最小的k个数

说明:本文仅供学习交流,转载请标明出处,欢迎转载! 题目:输入n个整数,找出其中最小的k个数. <剑指offer>给出了两种实现算法: 算法1:采用Partition+递归法,该算法可以说是快速排序和二分查找的有机结合.算法的时间复杂度为O(n),缺点在于在修改Partition的过程中会修改原数组的值. 算法2:采用top-k算法.如果要找最小的K个数,我们才用一个含有K个值的大顶堆:如果要找最大的K个数,我们采用小顶堆.该算法的时间复杂度为O(nlogK),是一种比较好的算法,启发于堆排序

老男孩教育每日一题-2017年5月17日-使用三剑客进行变化格式

1.题目 原始数据: 17/Apr/2015:09:29:24 +0800 17/Apr/2015:09:30:26 +0800 17/Apr/2015:09:31:56 +0800 18/Apr/2015:09:34:12 +0800 18/Apr/2015:09:35:23 +0800 19/Apr/2015:09:23:34 +0800 19/Apr/2015:09:22:21 +0800 20/Apr/2015:09:45:22 +0800 期望结果: 2015-04-17 09:29: