美文网首页程序员和王有志一起学习数据结构与算法
05.编程技巧:使用迭代优化递归程序

05.编程技巧:使用迭代优化递归程序

作者: 王有志 | 来源:发表于2022-08-30 09:27 被阅读0次

大家好,我是王有志。

今天我们将会分析上篇文章中递归算法存在的问题,并通过迭代去优化。

递归存在的问题

上一篇中,我们计算了序号10以内的斐波那契数。今天为了清晰的展示递归解法存在的问题,我们试着计算序号为50的斐波那契数,如果电脑的性能较差的话,就不要尝试了。

斐波那契数列执行时间.png
可以看到,从计算F(40)开始,递归解法的耗时如同“坐火箭”般上升,这种情况是我们无法忍受的。

在上一篇文章中,我们有一个练习就是优化递归求解第n个斐波那契数。那么今天,我们就一起看看通过递归求解,问题出现在哪里?

通过之前构建的F(6)的递归树,我们可以看到,递归求解斐波那契数时,存在大量重复的计算,例如:仅仅是计算F(2)就出现了5次。

那么比较容易想到的优化方案就是缓存计算结果,使用时再取出。因此我们引入缓存,减少重复计算,提升执行速度(和复杂度分析中的空间换时间呼应上了)。

我们来看下具体实现:

private static long fib_recursion_memory(int n) {
    return fib_recursion_memory(n, new long[n + 1]);
}

private static long fib_recursion_memory(int n, long[] memory) {
    if (n < 2) {
        return n;
    }
    if (memory[n] != 0) {
        return memory[n];
    }

    memory[n] = fib_recursion_memory(n - 1, memory) + fib_recursion_memory(n - 2, memory);
    return memory[n];
}

代码并不复杂,通过引入long类型数组,记录已经计算过的斐波那契数,以达到减少重复计算的目的。

除此之外,还有没有其他方法求解第n个斐波那契数?

迭代

我们在使用递归时,通常是将规模较大的问题拆分为规模较小的问题,依次求解组合的过程。反之我们也可以从最小规模的问题开始,逐步累积到规模较大的问题。

迭代正是这样一种方法。迭代是数学概念引入编程中的,非常容易与循环混淆。来看下百度百科中的定义:

迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值。

既然说到非常容易与循环混淆,那么再来看看循环的定义:

循环是程序设计语言中反复执行某些代码的一种计算机处理过程,常见的有按照次数循环和按照条件循环。

通过定义我们很容易看出迭代与循环的差别,循环只是程序的重复执行,对计算结果没有要求,而迭代需要将每次计算结果应用到下一次循环中,所以可以说迭代只循环的子集

迭代求解第n个斐波那契数

事实上,递归都是可以改写为迭代的形式(不那么优雅),而且邓俊峰老师也在《数据结构》中说到:

实际上,属于尾递归形式的算法,均可以简捷地转换为等效的迭代版本。

这给了我们使用迭代改写递归求解斐波那契数列的理论基础,下面我们直接开始。

首先斐波那契数列的的递推公式是从第3项开始,因此我们需要处理第1,2项的特殊情况,代码如下:

if(n <= 0) {
    return 0
}

if(n < 3) {
    return 1;
}

根据递推公示,如果计算n的值,我们需要知道n-1n-2的值,那么我们声明3个变量,用p代表n-2,用q代表n-1,用result代表n,那么p设置为F(1)q设置为F(2)result设置为F(3),代码如下:

int p = 1, q = 1, result = p + q;

现在我们已经有了3个变量,表示F(1)F(3),仅仅是这些,你已经可以计算出F(4)的值了:

p = q;
q = result;
result = p + q;

如果想要计算F(n)的值,我们只需要不断的重复计算F(4)的过程即可:

for(int i = 4; i <= n; i++) {
    p = q;
    q = result;
    result = p + q;
}

完整的代码如下:

private static int fib(int n) {
    if(n <= 0) {
        return 0;
    }

    if (n < 3) {
        return 1;
    }

    int p = 1, q = 1, result = p + q;
    for (int i = 4; i <= n; i++) {
        p = q;
        q = result;
        result = p + q;
    }
    return result;
}

随着循环的进行,每次计算的结果都会带入到下一次的循环,这就是定义中所指的每次迭代结果作为下一次迭代的初始值进行处理

至于通过迭代求解的斐波那契数列的时间复杂度,相信大家一眼就可以看出来了吧?

如果你了解动态规划的话,你很容易就能想到,迭代求解斐波那契数列就是简单的动态规划解法,不过这是后话,现在我们按下不表。

结语

今天的内容到这里就结束了,我们来回顾下都聊了哪些内容:

首先是回顾了递归求解斐波那契数列的问题,通过“记忆”优化了递归的执行速度,但是增加了空间复杂度。

然后为了更高效,我们引入了迭代,虽然我并不鼓励大家记忆概念和定义,但是你要明白相似概念的区别。

最后我们通过迭代的方式求解斐波那契数列,实现了O(n)复杂度。当然,斐波那契数列还有O(\log{n})的解法,不过这不是我们今天的内容。

练习

我们来做几道简单的题目:

如果最第53题和121题没有解出来也并没有关系,这里涉及到动态规划的知识,还没有接触到的小伙伴也不要着急,后面是有动态规划的专题的。

本篇内容的代码仓库:


好了,今天就到这里了,Bye~~

相关文章

  • 05.编程技巧:使用迭代优化递归程序

    大家好,我是王有志。 今天我们将会分析上篇文章中递归算法存在的问题,并通过迭代去优化。 递归存在的问题 上一篇中,...

  • 递归、迭代与递推三者的差别

    递归,递推,迭代的区别_csdn链接 递归: 程序调用自身的编程技巧称为递归,是函数自己调用自己。 使用递归要注意...

  • 递归

    1.递归是什么? 定义:程序调用自身的编程技巧称为递归。递归使用的是选择结构,对于解决同样问题的孪生兄弟:迭代,它...

  • 递归的解析

    解释:程序调用自身的编程技巧叫做递归。 程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在...

  • Rust语言编程实例100题-028

    Rust语言编程实例100题-028 题目:递归练习。程序调用自身的编程技巧称为递归( recursion)。递归...

  • Rust语言编程实例100题-026

    Rust语言编程实例100题-026 题目:递归练习。程序调用自身的编程技巧称为递归( recursion)。递归...

  • Rust语言编程实例100题-027

    Rust语言编程实例100题-027 题目:递归练习。程序调用自身的编程技巧称为递归( recursion)。递归...

  • 递归

    递归定义 程序调用自身的编程技巧称为递归(recursion).递归就是程序调用自身不断深入嵌套,直到满足条件退出...

  • 递归和迭代

    递归的基本概念:程序调用自身的编程技巧称为递归,是函数自己调用自己。 使用递归要注意的有两点:递归就是在过程或函数...

  • 算法02-递归

    算法02-递归 一、定义 1、思想 程序调用自身的编程技巧称为递归(recursion)。 递归做为一种算法在程序...

网友评论

    本文标题:05.编程技巧:使用迭代优化递归程序

    本文链接:https://www.haomeiwen.com/subject/rpphnrtx.html