算法通过村第七关-树(递归/二叉树遍历)青铜笔记|手撕递归

news2024/11/26 2:40:50

文章目录

  • 前言
  • 1. 递归的特征
  • 2. 如何写出好的递归
  • 3. 怎么看懂递归的代码
  • 总结


前言


提示:我们生活在24小时不眠不休的社会里但是没有24小时不眠不休的身体有些东西必须舍弃 -- 马特·海格

这一关,我看要谈论的是递归问题,说到它就牵扯到很多问题了

  1. 与树和二叉树的相关问题
  2. 二分查找相关问题
  3. 快速排序和并轨排序问题
  4. 回溯问题
  5. 动态规划问题

这一切都是递归算法为基础的,当然这一关也是必须掌握的。

1. 递归的特征

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

比如这个故事:

从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事······
在这里插入图片描述


如果看递归的结构,就像下面的这个样子,前面的一层取一摸一样的调用下一层,不同的只是输入和输出参数,这个递归过程是写递归的一个核心问题。
在这里插入图片描述
当然这个过程不能一致持续下去,一定要在满足某个要求之后返回结果,否则的话就会抛出我们常见的“StackOverFlow”的问题。

所有的递归有两个必要的特征:

  1. 执行时范围不断缩小,这样才能触底反弹
  2. 种植判断在调用递归的前面

理解这个,是我们掌握递归的辅助工具,我们一一分析:

  • 执行范围不断缩小

递归在数学里的概念是递推,设计递归就是努力寻找数学里的递推公式。经典的问题就是求阶乘,好多同学也是通过这个认识递归的吧,那时我们第一次见面。 还记得递推公式吗? f(n) = f(n-1) * n,很显然一定是触底之后才能反弹的,再比如就是典型的斐波那契数列递推公式了,f(n) = f(n -1) + f(n - 2);当然n也是在不断的缩小。这条规律可以辅助我们检查自己写的递推公式对不对。

再比如后面我们会遇到青蛙跳台阶问题,他的递推公式f(n) = f(n -1) + f(n - 2);你想奇怪呀?怎么会一样呢,没错,他就一样🤣。

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

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

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

  • 终止条件判断在递归调用的前面

递归之后可能还有终止条件,但是在执行递归之前,一定会有一个终止条件。这个条件可以帮助我么你检查自己写的算法对不对。

为什么呢?我们看个例子🌰:

很显然recursion()会不断的调用自己,这样一直递归下去,就无法退出来,知道抛出堆栈溢出异常(StackOverFlowError)。

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

所以:任何递归方法在执行之前,一定会有一个终止条件

实际上一个方法里的递归调用可能不止一次,还会加一些逻辑处理,比如下面这样,但是终止的条件仍然在前面。

public void recursion(参数0) {
    if(终止条件) {
        return}
        // 逻辑一
        recursion(参数1)// 逻辑二
        recursion(参数2)// ......
        recursion(参数n);
        可能还有其他一些逻辑运算
}

这一特点启示我们,可以先考虑在什么条件下终止,而相关代码要写在靠前的位置,之后再考虑递归的逻辑,这样可以很好的降低编写代码的难度。

2. 如何写出好的递归

明白了上面的道理,那么你就右疑问了,怎么写出好的递归方法呢?

  • 从小到大递推

递归该怎么写?递归源于数学里的归纳法,这个在高中数学里面有的。大致呢?你先猜出存在的递归关系,f(n) = pf(n-1),然后你只要证明当n增加1时,f(n + 1) = pf(n)也是成立的就说明你的猜想没有问题。不过在算法里面,我们写递归一般不需要证明,我么你先挑选几个较小的值验证一下,然后再选择几个较大的值验证一下就可以了。

很显然大部分从n = 1,2,3或者只有一两个元素开始写最简单。典型的就是斐波那契数列为 1 1 2 3 5 8 …,从 n = 3开始都满足f(n) = f(n - 1) + f(n -2),然后我们再选择某个比较大的n来验证就可以了。

所以-这就是我们要找的递推公式:

f(n) = f(n - 1) + f(n -2)

对于阶乘来说也一样

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)。

  • 分情况讨论,明确结束条件

我们说过递归里面的终止条件一定时靠前的,而大部分递归的终止条件不是n最小开始出低反弹时有几种情况:

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

// 算 n 的阶乘(假设n != 0)
int f(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 也给限制住,所以结果就变成如下这样:

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

有些情况不一定触底才开始反弹,而是达到某种要求就要停止,这样需要考虑的情况会比较多。解决这类问题最直接的方法就是枚举,可以将所有情况列举出来,然后再一一去优化。

只有列举清楚了才可能将终止条件写完整,所以在面试的情况下,不要上来就写,而是和面试官讨论你的设计方案,不要害怕和面试官讨论,假如有很明显的缺陷他也甚至提醒你一下,所以这也是一个借力打力的一个技巧。

确定终止条件队递归至关重要,后面很多题目会话很大的篇幅来分析怎么判断终止条件,一旦判断完毕,递推关系就是水到渠成了。

  • 组合成完整的方法

将递推公式和终止条件组合起来,变成完整的方法。

递归经常能看出很多骚操作代码,不要迷信呀,分情况逐个先写出来,之后再看看能否精简优化,不要一个步子迈得太大,容易出事故。我们还是那上面的例子举例,完善我们的代码:

// 算 n 的阶乘(假设n != 0)
int factorial(n) {
	if(n == 1){
        return 1;
	}
    rerurn n * factorial(n - 1);
}
// 斐波那契数列
int factorial(n){
	// 1. 先写递归结束条件
	if(n <= 2){
        return 1;
	}
	// 2. 接着写等价关系式
   return factorial(n - 1) + factorial(n - 2);
}

每次写递归,都要多方面考虑,后面我们写的时候要注意。

入参和出参??埋坑

3. 怎么看懂递归的代码

对很多人来说,递归最大的问题在于给了答案,看不懂代码,而递归的代码也贼难调试,其实一个转换一下思路就很好解决了。

我们先思考一个问题,上面的阶乘,如果n == 4 ,调用了几次上面的f()方法呢?很显然是4次。这个就是递归的一个特征【不撞南墙不回头】,n == 4,3,2是会继续递归,知道有1是满足条件退出,然后就return 1,不再递归了,而且是不断返回上一层并计算。

接着再看返回时每层参数的问题,递归本质上仍然是方法调用,所以可以按照方法调用的方式来检验写的对不对。

下面是一个完整的思路,你会发现递归不归是一个方法被调用了好几次,而每次n都在减小,这就是递归的过程,触底之后,也就是满足终止条件后就开始返回了。

递归的时候当前层的n被系统给保存,而返回的时候会自动设置会来,一次每层n是不一样的,所以就是重新拿到当前这一层n的值完成计算即可。

f(4)阶乘的过程如下:

在这里插入图片描述


总结

提示:递归思路,递归的图解,怎么写好递归

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

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

相关文章

QT基础教学(QT对象间的关系)

文章目录 前言一、QT间的父子关系二、聚合关系三、组合关系四、关联关系五、依赖关系总结 前言 本篇文章我们来讲解QT对象间的关系&#xff0c;理清楚QT对象间的关系是非常重要的&#xff0c;没有理清楚QT间的对象关系可能会导致内存的泄漏等各种问题的出现。 资料合集地微信…

HarmonyOS开发:那些开发中常见的问题汇总(一)

前言 本来这篇文章需要讲述静态共享包如何实现远程依赖和上传以及关于静态共享包私服的搭建&#xff0c;非常遗憾的告诉大家&#xff0c;由于组织管理申请迟迟未通过&#xff0c;和部分文档官方权限暂未开放&#xff0c;关于这方面的讲解需要延后了&#xff0c;大概需要等到202…

HeyFriday AI:智能AI写作工具

【产品介绍】​ 名称 HeyFriday AI 具体描述​ HeyFriday是一款智能AI写作工具&#xff0c;可以帮助用户快速生成博客、广告、创意故事等各种类型的文本内容&#xff0c;节省时间和金钱。​HeyFriday的团队由前谷歌NLP科学家&#xff08;ALBERT的第一作者&#xff…

VIRTIO-BLK代码分析(5)virtqueue通信

virtqueue用于VIRTIO驱动和VIRTIO设备进行通信。每个VIRTIO设备可能有一个或多个virtqueue&#xff0c;VIRTIO-BLK的virtqueue数目可以通过num_queues设置。 通过函数vring_create_virtqueue创建virtqueue&#xff0c;通过函数virtqueue_add_sgs添加数据。目前存在split virtqu…

【网络编程】UDP Socket编程

UDP Socket编程 一. DatagramSocket二. DatagramPacket三. InetSocketAddress四. 执行流程五. 代码示例: UDP 回显服务器 数据报套接字&#xff1a; 使用传输层 UDP 协议 UDP: 即 User Datagram Protocol&#xff08;用户数据报协议&#xff09;&#xff0c;传输层协议。 UDP…

Jmeter系列-定时器Timers的基本介绍(11)

简介 JMeter中的定时器&#xff08;Timer&#xff09;是一种重要的元件&#xff0c;用于模拟用户在不同时间间隔内发送请求的场景。通过使用定时器&#xff0c;可以模拟负载、并发和容量等不同情况下的请求发送频率。 使用定时器 可以在取样器下添加定时器&#xff0c;这样定…

机器学习第五课--广告点击率预测项目以及特征选择的介绍

这个项目的主要的目的是通过给定的广告信息和用户信息来预测一个广告被点击与否。 如果广告有很大概率被点击就展示广告&#xff0c;如果概率低&#xff0c;就不展示。 因为如果广告没有被点击&#xff0c;对双方&#xff08;广告主、平台&#xff09;来讲都没有好处。所以预测…

应用商店优化之关键词优化指南1

提高我们应用在应用商店中的知名度&#xff0c;完全取决于关键词。找到一些关键词的策略之后&#xff0c;我们应该选择最有前途的关键词并对其进行研究&#xff0c;从而创建新的组合和长尾关键词。 1、寻找关键词&#xff0c;找到一些潜在的关键词创意。 尝试考虑一下我们将如…

Element表格之表头合并、单元格合并

一、合并表头 el-table配置 :header-cell-style"headFirst"headFirst({ row, colunm, rowIndex, columnIndex }) {let base { background-color: rgba(67, 137, 249, 0.3), color: #333, text-align: center };//这里为了是将第一列的表头隐藏&#xff0c;就形成了合…

Python开发环境配置

Python 作为一个语言&#xff0c;代码需要一个解释器来进行解释&#xff0c;这个解析器就是专门针对 Python 代码的&#xff0c;只有它才知道这些代码是什么意思。编写代码也需要一定的工具&#xff0c;理论上所有的纯文本都可以编写代码&#xff0c;但工欲善其事必先利其器&am…

Python 办公自动化之 PDF 操作详解

1、PyMuPDF简介 1. 介绍 在介绍PyMuPDF之前&#xff0c;先来了解一下MuPDF&#xff0c;从命名形式中就可以看出&#xff0c;PyMuPDF是MuPDF的Python接口形式。 MuPDF MuPDF 是一个轻量级的 PDF、XPS和电子书查看器。MuPDF 由软件库、命令行工具和各种平台的查看器组成。 M…

安卓修改rom 移植rom必备常识 lib--**so文件基本解析

安卓系统lib-so文件 解包安卓固件中lib lib64分区下存在很多后缀为so的文件&#xff0c;其实都是系统的动态库&#xff0c;类似于win下面的*.dll&#xff0c;一般是由C/C编译成的动态库.在系统lib的文件夹下&#xff0c;置换移植其他系统的程序也需要修改更换相关so文件.没有它…

Android---底部弹窗之BottomSheetDialog

BottomSheetDialog 是Android开发中的一个弹出式对话框&#xff0c;它从屏幕底部弹出并覆盖部分主界面。 1. BottomSheetDialog的使用 // 参数2&#xff1a;设置BottomSheetDialog的主题样式&#xff1b;将背景设置为transparent&#xff0c;这样我们写的shape_bottom_sheet_…

C【函数】

1.常用API 1.strcpy&#xff1a;#include<string.h> char * strcpy ( char * destination, const char * source );int main(){char arr1[] "bit";char arr2[20] "###########";// bit\0########strcpy(arr2, arr1);printf("…

ChatGPT的未来

随着人工智能的快速发展&#xff0c;ChatGPT作为一种自然语言生成模型&#xff0c;在各个领域都展现出了巨大的潜力。它不仅可以用于日常对话、创意助手和知识查询&#xff0c;还可以应用于教育、医疗、商业等各个领域&#xff0c;为人们带来更多便利和创新。 在教育领域&#…

【刷题篇】贪心算法(二)

文章目录 找出工作所需最短时间活动选择无重叠区间 找出工作所需最短时间 某工厂有n个独立的作业&#xff0c;由m台相同的机器进行加工处理。作业i所需的加工时间为ti&#xff0c;任何作业在被处理时不能中断&#xff0c;也不能进行拆分处理。现厂长请你给他写一个程序:算出n个…

LLM 04-大模型的数据

LLM 03-大模型的数据 到目前为止&#xff0c;我们已经讨论了大型语言模型的行为&#xff08;能力和损害&#xff09;。现在&#xff0c;我们要剥开洋葱的第一层&#xff0c;开始讨论这些模型是如何构建的。任何机器学习方法的起点都是训练数据&#xff0c;因此这就是我们开始的…

JDK10特性

文章目录 JAVA10概述语法层次的变化局部变量的类型推断不能使用类型推断的场景变量的声明初始值nulllambda表达式方法引用为数组静态初始化成员变量不能使用其他不可以的场景 API层次的变化集合的copyOf方法 总结 JAVA10概述 2018年3月21日&#xff0c;Oracle官方宣布JAVA10正…

sizeof和strlen求取数组指针之辨析

目录 一维数组中sizeof Vs strlen 整型数组sizeof 字符数组 sizeof strlen 字符串数组 sizeof strlen 字符串的指针char *p sizeof strlen 二维数组中sizeof 今天主要来讲题目主要是数组&指针辨析题和笔试题。&#x1f197;最近心情有点焦虑。大家一定专注…

进阶测试知识之风险基础测试

风险基础测试&#xff08;Risk-Based Testing&#xff09;是一种测试策略&#xff0c;其主要思想是根据产品或系统中各个部分的风险程度来优先进行测试。风险通常是由两个因素决定的&#xff1a;一是问题&#xff08;如缺陷或错误&#xff09;发生的可能性&#xff0c;二是如果…