数据结构与算法解析 -- “递归”篇
- 2020-6-16
- 十步杀一人,千里不留行。事了拂衣去,深藏身与名。
李白 – 《侠客行 》
- 十步杀一人,千里不留行。事了拂衣去,深藏身与名。
一、概述
递归,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。也就是说,递归算法是一种直接或者间接调用自身函数或者方法的算法。
递归是一种应用非常广泛的算法(或者编程技巧)。很多数据结构和算法的编码实现都要用到递归,比如 DFS 深度优先搜索、前中后序二叉树遍历等等。
去的过程叫“递”,回来的过程叫“归”。基本上,所有的递归问题都可以用递推公式来表示。
诸如
f(n)=f(n-1)+1 其中,f(1)=1
用代码表示为
int f(int n) {if (n == 1) return 1;return f(n-1) + 1;
}
二、递归满足的三个条件
那究竟什么样的问题可以用递归来解决呢?
只要同时满足以下三个条件,就可以用递归来解决。
- 1、一个问题的解可以分解为几个子问题的解
何为子问题?子问题就是数据规模更小的问题。 - 2、这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样。
- 3、存在递归终止条件
把问题分解为子问题,把子问题再分解为子子问题,一层一层分解下去,不能存在无限循环,这就需要有终止条件。
三、如何编写递归代码
重点!!!:写递归代码最关键的是写出递推公式,找到终止条件,剩下将递推公式转化为代码就很简单了。
来个例子
假如这里有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这 n 个台阶有多少种走法?
可以根据第一步的走法把所有走法分为两类,第一类是第一步走了 1 个台阶,另一类是第一步走了 2 个台阶。所以 n 个台阶的走法就等于先走 1 阶后,n-1 个台阶的走法 加上先走 2 阶后,n-2 个台阶的走法。用公式表示就是:
f(n) = f(n-1)+f(n-2)
有了递推公式,递归代码基本上就完成了一半。我们再来看下终止条件。当有一个台阶时,我们不需要再继续递归,就只有一种走法。所以 f(1)=1。这个递归终止条件足够吗?我们可以用 n=2,n=3 这样比较小的数试验一下。
n=2 时,f(2)=f(1)+f(0)。如果递归终止条件只有一个 f(1)=1,那 f(2) 就无法求解了。所以除了 f(1)=1 这一个递归终止条件外,还要有 f(0)=1,表示走 0 个台阶有一种走法,不过这样子看起来就不符合正常的逻辑思维了。所以,我们可以把 f(2)=2 作为一种终止条件,表示走 2 个台阶,有两种走法,一步走完或者分两步来走。
所以,递归终止条件就是 f(1)=1,f(2)=2。这个时候,你可以再拿 n=3,n=4 来验证一下,这个终止条件是否足够并且正确。
我们把递归终止条件和刚刚得到的递推公式放到一起就是这样的:
f(1) = 1;
f(2) = 2;
f(n) = f(n-1)+f(n-2);
有了这个公式,我们转化成递归代码就简单多了。最终的递归代码是这样的:
int f(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
return f(n-1) + f(n-2);
}
写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。
递归比较难的地方是,当递归调用只有一个分支,也就是说“一个问题只需要分解为一个子问题”,我们很容易能够想清楚“递“和”归”的每一个步骤,所以写起来、理解起来都不难。
但是,当我们面对的是一个问题要分解为多个子问题的情况,递归代码就没那么好理解了。
对于分解成多个子问题的情况,人脑几乎没办法把整个“递”和“归”的过程一步一步都想清楚。
计算机擅长做重复的事情,所以递归正和它的胃口。而我们人脑更喜欢平铺直叙的思维方式。当我们看到递归时,我们总想把递归平铺展开,脑子里就会循环,一层一层往下调,然后再一层一层返回,试图想搞清楚计算机每一步都是怎么执行的,这样就很容易被绕进去。
对于递归代码,这种试图想清楚整个递和归过程的做法,实际上是进入了一个思维误区。很多时候,我们理解起来比较吃力,主要原因就是自己给自己制造了这种理解障碍。
那正确的思维方式应该是怎样的呢?
如果一个问题 A 可以分解为若干子问题 B、C、D,你可以假设子问题 B、C、D 已经解决,在此基础上思考如何解决问题 A。而且,你只需要思考问题 A 与子问题 B、C、D 两层之间的关系即可,不需要一层一层往下思考子问题与子子问题,子子问题与子子子问题之间的关系。屏蔽掉递归细节,这样子理解起来就简单多了。
因此,编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。
四、使用递归的注意事项
递归代码要警惕堆栈溢出
在实际的软件开发中,编写递归代码时,我们会遇到很多问题,比如堆栈溢出。而堆栈溢出会造成系统性崩溃,后果会非常严重。为什么递归代码容易造成堆栈溢出呢?我们又该如何预防堆栈溢出呢?函数调用会使用栈来保存临时变量。每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成返回时,才出栈。系统栈或者虚拟机栈空间一般都不大。如果递归求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险。
如何避免出现堆栈溢出呢?
- 限制递归调用的最大深度
递归调用超过一定深度(比如 1000)之后,我们就不继续往下再递归了,直接返回报错。
但这种做法并不能完全解决问题,因为最大允许的递归深度跟当前线程剩余的栈空间大小有关,事先无法计算。如果实时计算,代码过于复杂,就会影响代码的可读性。所以,如果最大深度比较小,比如 10、50,就可以用这种方法,否则这种方法并不是很实用。
- 限制递归调用的最大深度
- 递归代码要警惕重复计算
使用递归时还会出现重复计算的问题。对于上文中的那个爬楼梯的例子,如果我们把整个递归过程分解一下的话,那就是这样的:
从图中,我们可以直观地看到,想要计算 f(5),需要先计算 f(4) 和 f(3),而计算 f(4) 还需要计算 f(3),因此,f(3) 就被计算了很多次,这就是重复计算问题。
为了避免重复计算,我们可以通过一个数据结构(比如散列表)来保存已经求解过的 f(k)。当递归调用到 f(k) 时,先看下是否已经求解过了。如果是,则直接从散列表中取值返回,不需要重复计算,这样就能避免刚讲的问题了。- 时间、空间效率低
在时间效率上,递归代码里多了很多函数调用,当这些函数调用的数量较大时,就会积聚成一个可观的时间成本。在空间复杂度上,因为递归调用一次就会在内存栈中保存一次现场数据,所以在分析递归代码空间复杂度时,需要额外考虑这部分的开销。
- 时间、空间效率低
五、将递归代码转换成非递归代码
递归有利有弊,利是递归代码的表达力很强,写起来非常简洁;而弊就是空间复杂度高、有堆栈溢出的风险、存在重复计算、过多的函数调用会耗时较多等问题。所以,在开发过程中,我们要根据实际情况来选择是否需要用递归的方式来实现。
一般来说:递归代码一般可以使用,迭代循环的非递归写法
爬楼梯在不使用递归的代码如下|:
nt f(int n) {if (n == 1) return 1;if (n == 2) return 2;int ret = 0;int pre = 2;int prepre = 1;for (int i = 3; i <= n; ++i) {ret = pre + prepre;prepre = pre;pre = ret;}return ret;
}
六、参考资料
- 王争 – 《极客时间|数据结构与算法之美》
数据结构与算法解析 -- “递归”篇相关推荐
- 数据结构与算法之递归题目
数据结构与算法之递归题目 目录 求n!的结果 汉若塔问题 打印字符串的全部子序列,包括空字符串 打印一个字符串的全部排序 1. 求n!的结果 public static long getFactori ...
- Java入门算法(递归篇)丨蓄力计划
本专栏已参加蓄力计划,感谢读者支持 往期文章 一. Java入门算法(贪心篇)丨蓄力计划 二. Java入门算法(暴力篇)丨蓄力计划 三. Java入门算法(排序篇)丨蓄力计划 四. Java入门算法 ...
- 【数据结构与算法】起始篇
持续学习&持续更新中- 守破离 [数据结构与算法]起始篇 什么是数据结构和算法 什么是数据结构 什么是算法 为什么要学习数据结构和算法1 为什么要学习数据结构和算法2 为什么要学习数据结构和算 ...
- 数据结构和算法(十)递归-迷宫游戏
1. 数据结构和算法(十)递归-迷宫游戏 1.1 迷宫游戏 今天做一个简单的迷宫游戏,用二维数实现地图,让程序自动寻路的小游戏. 1.2 简单的迷宫 简单的迷宫 用二维数实现地图,找路策略:[右- ...
- 数据结构与算法之递归系列
本文来自一个不甘平凡的码农 写在前边 几个月之前就想写这样一篇文章分享给大家,由于自己有心而力不足,没有把真正的学到的东西沉淀下来,所以一直在不断的在自学. 然后又用了一个星期的时间去整理.分类,才有 ...
- 【数据结构与算法】第一篇:数据结构
系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 TODO:写完再整理 文章目录 系列文章目录 前言 STL库的介绍 容器(containers) 算法(algorith ...
- 数据结构与算法--第17篇(查找算法)
数据结构与算法 一,查找算法 1,简介: 2,顺序查找: 3,二分查找: 4,插值查找: 5,斐波那契(黄金分割法)查找: 一,查找算法 1,简介: 查找算法:常分为四种: 顺序查找(线性查找): 二 ...
- 大话数据结构与算法:基础篇
1.数据结构的重要性 数据结构是计算机软件相关专业的基础课程,几乎可以说,要想从事编程工作,无论是否是科班出身(比如我,标准的非科班人员,我是学医的,哈哈)都不可以绕过数据结构与算法这部分知识. 数据 ...
- python数据结构与算法 20 递归和递归三定律
递归 本章目标 理解很多复杂问题,如果不用递归就很难解决,用递归也许非常简单 学会怎样进行递归编程 理解并应用递归三定律 理解递归也是一种迭代 建立一个问题的递归方法 理解递归在计算机系统内是如何进行 ...
最新文章
- scala中Stream理解
- 当前系统license已过期什么意思_saas系统是什么意思?
- COM、COM+和DCOM的定义和区别
- 前端学习(1857)vue之电商管理系统电商系统之登录过程和token
- Ubuntu安装Google浏览器
- 用递归与分治策略求解网球循环赛日程表_算法设计:分治法(比赛日程安排)...
- 生活大爆炸版石头剪刀布(洛谷-P1328)
- 为OLED屏添加GUI支持2:2D图形库
- 再谈共相式GIS和ArcObjects
- 吴恩达机器学习(十二)主成分分析(降维、PCA)
- (C语言)字符串大小写无关查找替换
- 我要好offer之 二叉树大总结
- 若依二次开发添加 select 下拉框 变大 和 有空格存在
- pylon保存图片_3.Pylon 以实时图像采集讲解PylonC SDK使用流程 | 学步园
- 【vue】vue中element表格导出为excel表格
- 网站设计(3常用标签)
- android英文菜单,安卓recovery菜单中英文对照
- 《新时代的中国青年》白皮书——一个90后的自白
- java商家查询订单功能_查询订单详情示例代码
- java月份下拉菜单_实现一个日期下拉菜单
热门文章
- websocket实现用户在线管理与心跳检测
- 托福备考有用的心得 ——转载
- 污水管道流量测量,超声波与污水流量计比较
- stm32f103振镜_二维振镜激光扫描教学演示装置及其实现方法
- ADS 导入模型/模型库
- javaWeb 翻页功能实现思路
- Springboot毕设项目生鲜超市管理的设计与实现dvzk6java+VUE+Mybatis+Maven+Mysql+sprnig)
- 人像分割PP-HumanSeg模型onnx C++ windows部署
- SaaSBase:最受欢迎的跨境电商软件有哪些(下篇)
- 服务器智能导轨,R420/R520/R620/R720 R910 DELL服务器导轨 滑轨 支架 理线架