查看原文
其他

LeetCode 例题精讲 | 02 Path Sum:二叉树的子问题划分

nettee 面向大象编程 2022-06-20

本期例题:LeetCode 112 - Path Sum[1](Easy)

给定一个二叉树和一个目标和,判断该树中是否存在根结点到叶结点的路径,这条路径上所有结点值相加等于目标和。返回 true 或者 false。

示例:

给定如下二叉树,以及目标和 sum = 22。以下二叉树存在路径 5->4->11->2 满足目标和。

题目示例

链表和二叉树是大家面试中最常遇到的两种数据结构。上一期我们讲了链表的遍历框架,这一期我们接着讲二叉树的遍历框架。二叉树的题目比链表更常见、变种更多,但是掌握了基本思想则一点不难。

说到二叉树的遍历框架,很多人的脑海里立马蹦出来的就是前序、中序和后序遍历。但是机械地记住前中后序遍历没有太大意义,我们首先要掌握的是二叉树的递归思想。究竟怎样递归才能快速地解决二叉树问题呢?

这篇文章将会包含:

  • 二叉树的递归结构
  • 二叉树问题的子问题划分
  • 本期例题:Path Sum 的解法
  • 相关题目

二叉树与递归

如何定义二叉树?二叉树是每个结点最多只有两个分支的树。这是一个正确的定义,但对解决问题没有帮助。我们需要的是二叉树的递归定义:

  • 空树是一个二叉树
  • 如果  和  是二叉树,那么用一个根结点连接  和  得到的也是二叉树

可以看到,二叉树天生就是递归的。遍历一个二叉树,先处理根结点,左右两个子树又是二叉树,可以递归处理。这便是递归前序遍历的原理。

递归处理二叉树

我们很早就学会了递归函数,觉得它太简单,经常会忘记它的意义。递归本质上是将问题分解成同类的子问题,反复调用自己来进行求解。你可能更熟悉动态规划里的子问题,但实际上任何有递归函数的地方都有子问题。

许多二叉树问题都可以通过划分子问题来求解。如果我们思考出了子问题的划分方式,那么使用何种方式进行递归遍历,都是能很容易就推导出的。

下面,我们就用今天的例题 Path Sum (路径和)来学习子问题的划分方式。

Path Sum 的子问题划分

我们都知道,递归有两大要点:

  • 反复调用自身
  • 终止条件

而在二叉树结构上进行递归,则这两大要点变为:

  • 递归调用自己两个子树
  • 在叶结点处终止递归

其中,调用子树的部分是重点。我们需要保证在子树上求解的是与原问题相同的子问题,才能递归调用自身。而终止条件可以放在最后作为细节考虑。

再看这道 Path Sum 问题,我们的目标是找到从根到叶的路径,和为 22。如果左右某个子树中存在一个从根到叶的路径和为 17,再连上根结点的 5,正好是 17 + 5 = 22。我们把整个的 sum = 22 的问题变成了两个子树上的 sum = 17 问题,就可以对自身递归调用。

Path Sum 问题有多个变种,难度各有不同。本例题中只关注从根到叶的路径,是最简单的一种情况,直接使用最直观的递归方式即可。

整棵树上的 path sum 问题
子树上的 path sum 问题

根据以上的基本思路,我们可以得到以下的递归代码:

boolean hasPathSum(TreeNode root, int sum) {
int target = sum - root.val;
return hasPathSum(root.left, target)
|| hasPathSum(root.right, target);
}

这里的代码显然不完整,我们还要考虑递归终止情况的处理,以及一些边界情况。下面会讨论关于空指针、叶结点的几个细节问题。

递归解法的细节问题

细节 1:root == null 表示什么

void processTree(TreeNode root) {
if (root == null) {
// 空树
}
}

在二叉树中, root 为 null 表示空树。但这里的空树有两种含义:

第一个含义是整棵树都为空。二叉树题目一般都需要考虑这种情况,否则面试官会认为你考虑边界情况不周全。

第二个含义是某个子树为空。由于函数是递归调用的,参数 root 可以表示任何一个子树。特别的,叶结点的两个子树都为空,递归到这里就会自然遇到两个 root == null 的情况(如下图的 3、4)。一般情况下,我们用 root == null 作为递归的终止条件。

在 root == null 时终止递归

在例题 Path Sum 中,我们可以在 root == null 时判断目标和是否已经满足,即 sum 减为 0:

boolean hasPathSum(TreeNode root, int sum) {
if (root == null) {
return sum == 0;
}
int target = sum - root.val;
return hasPathSum(root.left, target)
|| hasPathSum(root.right, target);
}

细节 2:是否要判断叶结点

然而,上面的代码在 LeetCode 中提交会得到 Wrong Answer。当树为空树、sum = 0 时,我们的代码会错误地返回 true!

错误的原因是我们的递归终止条件写得太想当然了。符合条件的路径会把 sum 减为 0,但 sum 为 0 不代表找到了符合条件的路径。

再回顾题目描述:

给定一个二叉树和一个目标和,判断该树中是否存在根结点到叶结点的路径,这条路径上所有结点值相加等于目标和。

我们应当在叶结点处进行判断并结束递归,而不是在叶结点的两个空子树处判断。

boolean hasPathSum(TreeNode root, int sum) {
if (root == null) {
return false;
}
if (root.left == null && root.right == null) {
return root.val == sum;
}
int target = sum - root.val;
return hasPathSum(root.left, target)
|| hasPathSum(root.right, target);
}

一个小经验是:凡是题目描述里提到叶结点的,都需要显式判断叶结点,在叶结点处结束递归。

在叶结点处终止递归

总结

大部分的二叉树问题都是用递归来解决的。我们解决二叉树类问题时,应遵循的步骤是:

  1. 判断问题能否划分问子问题,应当划分为什么样的子问题
  2. 判断使用前序遍历还是后序遍历
  3. 检查空指针、叶结点等细节

以下是相关题目,这里只列出和本文例题紧密相关的一些题目:

  • 简单划分子问题的递归方法:
    • 100 - Same Tree[2]
    • 101 - Symmetric Tree[3]
  • 需要考虑叶结点的题目:
    • 111 - Minimum Depth of Binary Tree[4]
    • 129 - Sum Root to Leaf Numbers[5]
    • 257 - Binary Tree Paths[6]

二叉树是一个有很多套路和技巧的题目类型。这里讨论的只是其中最简单的一类题目,后续还会有更多的关于二叉树类题目的讲解,包括在遍历中使用全局变量、迭代式遍历等,敬请期待。

参考资料

[1]

LeetCode 112 - Path Sum: https://leetcode.com/problems/path-sum/

[2]

100 - Same Tree: https://leetcode.com/problems/same-tree

[3]

101 - Symmetric Tree: https://leetcode.com/problems/symmetric-tree

[4]

111 - Minimum Depth of Binary Tree: https://leetcode.com/problems/minimum-depth-of-binary-tree

[5]

129 - Sum Root to Leaf Numbers: https://leetcode.com/problems/sum-root-to-leaf-numbers/

[6]

257 - Binary Tree Paths: https://leetcode.com/problems/binary-tree-paths/


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存