「递归」的正确打开方式,看不懂你打我~

这是磊哥的第 189 期分享

作者 | 田小齐

来源 | 码农田小齐(ID:NYCSDE) 

分享 | Java中文社群(ID:javacn666)

前言

递归,是一个非常重要的概念,也是面试中非常喜欢考的。因为它不但能考察一个程序员的算法功底,还能很好的考察对时间空间复杂度的理解和分析。

本文只讲一题,也是几乎所有算法书讲递归的第一题,但力争讲出花来,在这里分享四点不一样的角度,让你有不同的收获。

  • 时空复杂度的详细分析

  • 识别并简化递归过程中的重复运算

  • 披上羊皮的狼

  • 适当炫技助我拿到第一份工作

算法思路

大家都知道,一个方法自己调用自己就是递归,没错,但这只是对递归最表层的理解。

那么递归的实质是什么?

答:递归的实质是能够把一个大问题分解成比它小点的问题,然后我们拿到了小问题的解,就可以用小问题的解去构造大问题的解。

那小问题的解是如何得到的?

答:用再小一号的问题的解构造出来的,小到不能再小的时候就是到了零号问题的时候,也就是 base case 了。

那么总结一下递归的三个步骤:

Base case:就是递归的零号问题,也是递归的终点,走到最小的那个问题,能够直接给出结果,不必再往下走了,否则,就会成死循环;

拆解:每一层的问题都要比上一层的小,不断缩小问题的 size,才能从大到小到 base case;

组合:得到了小问题的解,还要知道如何才能构造出大问题的解。

所以每道递归题,我们按照这三个步骤来分析,把这三个问题搞清楚,代码就很容易写了。

斐波那契数列

这题虽是老生常谈了,但相信我这里分享的一定会让你有其他收获。

题目描述

斐波那契数列是一位意大利的数学家,他闲着没事去研究兔子繁殖的过程,研究着就发现,可以写成这么一个序列:1,1,2,3,5,8,13,21… 也就是每个数等于它前两个数之和。那么给你第 n 个数,问 F(n) 是多少。

解析

用数学公式表示很简单:

代码也很简单,用我们刚总结的三步:

  • base case: f(0) = 0, f(1) = 1.

  • 分解:f(n-1), f(n-2)

  • 组合:f(n) = f(n-1) + f(n-2)

那么写出来就是:

class Solution {public int fib(int N) {if (N == 0) {return 0;} else if (N == 1) {return 1;}return fib(N-1) + fib(N-2);}
}

但是这种解法 Leetcode 给出的速度经验只比 15% 的答案快,因为,它的时间复杂度实在是太高了!

过程分析

那这就是我想分享的第一点,如何去分析递归的过程。

首先我们把这颗 Recursion Tree 画出来,比如我们把 F(5) 的递归树画出来:

那实际的执行路线是怎样的?

首先是沿着最左边这条线一路到底:F(5) → F(4) → F(3) → F(2) → F(1),好了终于有个 base case 可以返回 F(1) = 1 了,然后返回到 F(2) 这一层,再往下走,就是 F(0),又触底反弹,回到 F(2),得到 F(2) = 1+0 =1 的结果,把这个结果返回给 F(3),然后再到 F(1),拿到结果后再返回 F(3) 得到 F(3) = 左 + 右 = 2,再把这个结果返上去...

这种方式本质上是由我们计算机的冯诺伊曼体系造就的,目前一个 CPU 一个核在某一时间只能执行一条指令,所以不能 F(3) 和 F(4) 一起进行了,一定是先执行了 F(4) (本代码把 fib(N-1) 放在前面),再去执行 F(3).

我们在 IDE 里 debug 就可以看到栈里面的情况:这里确实是先走的最左边这条线路,一共有 5 层,然后再一层层往上返回。

没看懂的小伙伴可以看视频讲解哦~

时间复杂度分析

如何评价一个算法的好坏?

很多问题都有多种解法,毕竟条条大路通罗马。但如何评价每种方法的优劣,我们一般是用大 O 表达式来衡量时间和空间复杂度。

时间复杂度:随着自变量的增长,算法所需时间的增长情况。

这里大 O 表示的是一个算法在 worst case 的表现情况,这就是我们最关心的,不然春运抢车票的时候系统 hold 不住了,你跟我说这个算法很优秀?

当然还有其他衡量时间和空间的方式,比如

Theta: 描述的是 tight bound
Omega(n): 这个描述的是 best case,最好的情况,没啥意义

这也给我们了些许启发,不要说你平时表现有多好,没有意义;面试衡量的是你在 worst case 的水平;不要说面试没有发挥出你的真实水平,扎心的是那就是我们的真实水平。

那对于这个题来说,时间复杂度是多少呢?

答:因为我们每个节点都走了一遍,所以是把所有节点的时间加起来就是总的时间。

在这里,我们在每个节点上做的事情就是相加求和,是 O(1) 的操作,且每个节点的时间都是一样的,所以:

总时间 = 节点个数 * 每个节点的时间

那就变成了求节点个数的数学题:

在 N = 5 时,

最上面一层有1个节点,
第二层 2 个,
第三层 4 个,
第四层 8 个,
第五层 16 个,如果填满的话,想象成一颗很大的树:)

这里就不要在意这个没填满的地方了,肯定是会有差这么几个 node,但是大 O 表达的时间复杂度我们刚说过了,求的是 worst case.

那么总的节点数就是:
1 + 2 + 4 + 8 + 16

这就是一个等比数列求和了,当然你可以用数学公式来算,但还有个小技巧可以帮助你快速计算:

其实前面每一层的节点相加起来的个数都不会超过最后一层的节点的个数,总的节点数最多也就是最后一层节点数 * 2,然后在大 O 的时间复杂度里面常数项也是无所谓的,所以这个总的时间复杂度就是:

最后一层节点的个数:2^n

没看懂?别慌,去 B 站/油管看我的视频讲解哦,搜「田小齐」就好了。

空间复杂度分析

一般书上写的空间复杂度是指:

算法运行期间所需占用的所有内存空间

但是在公司里大家常用的,也是面试时问的指的是
Auxiliary space complexity

运行算法时所需占用的额外空间。

举例说明区别:比如结果让你输出一个长度为 n 的数组,那么这 O(n) 的空间是不算在算法的空间复杂度里的,因为这个空间是跑不掉的,不是取决于你的算法的。

那空间复杂度怎么分析呢?

我们刚刚说到了冯诺伊曼体系,从图中也很容易看出来,是最左边这条路线占用 stack 的空间最多,一直不断的压栈,也就是从 5 到 4 到 3 到 2 一直压到 1,才到 base case 返回,每个节点占用的空间复杂度是 O(1),所以加起来总的空间复杂度就是 O(n).

我在上面????的视频里也提到了,不懂的同学往上翻看视频哦~

优化算法

那我们就想了,为什么这么一个简简单单的运算竟然要指数级的时间复杂度?到底是为什么让时间如此之大。

那也不难看出来,在这棵 Recursion Tree 里,有太多的重复计算了。

比如一个 F(2) 在这里都被计算了 3 次,F(3) 被计算了 2 次,每次还都要再重新算,这不就是狗熊掰棒子吗,真的是一把辛酸泪。

那找到了原因之后,为了解决这种重复计算,计算机采用的方法其实和我们人类是一样的:记笔记

对很多职业来说,比如医生、律师、以及我们工程师,为什么越老经验值钱?因为我们见得多积累的多,下次再遇到类似的问题时,能够很快的给出解决方案,哪怕一时解决不了,也避免了一些盲目的试错,我们会站在过去的高度不断进步,而不是每次都从零开始。

回到优化算法上来,那计算机如何记笔记呢?

我们要想求 F(n),无非也就是要
记录 F(0) ~ F(n-1) 的值
那选取一个合适的数据结构来存储就好了。

那这里很明显了,可以用一个数组来存:

Index012345
F(n)011235

那有了这个 cheat sheet,我们就可以从前到后得到结果了,这样每一个点就只算了一遍,用一个 for loop 就可以写出来,代码也非常简单。

class Solution {public int fib(int N) {if (N == 0) {return 0;}if (N== 1) {return 1;}int[] notes = new int[N+1];notes[0] = 0;notes[1] = 1;for(int i = 2; i <= N; i++) {notes[i] = notes[i-1] + notes[i-2];}return notes[N];}
}

这个速度就是 100% 了~

但是我们可以看到,空间应该还有优化的余地。

那仔细想想,其实我们记笔记的时候需要记录这么多吗?需要从幼儿园到小学到初中到高中的笔记都留着吗?

那其实每项的计算只取决于它前面的两项,所以只用保留这两个就好了。

那我们可以用一个长度为 2 的数组来计算,或者就用 2 个变量。

更新代码:

class Solution {public int fib(int N) {int a = 0;int b = 1;if(N == 0) {return a;}if(N == 1) {return b;}for(int i = 2; i <= N; i++) {int tmp = a + b;a = b;b = tmp;}return b;}
}

这样我们就把空间复杂度优化到了 O(1),时间复杂度和用数组记录一样都是 O(n).

这种方法其实就是动态规划 Dynamic Programming,写出来的代码非常简单。

那我们比较一下 Recursion 和 DP:

Recursion 是从大到小,层层分解,直到 base case 分解不了了再组合返回上去;
DP 是从小到大,记好笔记,不断进步。

也就是 Recursion + Cache = DP

如何记录这个笔记,如何高效的记笔记,这是 DP 的难点。

有人说 DP 是拿空间换时间,但我不这么认为,这道题就是一个很好的例证。

在用递归解题时,我们可以看到,空间是 O(n) 在栈上的,但是用 DP 我们可以把空间优化到 O(1),DP 可以做到时间空间的双重优化。

其实呢,斐波那契数列在现实生活中也有很多应用。

比如在我司以及很多大公司里,每个任务要给分值,1分表示大概需要花1天时间完成,然后分值只有1,2,3,5,8这5种,(如果有大于8分的任务,就需要把它 break down 成8分以内的,以便大家在两周内能完成。)
因为任务是永远做不完的而每个人的时间是有限的,所以每次小组会开会,挑出最重要的任务让大家来做,然后每个人根据自己的 available 的天数去 pick up 相应的任务。

披着羊皮的狼

那有同学可能会想,这题这么简单,这都 2020 年了,面试还会考么?

答:真的会。

只是不能以这么直白的方式给你了。

比如很有名的爬楼梯问题:

一个 N 阶的楼梯,每次能走一层或者两层,问一共有多少种走法。

这个题这么想:

站在当前位置,只能是从前一层,或者前两层上来的,所以 f(n) = f(n-1) + f(n-2).

这题是我当年面试时真实被问的,那时我还在写 python,为了炫技,还用了lambda function:

f = lambda n: 1 if n in (1, 2) else f(n-1) + f(n-2)

递归的写法时间复杂度太高,所以又写了一个 for loop 的版本

def fib(n)a, b = 1, 1for i in range(n-1):a, b = b, a+breturn a 

然后还写了个 caching 的方法:

def cache(f):memo = {}def helper(x):if x not in memo:memo[x] = f(x)return memo[x]return helper
@cache
def fibR(n):if n==1 or n==2: return 1return fibR(n-1) + fibR(n-2)

还顺便和面试官聊了下 tail recursion:

tail recursion 尾递归:就是递归的这句话是整个方法的最后一句话。

那这个有什么特别之处呢?

尾递归的特点就是我们可以很容易的把它转成 iterative 的写法,当然有些智能的编译器会自动帮我们做了(不是说显性的转化,而是在运行时按照 iterative 的方式去运行,实际消耗的空间是 O(1))

那为什么呢?

因为回来的时候不需要 backtrack,递归这里就是最后一步了,不需要再往上一层返值。

def fib(n, a=0, b=1):if n==0: return aif n==1: return breturn fib(n-1, b, a+b)

最终,拿出了我的杀手锏:lambda and reduce

fibRe = lambda n: reduce(lambda x, n: [x[1], x[0]+x[1]], range(n), [0, 1])

看到面试官满意的表情后,就开始继续深入的聊了...

所以说,不要以为它简单,同一道题可以用七八种方法来解,分析好每个方法的优缺点,引申到你可以引申的地方,展示自己扎实的基本功,这场面试其实就是你 show off 的机会~lol

如果大家喜欢这种形式,请素质三连:

点击在看,鼓励下我!

瞎写点评论,假装我很红

转发给「需要」的人,即使他不需要

if快还是switch快?解密switch背后的秘密

一道题决定去留:为什么synchronized无法禁止指令重排,却能保证有序性?

关注公众号发送”进群“,老王拉你进读者群。

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

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

相关文章

Log4cpp 使用手册

参考资料&#xff1a; log4cpp 配置 与 使用http://www.cnblogs.com/welkinwalker/archive/2011/06/23/2088197.html 便利的开发工具-log4cpp快速使用指南 http://www.ibm.com/developerworks/cn/linux/l-log4cpp/ Log4cpp配置文件格式说明 http://sogo6.iteye.com/blog/115431…

switch 的性能提升了 3 倍,我只用了这一招!

这是我的第 190 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09; 分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;上一篇《if快还是switch快&#xff1f;解密switch背后的秘密》我们测试了 if 和 switch 的性能&am…

HashMap get不出对象时出错 解决

为什么80%的码农都做不了架构师&#xff1f;>>> 如题&#xff1a; Map map new HashMap(); map.put("1", "A"); map.put("2", "A"); map.put("3", "A"); map.put("4", "A")…

高质量SQL的30条建议!(后端必备)

这是我的第 191 期分享作者 | 捡田螺的小男孩来源 | 捡田螺的小男孩&#xff08;ID&#xff1a;gh_873ad5979a0b&#xff09; 分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;本文将结合实例demo&#xff0c;阐述30条有关于优化SQL的建议&#xff0c;多数…

Redis的自白:我为什么在单线程的这条路上越走越远?

这是我的第 192 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;我是 Redis&#xff0c;今年 11 岁了~曾几何时我是辣么的单纯&#xff0c;辣么的可爱&#xff0c;而如…

关于引用

2019独角兽企业重金招聘Python工程师标准>>> 1、 <!-- lang: html --> <!DOCTYPE HTML> <!-- lang: html --> <html> <!-- lang: html --> <head> <!-- lang: html --> <meta charset"utf-8" /> <!--…

一口气说出 6 种延时队列的实现方法,面试官满意的笑了

这是我的第 193 期分享作者 | 程序员内点事来源 | 程序员内点事&#xff08;ID&#xff1a;chegnxy-nds&#xff09; 分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;五一期间原计划是写两篇文章&#xff0c;看一本技术类书籍&#xff0c;结果这五天由于自…

acl 服务器编程框架特点介绍

2019独角兽企业重金招聘Python工程师标准>>> acl 中服务器框架模块是一个非常重要的模块&#xff0c;使用该模块技术人员可以快速地写出稳定、安全、高效的网络服务应用&#xff0c;该模块主要来源于著名的邮件服务器程序 (Postfix) 中的 master 模块&#xff0c;为…

人人都能看懂的 6 种限流实现方案!(纯干货)

这是我的第 195 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;为了上班方便&#xff0c;去年我把自己在北郊的房子租出去了&#xff0c;搬到了南郊&#xff0c;这样…

測试新浪微博@小冰 为代码机器人的一些方法

微软的微信小冰被腾讯封杀之后,如今移民到了新浪微博; 小冰 这里贴一些眼下有效的用来识别是这是"机器"而不是有正常人类智商的代码的方法: 1. 在正常的文字中夹杂其他符号,确保不存在有意义的连续的词汇,人眼能够分辨,机器不知所云而会露馅: 比方: ^^^小v冰^^^-…

秒建一个后台管理系统?用这5个开源免费的Java项目就够了

这是我的第 196 期分享作者 | Guide来源 | JavaGuide&#xff08;ID&#xff1a;JavaGuide&#xff09; 分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;大家好&#xff0c;我是 Guide 哥&#xff0c;一个三观比主角还正的技术人。今天推荐几个 Java 项目…

读《白帽子讲Web安全》之客户端脚本安全(一)

2019独角兽企业重金招聘Python工程师标准>>> 【第2章 浏览器安全】 1、同源策略&#xff08;Same Origin Policy&#xff09;是一种约定&#xff0c;它是浏览器最核心也最基本的安全功能。 浏览器的同源策略&#xff0c;限制了来自不同源的“document”或脚本&…

RocketMQ一行代码造成消息发送失败

这是我的第 198 期分享作者 | 丁威来源 | 中间件兴趣圈&#xff08;ID&#xff1a;dingwpmz_zjj&#xff09;分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;1、问题现象首先接到项目反馈使用 RocketMQ 会出现如下错误&#xff1a;错误信息关键点&#xf…

Java 中的 String 有没有长度限制?

这是我的第 199 期分享作者 | Hollis来源 | Hollis&#xff08;ID&#xff1a;hollischuang&#xff09; 分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;关于String有没有长度限制的问题&#xff0c;我之前单独写过一篇文章分析过&#xff0c;最近我又抽…

开源 免费 java CMS - FreeCMS2.1 菜单管理

2019独角兽企业重金招聘Python工程师标准>>> 项目地址&#xff1a;http://www.freeteam.cn/ 菜单管理 FreeCMS在设计时定位于面向二次开发友好&#xff0c;所以FreeCMS提供了菜单管理功能&#xff0c;二次开发人员可以自由增加新的功能菜单到FreeCMS。 为了让后台…

本来想用“{{”秀一波,结果却导致了内存溢出!

这是我的第 200 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;生活中的尴尬无处不在&#xff0c;有时候你只是想简单的装一把&#xff0c;但某些“老同志”总是在不…

局部变量竟然比全局变量快 5 倍?

这是我的第 201 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;喽&#xff0c;大家好&#xff0c;磊哥的性能优化篇又来了&#xff01;其实写这个性能优化类的文章初…

FreeMarker笔记 前言第1章 入门

简介 简介 FreeMarker是一款模板引擎&#xff1a;一种基于模板的、用来生成输出文本&#xff08;任何来自于HTML格式的文本用来自动生成源代码&#xff09;的通用工具。它是为Java程序员提供的一个开发包或者说是类库。它不是面向最终用户&#xff0c;而是为程序员提供的可以嵌…

厉害了,3万字的MySQL精华总结 + 面试100问!

这是我的第 202 期分享作者 | 派大新来源 | JavaKeeper&#xff08;ID&#xff1a;JavaKeeper&#xff09;分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;❝写在之前&#xff1a;不建议那种上来就是各种面试题罗列&#xff0c;然后背书式的去记忆&#x…

网页视频播放器代码大全 + 21个为您的站点和博客提供的免费视频播放器

推荐 使用 极酷 Web在线播放器。网页中嵌入视频代码综合全然版 1.avi格式 代码片断例如以下&#xff1a;  程序代码 <objectid"video"width"400"height"200"border"0"classid"clsid:CFCDAA03-8BE4-11cf-B84B-0020AFBBCCFA&q…