算法与数据结构:当代程序员必备技能(算法)丨递归详解

​前言

递归是一种很是重要的算法思想,不管你是前端开发,仍是后端开发,都须要掌握它。在平常工做中,统计文件夹大小,解析xml文件等等,都须要用到递归算法。它太基础过重要了,这也是为何面试的时候,面试官常常让咱们手写递归算法。本文呢,将跟你们一块儿学习递归算法~前端

 

什么是递归?

递归,在计算机科学中是指一种经过重复将问题分解为同类的子问题而解决问题的方法。简单来讲,递归表现为函数调用函数自己。在知乎看到一个比喻递归的例子,我的以为很是形象,你们看一下:程序员

面试

递归最恰当的比喻,就是查词典。咱们使用的词典,自己就是递归,为了解释一个词,须要使用更多的词。当你查一个词,发现这个词的解释中某个词仍然不懂,因而你开始查这第二个词,惋惜,第二个词里仍然有不懂的词,因而查第三个词,这样查下去,直到有一个词的解释是你彻底能看懂的,那么递归走到了尽头,而后你开始后退,逐个明白以前查过的每个词,最终,你明白了最开始那个词的意思。算法

编程

来试试水,看一个递归的代码例子吧,以下:后端

 


递归的特色

实际上,递归有两个显著的特征,终止条件和自身调用:数组

自身调用:原问题能够分解为子问题,子问题和原问题的求解方法是一致的,即都是调用自身的同一个函数。微信

终止条件:递归必须有一个终止的条件,即不能无限循环地调用自己。函数

结合以上demo代码例子,看下递归的特色:学习

 

递归与栈的关系

其实,递归的过程,能够理解为出入栈的过程的,这个比喻呢,只是为了方便读者朋友更好理解递归哈。以上代码例子计算sum(n=3)的出入栈图以下:

 

为了更容易理解一些,咱们来看一下 函数sum(n=5)的递归执行过程,以下:

 

计算sum(5)时,先sum(5)入栈,而后原问题sum(5)拆分为子问题sum(4),再入栈,直到终止条件sum(n=1)=1,就开始出栈。

sum(1)出栈后,sum(2)开始出栈,接着sum(3)。

最后呢,sum(1)就是后进先出,sum(5)是先进后出,所以递归过程能够理解为栈出入过程啦~

递归的经典应用场景

哪些问题咱们能够考虑使用递归来解决呢?即递归的应用场景通常有哪些呢?

阶乘问题

二叉树深度

汉诺塔问题

斐波那契数列

快速排序、归并排序(分治算法也使用递归实现)

遍历文件,解析xml文件

 

递归解题思路

解决递归问题通常就三步曲,分别是:

第一步,定义函数功能

第二步,寻找递归终止条件

第二步,递推函数的等价关系式

这个递归解题三板斧理解起来有点抽象,咱们拿阶乘递归例子来喵喵吧~

1.定义函数功能

定义函数功能,就是说,你这个函数是干吗的,作什么事情,换句话说,你要知道递归原问题是什么呀?好比你须要解决阶乘问题,定义的函数功能就是n的阶乘,以下:

 

2.寻找递归终止条件

递归的一个典型特征就是必须有一个终止的条件,即不能无限循环地调用自己。因此,用递归思路去解决问题的时候,就须要寻找递归终止条件是什么。好比阶乘问题,当n=1的时候,不用再往下递归了,能够跳出循环啦,n=1就能够做为递归的终止条件,以下:

 

3.递推函数的等价关系式

递归的 「本义」 ,就是原问题能够拆为同类且更容易解决的子问题,即 「原问题和子问题均可以用同一个函数关系表示。递推函数的等价关系式,这个步骤就等价于寻找原问题与子问题的关系,如何用一个公式把这个函数表达清楚」 。阶乘的公式就能够表示为 f(n) = n * f(n-1), 所以,阶乘的递归程序代码就能够写成这样,以下:

 

「注意啦」 ,不是全部递推函数的等价关系都像阶乘这么简单,一会儿就能推导出来。须要咱们多接触,多积累,多思考,多练习递归题目滴~

leetcode案例分析

来分析一道leetcode递归的经典题目吧~

原题连接在这里哈:https://leetcode-cn.com/problems/invert-binary-tree/

「题目:」  翻转一棵二叉树。

输入:

 

输出:

 

照以上递归解题的三板斧来:

「1. 定义函数功能」

函数功能(即这个递归原问题是),给出一颗树,而后翻转它,因此,函数能够定义为:

 

「2.寻找递归终止条件」

这棵树何时不用翻转呢?固然是当前节点为null或者当前节点为叶子节点的时候啦。所以,加上终止条件就是:

 

「3. 递推函数的等价关系式」

原问题之你要翻转一颗树,是否是能够拆分为子问题,分别翻转它的左子树和右子树?子问题之翻转它的左子树,是否是又能够拆分为,翻转它左子树的左子树以及它左子树的右子树?而后一直翻转到叶子节点为止。嗯,看图理解一下咯~

 

首先,你要翻转根节点为4的树,就须要 「翻转它的左子树(根节点为2)和右子树(根节点为7)」 。这就是递归的 「递」 的过程啦

 

而后呢,根节点为2的树,不是叶子节点,你须要继续 「翻转它的左子树(根节点为1)和右子树(根节点为3)」 。由于节点1和3都是 「叶子节点」 了,因此就返回啦。这也是递归的「递」 的过程~

 

同理,根节点为7的树,也不是叶子节点,你须要翻转 「它的左子树(根节点为6)和右子树(根节点为9)」 。由于节点6和9都是叶子节点了,因此也返回啦。

 

左子树(根节点为2)和右子树(根节点为7)都被翻转完后,这几个步骤就 「归来」 ,即递归的归过程,翻转树的任务就完成了~

 

显然, 「递推关系式」 就是:invertTree(root)= invertTree(root.left) + invertTree(root.right);

因而,很容易能够得出如下代码:

 

这里代码有个地方须要注意,翻转完一棵树的左右子树,还要交换它左右子树的引用位置。

root.left=right; 

root.right=left;

所以,leetcode这个递归经典题目的 「终极解决代码」 以下:

 

拿终极解决代码去leetcode提交一下,经过啦~

 

递归存在的问题

递归调用层级太多,致使栈溢出问题

递归重复计算,致使效率低下

栈溢出问题

每一次函数调用在内存栈中分配空间,而每一个进程的栈容量是有限的。

当递归调用的层级太多时,就会超出栈的容量,从而致使调用栈溢出。

其实,咱们在前面小节也讨论了,递归过程相似于出栈入栈,若是递归次数过多,栈的深度就须要越深,最后栈容量真的不够咯

「代码例子以下:」

 

怎么解决这个栈溢出问题?首先须要 「优化一下你的递归」 ,真的须要递归调用这么屡次嘛?若是真的须要,先稍微 「调大JVM的栈空间内存」 ,若是仍是不行,那就须要弃用递归, 「优化为其余方案」 咯~

重复计算,致使程序效率低下

咱们再来看一道经典的青蛙跳阶问题:一只青蛙一次能够跳上1级台阶,也能够跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

绝大多数读者朋友,很容易就想到如下递归代码去解决:

 

可是呢,去leetcode提交一下,就有问题啦,超出时间限制了!

为何超时了呢?递归耗时在哪里呢?先画出 「递归树」 看看:

 

要计算原问题 f(10),就须要先计算出子问题 f(9) 和 f(8)

而后要计算 f(9),又要先算出子问题 f(8) 和 f(7),以此类推。

一直到 f(2) 和 f(1),递归树才终止。

咱们先来看看这个递归的时间复杂度吧, 「递归时间复杂度 = 解决一个子问题时间*子问题个数」

一个子问题时间 = f(n-1)+f(n-2),也就是一个加法的操做,因此复杂度是  「O(1)」 

问题个数 = 递归树节点的总数,递归树的总结点 = 2^n-1,因此是复杂度 「O(2^n)」 

所以,青蛙跳阶,递归解法的时间复杂度 = O(1) * O(2^n) = O(2^n),就是指数级别的,爆炸增加的, 「若是n比较大的话,超时很正常的了」 

回过头来,你仔细观察这颗递归树,你会发现存在 「大量重复计算」 ,好比f(8)被计算了两次,f(7)被重复计算了3次...因此这个递归算法低效的缘由,就是存在大量的重复计算!

「那么,怎么解决这个问题呢?」

既然存在大量重复计算,那么咱们能够先把计算好的答案存下来,即造一个备忘录,等到下次须要的话,先去 「备忘录」 查一下,若是有,就直接取就行了,备忘录没有才再计算,那就能够省去从新重复计算的耗时啦!这就是 「带备忘录的解法」

咱们来看一下 「带备忘录的递归解法」 吧~

通常使用一个数组或者一个哈希map充当这个 「备忘录」 

假设f(10)求解加上 「备忘录」 ,咱们再来画一下递归树:

「第一步」 ,f(10)= f(9) + f(8),f(9) 和f(8)都须要计算出来,而后再加到备忘录中,以下:

 

「第二步,」  f(9) = f(8)+ f(7),f(8)= f(7)+ f(6), 由于 f(8) 已经在备忘录中啦,因此能够省掉,f(7),f(6)都须要计算出来,加到备忘录中~

 

「第三步,」  f(8) = f(7)+ f(6),发现f(8),f(7),f(6)所有都在备忘录上了,因此均可以剪掉。

 

因此呢,用了备忘录递归算法,递归树变成光秃秃的树干咯,以下:

 

带「备忘录」的递归算法,子问题个数=树节点数=n,解决一个子问题仍是O(1),因此 「带「备忘录」的递归算法的时间复杂度是O(n)」 。接下来呢,咱们用带「备忘录」的递归算法去撸代码,解决这个青蛙跳阶问题的超时问题咯~,代码以下:

 

写在最后

既然大家能看到这里说明这篇文章对大家的帮助仍是有的,笔者可不能够给大家索要一个小小的赞呢。固然啦,笔者实际上是一位C/C++的程序员哦~天天分享的更多的固然是C语言C++的知识,不过今天看到这一篇Java程序员分享的算法知识仍是很不错的,因此分享给你们。

原文连接:https://xie.infoq.cn/article/0d1cf877d9e0b31a16cd76486?utm_source=tuicool&utm_medium=referral

另外若是你想更好的提高你的编程能力,学好C语言C++编程!弯道超车,快人一步!

分享(源码、项目实战视频、项目笔记,基础入门教程)

欢迎转行和学习编程的伙伴,利用更多的资料学习成长比本身琢磨更快哦!

C语言C++编程学习交流圈子,Q群1030652847点击进入】微信公众号:C语言编程学习基地

相关文章
相关标签/搜索