一刷完了,原来这才是二叉树的真面貌...... —— 二叉树总结篇

213 阅读12分钟

不知不觉二叉树已经刷了九天二叉树系列,刷二叉树的力扣算法题将近两个星期,做了30+二叉树经典题目,其中知识点较多,因此打算来一次汇总总结,梳理之前学过的东西~
回忆每一道二叉树的题目中,都使用了递归三部曲来分析题目,相信大家以后看到二叉树,看到递归,都会想:返回值、参数是什么?终止条件是什么?单层逻辑是什么?

于是在刷完这30+的二叉树题目后,根据递归三部曲,加上自己想法,

在文末总结出以下容易困惑我的点,可在右方目录跳转

另外几乎每一道题目都有对应的迭代法,可以用来进一步提高自己。

下面把做过的30+题目系统地分门别类,循序渐进学习与复习二叉树,最好的情况是:看到一个标题,能回想一下对应的解题思路,如果忘了就赶紧回去看当时的笔记。这样很快就可以系统性的复习一遍二叉树了。

二叉树的理论基础

二叉树的遍历方式

求二叉树的属性

(迭代法为拔高,可二刷再思考)
书写递归法的单层递归逻辑时,最好以最简单的情况思考

  • 101. 对称二叉树
    • 递归:后序(跨树后序),比较的是根节点的左子树与右子树的内侧与外侧是否相等(即是否相互翻转)
    • 迭代:使用队列/栈将两个节点顺序放入容器中进行比较
  • 104. 二叉树的最大深度
    • 递归:后序,从下到上,求根节点最大高度就是最大深度,通过递归函数的返回值做计算树的高度
    • 迭代:层序遍历
  • 111. 二叉树的最小深度
    • 递归:后序,从下往上,求根节点最小高度就是最小深度,注意最小深度的定义
    • 迭代:层序遍历
  • 222 完全二叉树的节点个数
    • 递归:后序,通过完全二叉树的特性以及递归函数的返回值计算节点数量
    • 迭代:层序遍历
  • 110. 平衡二叉树
    • 递归:后序,注意后序求高度和前序求深度,递归过程判断高度差
    • 迭代:效率很低,不推荐
  • 257 二叉树的所有路径
    • 递归:前序,方便让父节点指向子节点,涉及回溯处理根节点到叶子的所有路径
    • 迭代:一个栈模拟递归,一个栈来存放对应的遍历路径
  • 404 左叶子之和
    • 递归:后序,必须借助父节点,三层约束条件,才能判断是否是左叶子。
    • 迭代:直接模拟后序遍历
  • 513 找树左下角的值
    • 递归:顺序无所谓,优先左孩子搜索,再去右孩子搜索(妙处:同深度避免右孩子赋值)同时找深度最大的叶子节点。
    • 迭代:层序遍历找最后一行最左边
  • 112 路径总和
    • 递归:顺序无所谓,这里注意重要知识点:递归函数什么时候需要返回值?什么时候不需要返回值?

递归函数返回值为bool类型是为了搜索一条边,没有返回值是搜索整棵树。

  • 迭代:栈里元素不仅要记录节点指针,还要记录从头结点到该节点的路径数值总和

二叉树的修改与构造

  • 226 翻转二叉树
    • 递归:前后序都行,交换左右孩子
    • 迭代:直接模拟前序遍历
  • 106 从中序与后序遍历序列构造二叉树
    • 递归:前序,重点在于根据中后序特点找分割点,分左右区间构造
    • 迭代:比较复杂,意义不大
  • 654 最大二叉树
    • 递归:前序,分割点为数组最大值,分左右区间构造
    • 迭代:比较复杂,意义不大
  • 617 合并二叉树
    • 递归:前序(中左右) - 符合常规思维,同时操作两个树的节点,注意合并的规则
    • 迭代:使用队列,类似层序遍历

求二叉搜索树的属性

中序遍历二叉搜索树为递增序列

二叉树公共祖先问题

二叉搜索树的修改与构造

(删除节点要比插入难,因为插入只需插入到叶节点,而删除节点可能删除中间节点,这就需要考虑具体情况 - 5种。)

✍最后总结

除了通用方法论,还有以下需要思考的细节:

1. 递归三部曲的优化

二叉树难点就难在递归上,经常搞的云里雾里,以下是结合资料总结的方法:

  • 1. 递归方法返回值以及参数:这里需要注意的是,不是所有参与遍历的参数都要在方法中传参,只有是方法所需参数才需要传参。需要分清楚:
    • 全局变量:在遍历中仅使用与记录。如:ArrayList result等等只用于记录数据
    • 方法参数:体现回溯思想。即在全局变量的基础上,在进行下一次遍历时需要改变值,而结束遍历后又需要原来的值。如:traversal(root.left),组合问题时在循环中xxx(i + 1)
  • 2. 终止条件:用手笔纸图画模拟如何运转,思考什么时候终止。例如二叉树最常见的终止条件就是遇到叶节点。我们可以以最简单的例子入手,如下图左,

思考:当只有一个节点时应该返回什么值对递归方法返回值恰当?

  • 3. 单层递归逻辑:同样以最简单的例子入手,如下图右。举一个符合题目的二叉树,再举一棵不符合题目的数。先思考这颗二叉树应该怎么运转(前中后序遍历 or 其他),然后再想如何使用代码实现。


写出代码后再套入到题目给出的二叉树中,看是否忽略某些条件,以完善代码(例如在 98 验证二叉搜索树时,如果只局限在简单的二叉树很可能判断条件是中间节点大于左节点小于右节点,而正确的应该是中间节点大于左子树所有值小于右子树所有值

以上方法帮助了我从二叉树云里雾里的递归中拯救出来,希望对看到这的你有启发。

2. 返回值 - 递归函数什么时候需要返回值?什么时候不需要返回值?

这里总结如下三点:

  • 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
  • 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236 二叉树的最近公共祖先中)
  • 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)

第一个点很好理解,不用处理递归返回值就不设置返回值。但第二点和第三点又如何理解呢?都需要返回值,一个是立即返回一个是满足条件返回,其实就是对应接下来问题。

3. 单层递归逻辑 - 遍历时仅遍历左节点还是遍历整棵左树?

如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树呢? 搜索一条边的写法:

if (递归函数(root.left)) return ;

if (递归函数(root.right)) return ;

搜索整个树写法:

left = 递归函数(root.left);  // 左
right = 递归函数(root.right); // 右
left与right的逻辑处理;         // 中

⭕在递归函数有返回值的情况下:

  • 如果要搜索一条边,递归函数返回值不为空的时候,立刻返回。
  • 如果搜索整个树,直接用一个变量left、right接住返回值,因为这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑

(简单来说,如果一条边满足条件则马上返回。而需要结合left 和 right 返回结果进行进一步返回判断的则是需要搜索整棵书的)。
例如在 236 二叉树的最近公共祖先为什么要遍历整棵树呢?直观上来看,找到最近公共祖先,直接一路返回就可以了。 如图: 就像图中一样直接返回7,多美滋滋。 但事实上还要遍历根节点右子树(即使此时已经找到了目标节点了),也就是图中的节点4、15、20。 因为在如下代码的后序遍历中,如果想利用left和right做逻辑处理, 不能立刻返回,而是要等left与right逻辑处理完之后才能返回

left = 递归函数(root.left);  // 左
right = 递归函数(root.right); // 右
left与right的逻辑处理;         // 中

所以此时要知道我们要遍历整棵树。知道这一点,对 236 二叉树的最近公共祖先 就有一定深度的理解了。

进一步理解:既然还会访问 4、15、20等节点,那么这个算法还具有扩展性,还可以处理除了p q外多个节点的公共祖先

附一个完整流程图:
0316e9d0e8bc4f8496c59bb828620171~tplv-k3u1fbpfcp-zoom-in-crop-mark 1512 0 0 0.webp

4. 遍历顺序问题

在二叉树题目选择什么遍历顺序是不少同学头疼的事情,做了这么多二叉树的题目了,给大家大体分分类

  • 涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点。
  • 求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。
  • 出现二叉搜索树,根据其特性,大概率就是中序了,要不白瞎了有序性了。

注意在普通二叉树的属性中,一般为后序,例如单纯求深度就用前序,二叉树:找所有路径
也用了前序,这是为了方便让父节点指向子节点。
所以求普通二叉树的属性还是要具体问题具体分析。


最后,二叉树系列一刷就到此结束了,花了几个周末才把总结篇写完。二叉树这部分肯定要认真二刷了,因为二叉树题目最多而且个人对其遍历总是云里雾里,因此花了时间慢慢想慢慢磨,总结以上方法,希望二刷时候更加从容。

学习资料:

二叉树总结篇