18.面试算法-递归基础

embedded/2024/10/10 20:05:28/

算法的核心无疑是递归思想和深度优先的问题。我们首先来分析怎么写递归的代码,有些题目用非递归也能解决,这里我们的重点是训练递归

1.递归的特征

递归,大部分人都知道怎么回事,但是代码就是写不出来,所谓“你讲的都对,但我就是不会”。递归的本质仍然是方法调用,不过是自己调用自己,系统给我们维护了不同调用之间的保存和返回等功能。

这种例子在现实中也有很多的,例如有一个笑话:

从前啊,有座山,山上有座庙,庙里有个老和尚和一个小和尚在讲故事,老和尚对小和尚说:

从前啊,有座山,山上有座庙,庙里有个老和尚和一个小和尚在讲故事,老和尚对小和尚说:

从前啊,有座山,山上有座庙,庙里有个老和尚和一个小和尚在讲故事,老和尚对小和尚说:

·····

如果看递归代码的结构,就像下面这个样子,前面的每一层都去一模一样地调下一层,不同的只是输入和输出的参数。
在这里插入图片描述
当然这个过程不能一直持续下去,一定要在满足某个要求之后返回结果的,所以递归里总是有一个终止的代码。例如阶乘的递归调用时到了1之后就要逐步返回了。

递归的问题,我们经常感觉很晕,如何有效学习递归呢?笔者总结了递归的几个典型的特征:

①执行时范围不断缩小,这样才能触底反弹

②终止判断在调用递归的前面,一般都在最前面

③自己写的时候从小到大递推更容易。

理解这几条经验可以辅助我们更好的理解递归,我们一条条来看:

【1】执行范围不断缩小

递归就是数学里的递推,设计递归就是努力寻找数学里的递推公式,例如阶乘的递推公式就是f(n)=n*f(n-1),很明显一定是要触底之后才能反弹。再比如斐波那契数列的递归公式为f(n)=f(n-1)+f(n-2),n也在不断缩小。这条规律可以辅助我们检查递推公式对不对。
再比如青蛙跳台阶的递推公式为:f(n)=f(n-1) + f(n-2),哎!怎么和斐波那契数列一样?是的,就是一样。

范围缩小不一定只体现n的变化上,在递归中我们会大量使用类似这样的结构:

java">int leftDepth = getDepth(node.left); // 左
int rightDepth = getDepth(node.right); // 右

递归一次,都将范围缩小到当前节点的左子或者右子,范围也是在不断缩小的。

【2】终止条件判断在递归调用的前面

终止判断一般是在最前面的,也就是下面这样:

java">public void recursion(参数0) { if (终止条件) {return ;}recursion(参数1);  print(root.value);
}

终止判断条件至少要在递归的前面,为什么呢?看下面这一个例子,很明显这样会一直递归下去,无法退出来,直到抛出堆栈溢出异常(StackOverflowError)。

java">public void recursion(参数0) { recursion(参数1);if (终止条件) { return ;}
}

理解这条规律,可以降低我们写递归的难度。实际一个方法里的递归调用可能不止一次,还会加一些逻辑处理,比如下面这样,但是终止的条件仍然在前面。

java">public void recursion(参数0) { if (终止条件) {return; }//可能有一些逻辑运算1 recursion(参数1);// 可能有一些逻辑运算1 recursion(参数2);// ......3recursion(参数n);// 可能有一些逻辑运算
}

这一特点启示我们,假如要写递归,可以先考虑清楚什么情况下要终止,而且相关代码要写在靠前位置的,明确这些,递归就完成一半了。对于二叉的问题,我们会花大量时间来分析什么时候终止,而这些问题明确了,剩下的就水到渠成了。

【3】写的时候从小到大递推更容易

递归该怎么写呢?递归源自数学里的归纳法,这个在高中数学中学过,大致就是你先猜测出存在递归关系,f(n)=δf(n-1),然后你只要证明当 n增加一个时, f(n+1)=δf(n)也是成立就说明你的猜测是对的。不过我们写递归一般不需要证明,验证的时候也可以先选几个比较小的,再选择几个比较大的验证即可。

我们写递归的一个重要工作就是确定这个递推关系。那该如何猜测出递推关系呢?很明显从n=1, 2, 3开始写最简单。例如斐波那契序列为 1 1 2 3 5 8 …,很明显从n=3开始都满足f(n)= f(n-1) + f(n-2),然后我们再选择某个比3 个更大的n来验证即可。

这个思路也是我们后面实现递归算法的重要法宝之一。不过在写的时候不仅仅要从小到大考虑,还可能要分很多种情况来讨论,我们后面再说。

确定递推公式无疑是最重要的问题,根据笔者的经验,先从小到大递推一下看看,会更容易。不要上来就考虑n很大的情况,因为我们大部人的抽象能力没那么好,特别是在面试的时候容易慌,直接写 n的递推公式容易和n-1的处 理杂糅在一起,以致越写越乱。

我们还是从简单的开始一步步分析递归的过程。我们仍然以阶乘和斐波那契数列为例来看。斐波那契数列的是这样一个数列: 1 、1 、2 、3 、5 、8 、13 、21 、34…. ,即第一项 f(1) = 1,第二项 f(2) = 1…… ,很明显就是第 n 项目为 f(n)= f(n-1) + f(n-2)。求第 n 项的值是多少。阶乘也一样:

java">n=1 f(1)=1
n=2 f(2)=2*f(1)=2
n=3 f(3)=3*f(2)=6
n=4 f(4)=4*f(3)=24
....

由此我们可以推测递推公式是f(n)=n*f(n-1)。

递归有两种方式,一种是自下而上,一种是自顶向下,前者就是我们上面说的从小到大递推。而后者在某个场景写更好些,但是不太好验证,而且还存在大量重复计算的问题。因此我们推荐自下而上方式,大部分递归问题,都可以从小到大逐步分析,而且这样也可以非常方便的找到各种情况下的终止条件,因此能让我们更容易写出递归程序。

2.如何写递归

从前面的分析我们可以看到,要写出递归,需要解决几个问题:

①递推公式是什么,这是递归的灵魂,我们必须先明确了才可以下手。

②入参和出参是什么,怎么保证递归时入参能不断缩小。

递归在什么情况下可以终止,此时要返回什么,先明确终止条件可以降低实现难度。

④前面问题都解决之后,将递推公式转换成方法调用,组装成完整的实现代码。

⑤怎么验证写的递归对不对。一般的方法我们debug一下就行,但是递归很难,debug几次之后可能自己就晕了,特别是在返回的时候,我们经常搞不清楚当前的n到底是什么。

这些基本步骤是相辅相成的,顺序也不一定是上面的一二三,但是我们可以不断思考这几个问题。我们已经明确了什么,还没明确什么,从而将问题步步拆解。

第一步:从小到大递推,分情况讨论

如上一节所述从小到头递推,可以大大降低编写难度,可以方便总结出递推公式什么、会有几种情况,这样也能总结出结束条件是什么。后面题目我们再看。

第二步:明确结束条件,初步确定入参

我们说过递归里终止条件一定是靠前的,而大部分递归的终止条件不过是n最小开始触底反弹时的几种情况。例如 n=0, n=1,必要的时候加个n=2时要返回哪种固定结果。例如对于1+2+3+…累加的话,终止条件就是:

java">int f(int n){if(n ==1){return 1;}
}

对于阶乘,当 n = 1 时你就应该知道 f(1) = 1 ,也就是下面这样子:

java">// 算n的阶乘(假设n不为0)
int f(int n){if(n == 1){return 1;}
}

有时候需要考虑的终止条件不止一个,例如斐波那契数列的递推公式f(n) = f(n-1) + f(n-2)里,如果n=2时会出现 f(2)=f(1)+f(0),很明显这里是没有f(0)的,所以我们要将n==2也给限制住,所以结束条件是这样的:

java">int f(int n){if(n <=2){return 1;}
}

有些情况不一定是触底才开始反弹,而是达到某种要求就可以停止了,这就是递归的剪枝优化。解决这类问题最直接的方式就是枚举,将可能的情况列举一下,再逐步优化。只有列举清楚了才可能将终止条件写完整,所以在面试的时候千万不要上来就写,而应该先和面试官讨论你的设计方案,不要害怕与面试官讨论!假如有明显的缺陷他甚至会提醒你的,所以这也是借力打力的一个技巧。

之后是初步确定入参和出参,为什么说要初步确定呢?平时写工程代码我们也会发现很多方法很难上来就将入参想清楚,都是先写,缺了什么再补,递归对此要求更高,不仅要满足当前需要,还要满足递归的需要,所以这个很难一下子想出来,我们可以先将能想到的参数都大胆的写上去,然后一边写一边改。笔者面试时一般第一次写的都非常随意,而且有大量的涂改,所以写完后会和面试官说,刚刚写的比较随意,我马上重抄一遍。重抄一遍的会比较工整,让别人能看清楚,一般面试官是允许你这么做的。

第三步:将等价关系变成方法调用,完成代码

同一个问题,不同的人实现差异可能会非常大,甚至只用一行就将所有操作都包含进去了。我们不用迷信那些骚操作,而应先分情况逐个先写出来,之后再看能否精简优化,不要步子太大,否则会扯到蛋。这里的阶乘和斐波那契数列还是能比较容易找到的,继续完善我们的代码,如下:

java">// 算n的阶乘(假设n不为0)
int f(int n){if(n ==1){return n;}// 把f(n)的等价操作写进去 return f(n-1) * n;
}

斐波那契数列的为:

java">int f(int n){// 1.先写递归结束条件 if(n <= 2){return 1; }// 2.接着写等价关系式return f(n-1) + f(n - 2);
}

这么说没什么问题,但是真正想清楚还是有些难度的。每次写递归的时候,你就强迫自己试着去从这几个方面来考虑。后面我们会不断通过题目来感受上面这个过程。

第四步:从大到小画图推演

如果时间还够,特别是刚开始学习时候,写完后可以再画图推演一遍。递归的代码很难调试,特别是返回的时候系统帮我们完成了很多事情,自己调试几步就晕了。为此,笔者发现一个比较好的方式是从大到小,画图推演一下。

我们知道递归本质上仍然是方法调用,所以可以按照方法调用的方式来分析写的对不对。下面这个图完整的表示了求阶乘的过程,你会发现递归不过是一个方法被调了好几次,每次n都在减小,这就是递进的过程。触底之后,也就是满足终止条件之后就开始返回了。递进的时候当前层的n被系统给保存了,而返回的时候会自动设置回来,因此每层的n 自然是不一样的,所以此时就是重新拿到当前这一层 n的值完成计算即可。

例如我们将f(4)阶乘的过程如下:
在这里插入图片描述
通过这个图你会发现所谓的递归与普通的方法调用没什么区别,只不过我们平时是f(n)里调用g(m),这里是f(n)继续调用自己而已,没有任何神秘的。


http://www.ppmy.cn/embedded/124608.html

相关文章

Hive数仓操作(五)

一、Hive 信息查看 Hive的元数据管理&#xff1a; Hive 将表的元数据&#xff08;如表名、列名、类型等&#xff09;存储在关系型数据库中&#xff0c;通常是 MySQL。元数据的主要表包括&#xff1a; TBLS&#xff1a;存储表的信息&#xff08;表名、类型、ID 等&#xff09;。…

【算法】博弈论(C/C++)

个人主页&#xff1a;摆烂小白敲代码 创作领域&#xff1a;算法、C/C 持续更新算法领域的文章&#xff0c;让博主在您的算法之路上祝您一臂之力 欢迎各位大佬莅临我的博客&#xff0c;您的关注、点赞、收藏、评论是我持续创作最大的动力 目录 博弈论&#xff1a; 1. Grundy数…

如何理解运行 lspci 命令得到的输出信息?

输出信息 00:00.0 Host bridge: Intel Corporation Sky Lake-E DMI3 Registers (rev 04) 00:04.0 System peripheral: Intel Corporation Sky Lake-E CBDMA Registers (rev 04) 00:04.1 System peripheral: Intel Corporation Sky Lake-E CBDMA Registers (rev 04) 00:04.2 Sy…

(Kafka源码五)Kafka服务端处理消息

Kafka 服务端&#xff08;Broker&#xff09;采用 Reactor 的架构思想&#xff0c;通过1 个 Acceptor&#xff0c;N 个 Processor(N默认为3)&#xff0c;M 个 KafkaRequestHandler&#xff08;M默认为8&#xff09;&#xff0c;来处理客户端请求&#xff0c;这种模式结合了多线…

gitSVN提交规范

commit message subject &#xff1a; 空格 message 主体 例如&#xff1a; feat&#xff1a;增加用户注册功能 常见的 subject 种类以及含义如下&#xff1a; feat: 新功能&#xff08;feature&#xff09; 用于提交新功能。 例如&#xff1a;feat: 增加用户注册功能 f…

学习《啊哈,算法!》的时候的感想

方法&#xff0c;看完要想一想怎么用到学习中来。 ——小龙 就比如说我在大一看的书里面写的——写代码之前可以写一下流程图&#xff0c;大一升大二的暑假自学算法&#xff0c;我最初看代码老是错误&#xff0c;便想起来减少代码错误的一种大佬用的方法&#xff0c;写代码之前…

SkyWalking 告警功能

SkyWalking 告警功能是在 6.x 版本新增的,其核心由一组规则驱动,这些规则定义在config/alarm-settings.yml文件中。 告警规则 告警规则:它们定义了应该如何触发度量警报,应该考虑什么条件。Webhook(网络钩子):定义当警告触发时,哪些服务终端需要被告知。常用告警规则 …

[SpringBoot] 苍穹外卖--面试题总结--上

前言 1--苍穹外卖-SpringBoot项目介绍及环境搭建 详解-CSDN博客 2--苍穹外卖-SpringBoot项目中员工管理 详解&#xff08;一&#xff09;-CSDN博客 3--苍穹外卖-SpringBoot项目中员工管理 详解&#xff08;二&#xff09;-CSDN博客 4--苍穹外码-SpringBoot项目中分类管理 详…