OMG OMG OMG OMG!!!自闭了,又遇到了一个很烦很烦很烦类型的题目
可以先去看文章 二叉树--纲领篇,熟悉二叉树的核心要义!前文介绍过树相关题目所用到的方法无非两种「分解子问题」&&「遍历」,详情可见 二叉树「遍历」And「分解」
同时也可以去看看本人总结的一篇关于二叉搜索树经典题目的文章,里面详细的分析了用递归解决问题时需要明确的要素,详情可见 二叉搜索树(BST)
接下来先梳理一下路径相关题目的形式
详情可见题目 路径总和
这个题目很简单,没什么好说的,直接看代码
// 分解子问题
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) return false;
if (root.left == null && root.right == null && root.val == targetSum) return true;
return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
}
详情可见题目 路径总和 II
这个题目有点意思,「分解子问题 && 遍历」都可以写出来,只不过两个方法的麻烦程度简直不能比!!!
本题和这个里面的「二叉树所有路径」很像,可以参考 传送门
x// 遍历
// 一般遍历都需要借助全局变量
private List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
traversal(root, targetSum, new ArrayList<Integer>());
return res;
}
private void traversal(TreeNode root, int targetSum, List<Integer> list) {
if (root == null) return ;
list.add(root.val);
if (root.left == null && root.right == null && root.val == targetSum) {
res.add(new ArrayList<>(list));
}
traversal(root.left, targetSum - root.val, list);
traversal(root.right, targetSum - root.val, list);
list.remove(list.size() - 1);
}
// 分解子问题
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
List<Integer> list = new ArrayList<>();
list.add(root.val);
if (root.left == null && root.right == null && root.val == targetSum) {
res.add(new ArrayList<>(list));
return res;
}
List<List<Integer>> leftRes = pathSum(root.left, targetSum - root.val);
List<List<Integer>> rightRes = pathSum(root.right, targetSum - root.val);
if (leftRes.size() != 0) {
for (List<Integer> left : leftRes) {
List<Integer> cur = new ArrayList<>(list);
cur.addAll(left);
res.add(cur);
}
}
if (rightRes.size() != 0) {
for (List<Integer> right : rightRes) {
List<Integer> cur = new ArrayList<>(list);
cur.addAll(right);
res.add(cur);
}
}
return res;
}
详情可见题目 路径总和 III
这个题目需要用到「前缀和」的思想,详情可见 前缀和技巧
xxxxxxxxxx
private Map<Integer, Integer> preSumCount = new HashMap<>();
private int pathSum;
private int targetSum;
private int res;
public int pathSum(TreeNode root, int targetSum) {
if (root == null) return 0;
this.pathSum = 0;
this.targetSum = targetSum;
this.res = 0;
// base case
preSumCount.put(0, 1);
traversal(root);
return res;
}
private void traversal(TreeNode root) {
if (root == null) return ;
// 先序位置:首次进入时执行
pathSum += root.val;
res += preSumCount.getOrDefault(pathSum - targetSum, 0);
preSumCount.put(pathSum, preSumCount.getOrDefault(pathSum, 0) + 1);
traversal(root.left);
traversal(root.right);
// 后序位置:即将离开时的执行
preSumCount.put(pathSum, preSumCount.get(pathSum) - 1);
pathSum -= root.val;
}
详情可见题目 从叶结点开始的最小字符串
其实这个题目就是遍历出所有路径,然后比较路径的大小
只不过比较的时候有点麻烦,涉及到了字符串的比较
xxxxxxxxxx
// 遍历
// 借助全局遍历
// 存放结果
private String res = new String();
// 中间路径
private StringBuffer sb = new StringBuffer();
public String smallestFromLeaf(TreeNode root) {
traversal(root);
return res;
}
private void traversal(TreeNode root) {
if (root == null) return ;
// 进入时加入当前元素
sb.append((char) (root.val + 'a'));
// 判断是否为叶子节点
if (root.left == null && root.right == null) {
// 比较过程
String s = new StringBuffer(sb).reverse().toString();
if ("".equals(res)) res = s;
res = s.compareTo(res) <= 0 ? s : res;
}
traversal(root.left);
traversal(root.right);
// 离开时移除当前元素
sb.setLength(sb.length() - 1);
}
详情可见题目 二叉树的直径
思路:分解子问题,当前节点的直径和 = 左子树的高度 + 右子树的高度
技巧:利用后序遍历,刚好可以一边求高度,一边取最值;不然就需要一次次的重复求好多次高度
⚠️ 区分清楚 高度 && 深度
xxxxxxxxxx
private int res = 0;
public int diameterOfBinaryTree(TreeNode root) {
diameterOfBinaryTreeHelper(root);
return res;
}
private int diameterOfBinaryTreeHelper(TreeNode root) {
if (root == null) return 0;
int lh = diameterOfBinaryTreeHelper(root.left);
int rh = diameterOfBinaryTreeHelper(root.right);
// 后序遍历处理阶段
// 更新最值
res = Math.max(res, lh + rh);
// 返回当前子树高度
return Math.max(lh, rh) + 1;
}
详情可见题目 二叉树中的最大路径和
这个题目难度是 hard
,看起来吓一跳,但是本人一开始瞎摸索,居然写出来了,但是没有其他人的解法妙
xxxxxxxxxx
// 本人解法 😭
private int res = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxPathSumHelper(root);
return res;
}
private int maxPathSumHelper(TreeNode root) {
if (root == null) return 0;
int leftMax = maxPathSumHelper(root.left);
int rightMax = maxPathSumHelper(root.right);
// 主要利用下面四行代码进行了负值的舍弃
// 分别和加上左右,只加左或右,只有根 进行最大值取舍
// 其实效果和下面与 0 进行比较一样
res = Math.max(res, leftMax + rightMax + root.val);
res = Math.max(res, leftMax + root.val);
res = Math.max(res, rightMax + root.val);
res = Math.max(res, root.val);
// 返回值也只要和根单独进行一个比较
return Math.max(Math.max(leftMax, rightMax) + root.val, root.val);
}
// 优质解法
private int res = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxPathSumHelper(root);
return res;
}
private int maxPathSumHelper(TreeNode root) {
if (root == null) return 0;
// 核心地方:和 0 做一个取舍
// 如果是负值,则舍弃
// 这样就可以不需要从叶子节点开始到叶子节点结束
int leftMax = Math.max(0, maxPathSumHelper(root.left));
int rightMax = Math.max(0, maxPathSumHelper(root.right));
res = Math.max(res, leftMax + rightMax + root.val);
return Math.max(leftMax, rightMax) + root.val;
}
突然看到了一篇面经,有的面试官需要让你输出最长和的路径,下面来实现一下:
xxxxxxxxxx
class Solution {
// 存储结果
// 一开始路径和需要初始为最小值
private Pair ans = new Pair(Integer.MIN_VALUE, new ArrayList<>());
public int maxPathSum(TreeNode root) {
maxPathSumHelper(root);
return ans.sum;
}
private Pair maxPathSumHelper(TreeNode root) {
Pair res = new Pair(0, new ArrayList<>());
if (root == null) return res;
Pair leftRes = maxPathSumHelper(root.left);
Pair rightRes = maxPathSumHelper(root.right);
// 舍去路径和为负数的孩子
if (leftRes.sum <= 0) leftRes = new Pair(0, new ArrayList<>());
if (rightRes.sum <= 0) rightRes = new Pair(0, new ArrayList<>());
// 记录当前路径和,即:左孩子 根 右孩子
int curSum = root.val + leftRes.sum + rightRes.sum;
// 更新最长路径和
if (ans.sum < curSum) {
List<Integer> curPath = new ArrayList<>();
curPath.addAll(leftRes.path);
curPath.add(root.val);
curPath.addAll(rightRes.path);
ans = new Pair(curSum, curPath);
}
// 处理返回值
// 从左右孩子中选择一条更长的路
if (leftRes.sum >= rightRes.sum) {
res.path.addAll(leftRes.path);
res.path.add(root.val);
res.sum = root.val + leftRes.sum;
} else {
res.path.add(root.val);
res.path.addAll(rightRes.path);
res.sum = root.val + rightRes.sum;
}
return res;
}
}
class Pair {
// 存储路径和
int sum;
// 存储路径
List<Integer> path;
public Pair(int sum, List<Integer> path) {
this.sum = sum;
this.path = path;
}
}
详情可见题目 最长同值路径
当时一瞅,不知道何从下手;直到看到了一句提醒,有了启发 返回和当前节点值相同的最大高度
说白了就是搞清楚递归三要素中返回值的含义
xxxxxxxxxx
// 记录最终结果
private int res = 0;
public int longestUnivaluePath(TreeNode root) {
longestUnivaluePathHelper(root);
return res;
}
private int longestUnivaluePathHelper(TreeNode root) {
if (root == null) return 0;
int left = longestUnivaluePathHelper(root.left);
int right = longestUnivaluePathHelper(root.right);
// 后序遍历处理阶段
// maxLen:记录当前路径长度
int maxLen = 0;
// maxSameDepth:记录当前最大高度
int maxSameDepth = 0;
// 如果左孩子不为空,且值相同
if (root.left != null && root.val == root.left.val) {
// maxLen 加上左孩子路径
maxLen += (left + 1);
// 更新最大高度
maxSameDepth = Math.max(maxSameDepth, left + 1);
}
if (root.right != null && root.val == root.right.val) {
// maxLen 加上右孩子路径
maxLen += (right + 1);
// 更新最大高度
maxSameDepth = Math.max(maxSameDepth, right + 1);
}
// 更新最终结果
res = Math.max(res, maxLen);
return maxSameDepth;
}
是不是感觉一下把正确代码给出,看似太简单了
直接上几个本人自闭时候的错误版本(核心代码部分)
xxxxxxxxxx
// 错误版本 1
int maxLen = 0;
if (root.left != null && root.val == root.left.val) maxLen += (left + 1);
if (root.right != null && root.val == root.right.val) maxLen += (right + 1);
res = Math.max(res, maxLen);
// 错误点:直接返回了路径的长度,会导致结果 大于 正确结果
// 正确思路:返回和当前节点值相同的最大高度
return curRes;
// --------------- 分隔线 ---------------
// 错误版本 2
// 后序遍历处理阶段
int maxLen = 0;
if (root.left != null && root.val == root.left.val) maxLen += (left + 1);
if (root.right != null && root.val == root.right.val) maxLen += (right + 1);
res = Math.max(res, maxLen);
// 错误点:没有考虑值相同这个因素,值不同的时候,也会返回 +1 的结果,会导致错误
// 正确思路:只有当值相同的时候,才返回;值不同的时候应该返回 0
return Math.max(left, right) + 1;
详情可见题目 二叉树中的最长交错路径
这个题目很有意思,本人一开始想到的就是分解子问题,结果把自己绕来绕去 绕晕了 😭😭😭
下面是分解的解法,可惜超时 😭😭😭
不过可以体会一下这种思想,即:最优解可能不在当前节点
xxxxxxxxxx
private int res = 0;
public int longestZigZag(TreeNode root) {
helper(root);
return res;
}
// 对于当前节点,最优解可能出现在该节点向左孩子进行搜索,也可能出现在该节点向右孩子进行搜索
// 同时,也可能最优解不在当前节点,而在左右节点中
private void helper(TreeNode root) {
if (root == null) return ;
// 对于最优解就在当前节点上时
res = Math.max(res, Math.max(searchZigZag(root, 0), searchZigZag(root, 1)));
// 如果不在,则继续搜索左右节点
helper(root.left);
helper(root.right);
}
// 给定方向,进行搜索
private int searchZigZag(TreeNode root, int dir) {
if (root == null) return -1;
if (dir == 0) return searchZigZag(root.left, 1) + 1;
else return searchZigZag(root.right, 0) + 1;
}
下面是进行了优化的解法,该解法很巧妙。利用一个数组,数组的第一个数表示当前节点向左搜索的结果,数组的第二个数表示当前节点向右搜索的结果
xxxxxxxxxx
private int res = 0;
public int longestZigZag(TreeNode root) {
getPathSum(root);
return res;
}
private int[] getPathSum(TreeNode root) {
// 如果为空 -1 (此处也很妙)
// 如果是叶子节点,则可以 +1 变成 0
// 如果是两个节点,则继续 +1 变成 1
// 刚好满足节点数量和边之间的关系 妙呀~~~
if (root == null) return new int[]{-1, -1};
int[] left = getPathSum(root.left);
int[] right = getPathSum(root.right);
// 后序遍历时处理 (发现很多题目都借助了后序遍历的特点)
// 更新值,注意逻辑
int leftPath = left[1] + 1;
int rightPath = right[0] + 1;
res = Math.max(res, Math.max(leftPath, rightPath));
return new int[]{leftPath, rightPath};
}
详情可见题目 二叉树中的列表
这个题目也是变相的一种寻找路径,判断是否存在符合的路径
这个题目和上面的题目很像,如果利用「分解子问题」的思想去写更像
正确解可能是当前节点,也可能是孩子节点开始
xxxxxxxxxx
public boolean isSubPath(ListNode head, TreeNode root) {
if (head == null) return true;
if (root == null) return false;
// 三者满足其一就 🉑️
return isSub(head, root) || isSubPath(head, root.left) || isSubPath(head, root.right);
}
private boolean isSub(ListNode head, TreeNode root) {
if (head == null) return true;
if (root == null) return false;
if (root.val != head.val) return false;
return isSub(head.next, root.left) || isSub(head.next, root.right);
}