使用迭代优化递归程

封面:从斐波那契数列到递归.png

王有志,一个分享硬核Java技术的互金摸鱼侠
加入Java人的提桶跑路群:共同富裕的Java人

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

递归存在的问题

上一篇中,我们计算了序号10以内的斐波那契数。今天为了清晰的展示递归解法存在的问题,我们试着计算序号为50的斐波那契数,如果电脑的性能较差的话,就不要尝试了。
图1:斐波那契数列执行时间.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−1 和 n−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 ) O(\log\_{}{n}) O(log_n) 的解法,不过这不是我们今天的内容。

练习

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

  • 剑指Offer 10-I 斐波那契数列
  • 53.最大子数组和
  • 70.爬楼梯
  • 121.买卖股票的最佳时机

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


如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核Java技术的金融摸鱼侠王有志,我们下次再见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/608912.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Leetcode】236.二叉树的最近公共祖先

一、题目 1、题目描述 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” 示例1…

商品秒杀总结

秒杀模块编写思维总结 分为两种模式一个是限时购买&#xff0c;一个是限裤购买。 我们这里使用的是指定时间段下面的限库购买 单独使用一个库来存储数据&#xff0c;下面有两张表&#xff0c;一张表是具体的商品&#xff0c;一张表是订单。用户下的订单数据都要放到redis 中…

【Linux】journalctl和dmesg日志的区别

journalctl 和 dmesg 是两个在 Linux 系统中查看日志信息的工具&#xff0c;它们提供了不同层次和角度的系统日志。 journalctl&#xff1a; journalctl 是 systemd-journald 服务的一个前端&#xff0c;用于检查和查询系统日志。它使用 systemd 的日志系统&#xff0c;将日志信…

每日coding

今天无意点进一个回文串的题目&#xff0c;对于回文串我第一印象就用双指针&#xff0c;但是确实没写出来&#xff0c;看了评论区题解&#xff0c;绝大多数都是用dp来做的&#xff0c;但是找到一个用双指针做的&#xff0c;刚好延续思路&#xff0c;先把题目贴出来。 5、最长回…

64、图片预处理:Normalize

上一篇介绍了图像预处理中 resize 和 crop 的概念,在仓库的 python 预处理函数中,在 resize 和 crop之后,还有几个预处理的过程:一个是归一化,另外就是transpose 和reshape。 这一节就介绍一下,为什么在推理之前还需要对图像进行归一化。 归一化(Normalization) 在深…

Python用法:if __name__ == “__main__“的作用

文章目录 前言一、__name__是什么二、if __ name__ == __ main__语句的使用1.tempconv.py2.calc.py三、存在的问题查看__name__属性:四、总结前言 与C语言不同,Python是一种解释型脚本语言,在执行之前不同要将所有代码先编译成中间代码,Python程序运行时是从模块顶行开始,…

Nacos使用MySQL8时区问题导致启动失败

文章目录 配置下mysql的时区方式一 (永久)方式二&#xff08;临时&#xff09; 由于mysql8需要配置时区&#xff0c;如果不配置时区&#xff0c;nacos就连不上mysql&#xff0c;从而也就无法登录nacos自带的图形化界面 配置下mysql的时区 方式一 (永久) 直接修改配置文件&…

关于“Python”的核心知识点整理大全63

目录 20.2.11 使用 Git 跟踪项目文件 1. 安装Git 2. 配置Git 3. 忽略文件 .gitignore 注意 4. 提交项目 20.2.12 推送到 Heroku 注意 20.2.13 在 Heroku 上建立数据库 20.2.14 改进 Heroku 部署 1. 在Heroku上创建超级用户 注意 注意 20.2.11 使用 Git 跟踪项目文件…

Vue3-37-路由-组件内的路由守卫 onBeforeRouteLeave 和 onBeforeRouteUpdate

简介 组件内的路由守卫&#xff0c;实际上就是两个 API 方法。 他们与普通的守卫不同的是 &#xff1a; 他们是写在组件内的&#xff0c;在组件中监听路由的变化&#xff0c;不是全局的&#xff0c;比较灵活。 以下是两个 API 的功能说明&#xff1a;onBeforeRouteLeave() : 守…

Java中的序列化方法探索

.为什么要序列化 对象不序列化&#xff0c;能直接存储吗&#xff1f; 在 Java 中&#xff0c;序列化是将对象的状态信息转换为可以存储或传输的形式&#xff08;例如&#xff0c;转换为字节流&#xff09;的过程。在对象数据需要在网络上传输或需要在磁盘上持久化存储时&#…

指针的含义、表示、规范、存储、运用

指针的含义、表示、规范、存储、运用 指针的含义指针的表示指针的规范先声明再定义声明和定义一起表示错误表示 指针的存储理解一个变量的存储过程和原理理解一个指针的存储过程和原理理解多个指针的存储过程和原理 指针的运用 指针的含义 表示某个变量或数据所在的内存地址 注…

使用tailscale访问对端局域网上的其他设备

当tailscale客户端应用程序直接安装在组织中的每个客户端、服务器和虚拟机上时&#xff0c;Tailscale 效果最佳。这样&#xff0c;流量就会被端到端加密&#xff0c;并且无需配置即可在物理位置之间移动机器。 但是&#xff0c;在某些情况下&#xff0c;你不能或不想在每台设备…

Linux第18步_安装“Ubuntu系统下的C语言编GCC译器”

Ubuntu系统没有提供C/C的编译环境&#xff0c;因此还需要手动安装build-essential软件包&#xff0c;它包含了 GNU 编辑器&#xff0c;GNU 调试器&#xff0c;和其他编译软件所必需的开发库和工具。本节用于重点介绍安装“Ubuntu系统下的C语言编译器GCC”和使用。 1、在安装前…

图片纹理贴图

/* * 当需要给图形赋予真实颜色的时候&#xff0c;不太可能为没一个顶点指定一个颜色&#xff0c;通常会采用纹理贴图 * 每个顶点关联一个纹理坐标 (Texture Coordinate) 其它片段上进行片段插值 * */#include <iostream> #define STBI_NO_SIMD #define STB_IMAGE_IMPLE…

SQL窗口函数

一 什么是SQL窗口函数&#xff1f; SQL窗口函数说的是在查询现有的结果集的特定“窗口”&#xff08;即数据集的一个分区或整个结果集&#xff09;再作处理统计&#xff08;排序&#xff0c;聚合统计&#xff0c;如count,sum等&#xff09;&#xff0c;最简单的理解就是执行两…

Vim 用法详解

一、命令C&#xff1a;删除–>剪切–>进入插入模式 c命令详解&#xff1a; C or c$表示修改当前行上光标后面的部分. 进入编辑状态. c0 or c^表示从光标处到当前行行首的部分进行修改&#xff0c;^代表首个非空格处。 cc OR S修改当前行. 进入编辑状态. cw从光标所在…

【嵌入式移植】1、Ubuntu系统准备

Ubuntu系统准备 虚拟机与Ubuntu安装下载Ubuntu创建虚拟机系统配置 虚拟机与Ubuntu安装 嵌入式移植通常使用Linux操作系统的环境&#xff0c;使用Linux下的交叉编译工具链对BootLoader、kernel以及应用程序进行编译&#xff0c;然后下载运行。当然也可以通过各类IDE或者Windows…

从文本(.txt)文件中读取数据时出现中文乱码

前言 当需要从记事本中读取数据时&#xff0c;发现读取的数据会出现中文乱码&#xff0c;我尝试了C和C读取文件&#xff0c;发现都是这样。 乱码原因 文本文件的保存默认使用UTF-8编码方式&#xff0c;而VS编译器的编码方式是GBK&#xff0c;所以不同的编码方式导致了乱码。…

【leetcode】力扣算法之删除链表中倒数第n个节点【中等难度】

删除链表中倒数第n个节点 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 用例 输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5] 输入&#xff1a;head [1], n 1 输出&#xff1a;[] 输入&#xff1a;head …

各种锁的概述

乐观锁与悲观锁 悲观锁指对数据被外界修改持保守态度&#xff0c;认为数据很容易就会被其他线程修改&#xff0c;所以在数据被处理前先对数据进行加锁&#xff0c;并在整个数据处理过程中&#xff0c;使数据处于锁定状态。 悲观锁的实现往往依靠数据库提供的锁机制&#xff0…