• 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;
}

六、参考资料

  • 王争 – 《极客时间|数据结构与算法之美》

数据结构与算法解析 -- “递归”篇相关推荐

  1. 数据结构与算法之递归题目

    数据结构与算法之递归题目 目录 求n!的结果 汉若塔问题 打印字符串的全部子序列,包括空字符串 打印一个字符串的全部排序 1. 求n!的结果 public static long getFactori ...

  2. Java入门算法(递归篇)丨蓄力计划

    本专栏已参加蓄力计划,感谢读者支持 往期文章 一. Java入门算法(贪心篇)丨蓄力计划 二. Java入门算法(暴力篇)丨蓄力计划 三. Java入门算法(排序篇)丨蓄力计划 四. Java入门算法 ...

  3. 【数据结构与算法】起始篇

    持续学习&持续更新中- 守破离 [数据结构与算法]起始篇 什么是数据结构和算法 什么是数据结构 什么是算法 为什么要学习数据结构和算法1 为什么要学习数据结构和算法2 为什么要学习数据结构和算 ...

  4. 数据结构和算法(十)递归-迷宫游戏

    1. 数据结构和算法(十)递归-迷宫游戏 1.1 迷宫游戏   今天做一个简单的迷宫游戏,用二维数实现地图,让程序自动寻路的小游戏. 1.2 简单的迷宫 简单的迷宫 用二维数实现地图,找路策略:[右- ...

  5. 数据结构与算法之递归系列

    本文来自一个不甘平凡的码农 写在前边 几个月之前就想写这样一篇文章分享给大家,由于自己有心而力不足,没有把真正的学到的东西沉淀下来,所以一直在不断的在自学. 然后又用了一个星期的时间去整理.分类,才有 ...

  6. 【数据结构与算法】第一篇:数据结构

    系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 TODO:写完再整理 文章目录 系列文章目录 前言 STL库的介绍 容器(containers) 算法(algorith ...

  7. 数据结构与算法--第17篇(查找算法)

    数据结构与算法 一,查找算法 1,简介: 2,顺序查找: 3,二分查找: 4,插值查找: 5,斐波那契(黄金分割法)查找: 一,查找算法 1,简介: 查找算法:常分为四种: 顺序查找(线性查找): 二 ...

  8. 大话数据结构与算法:基础篇

    1.数据结构的重要性 数据结构是计算机软件相关专业的基础课程,几乎可以说,要想从事编程工作,无论是否是科班出身(比如我,标准的非科班人员,我是学医的,哈哈)都不可以绕过数据结构与算法这部分知识. 数据 ...

  9. python数据结构与算法 20 递归和递归三定律

    递归 本章目标 理解很多复杂问题,如果不用递归就很难解决,用递归也许非常简单 学会怎样进行递归编程 理解并应用递归三定律 理解递归也是一种迭代 建立一个问题的递归方法 理解递归在计算机系统内是如何进行 ...

最新文章

  1. scala中Stream理解
  2. 当前系统license已过期什么意思_saas系统是什么意思?
  3. COM、COM+和DCOM的定义和区别
  4. 前端学习(1857)vue之电商管理系统电商系统之登录过程和token
  5. Ubuntu安装Google浏览器
  6. 用递归与分治策略求解网球循环赛日程表_算法设计:分治法(比赛日程安排)...
  7. 生活大爆炸版石头剪刀布(洛谷-P1328)
  8. 为OLED屏添加GUI支持2:2D图形库
  9. 再谈共相式GIS和ArcObjects
  10. 吴恩达机器学习(十二)主成分分析(降维、PCA)
  11. (C语言)字符串大小写无关查找替换
  12. 我要好offer之 二叉树大总结
  13. 若依二次开发添加 select 下拉框 变大 和 有空格存在
  14. pylon保存图片_3.Pylon 以实时图像采集讲解PylonC SDK使用流程 | 学步园
  15. 【vue】vue中element表格导出为excel表格
  16. 网站设计(3常用标签)
  17. android英文菜单,安卓recovery菜单中英文对照
  18. 《新时代的中国青年》白皮书——一个90后的自白
  19. java商家查询订单功能_查询订单详情示例代码
  20. java月份下拉菜单_实现一个日期下拉菜单

热门文章

  1. websocket实现用户在线管理与心跳检测
  2. 托福备考有用的心得 ——转载
  3. 污水管道流量测量,超声波与污水流量计比较
  4. stm32f103振镜_二维振镜激光扫描教学演示装置及其实现方法
  5. ADS 导入模型/模型库
  6. javaWeb 翻页功能实现思路
  7. Springboot毕设项目生鲜超市管理的设计与实现dvzk6java+VUE+Mybatis+Maven+Mysql+sprnig)
  8. 人像分割PP-HumanSeg模型onnx C++ windows部署
  9. SaaSBase:最受欢迎的跨境电商软件有哪些(下篇)
  10. 服务器智能导轨,R420/R520/R620/R720 R910 DELL服务器导轨 滑轨 支架 理线架