前言
记录 LeetCode 刷题中遇到的二叉树相关题目,第二篇
257.二叉树的所有路径
采用分治的思想,当前节点应该返回的路径应该是左子树和右子树所有应该返回的路径前面再加一个当前节点的值
public List<String> binaryTreePaths(TreeNode root) {
//递归分治的边界,通常在树中做递归边界都是空节点
if(root == null) return null;
//当前节点没有左右孩子,也就是叶子节点,那么要返回的路径就是只有自己的路径
if(root.left == null && root.right == null){
List<String> res = new ArrayList<>();
res.add(String.valueOf(root.val));
return res;
}
//分治
List<String> leftStrs = binaryTreePaths(root.left);
List<String> rightStrs = binaryTreePaths(root.right);
List<String> all = new ArrayList<>();
//结合分治得到的两个结果为总的结果
if(leftStrs != null){
for(String s : leftStrs){
all.add(root.val + "->" + s);
}
}
if(rightStrs != null){
for(String s : rightStrs){
all.add(root.val + "->" + s);
}
}
return all;
}
二刷
前面递归分治的做法好像不太好理解 (我可没有说我自己写的回来看的时候一开始都理解不了)
二刷用了下面的做法,用 StringBuilder 变量 sb 维护当前的遍历序列,res 记录所有路径的集合。基于先序遍历,如果当前节点是叶子节点,就把遍历序列 sb 添加到 res 中;
否则的话把当前节点的 val 加到序列当中后继续遍历左右子节点。遍历完左右节点可能 sb 就被改变了,所以需要在遍历之前用一个 tmp 变量记录一下当前的 sb 内容,遍历完左右节点后把 sb 还原到 tmp 的内容。相当于一种回退操作
class Solution {
StringBuilder sb = new StringBuilder();
List<String> res = new ArrayList<>();
public List<String> binaryTreePaths(TreeNode root) {
if(root == null) return null;
//要注意添加"->"
if(sb.length() > 0) sb.append("->").append(root.val);
else sb.append(root.val);
if(root.left == null && root.right == null){
res.add(sb.toString());
return res;
}
StringBuilder tmp = new StringBuilder(sb.toString());
if(root.left != null){
binaryTreePaths(root.left);
sb = tmp;
}
if(root.right != null){
binaryTreePaths(root.right);
sb = tmp;
}
return res;
}
}
112. 路径总和
class Solution {
public boolean rec(TreeNode root,int targetSum,int sum){
if(root == null) return false;
sum += root.val;
if(root.left == null && root.right == null && sum == targetSum){
return true;
}
return rec(root.left,targetSum,sum) || rec(root.right,targetSum,sum);
}
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root == null) return false;
return rec(root,targetSum,0);
}
}
113. 路径总和 II
相比于 112 题,要找到所有累积和为 targetSum 的路径
基于先序遍历,记录遍历过程中路径的累积总和,当遍历到叶子节点时,判断总和是否等于 targetSum,等于的话将该序列添加到 res 中;否则就将当前节点的值加到 currentSum 以及记录的路径总和中,然后继续遍历左右子树
class Solution {
List<List<Integer>> res; //记录最后的答案
LinkedList<Integer> single; //记录每一条路径上的和
//currentSum 记录遍历到t时路径上的累积总和(还未加上t的值
public void rec(TreeNode t,int targetSum,int currentSum){
if(t == null) return;
if(t.left == null && t.right == null){ //遇到叶子节点
if(currentSum + t.val == targetSum){ //判断路径总和是否符合条件
single.add(t.val);
res.add(new LinkedList<>(single));
single.removeLast();
}
}else{
currentSum += t.val;
single.add(t.val);
rec(t.left,targetSum,currentSum);
rec(t.right,targetSum,currentSum);
single.removeLast();
}
}
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
res = new LinkedList<>();
single = new LinkedList<>();
rec(root,targetSum,0);
return res;
}
}
剑指 Offer II 049. 从根节点到叶节点的路径数字之和
递归的思路:对于当前节点,它代表的值就是父节点代表的值乘以10再加上当前节点的值,然后如果当前节点为叶子节点,那就直接返回这个值,否则就向下递归,求左右子树上叶节点代表的数字之和
class Solution {
public int sumNumbers(TreeNode root) {
return sonNumber(0,root);
}
int sonNumber(int parent,TreeNode t){
if(t == null) return 0;
int val = parent * 10 + t.val;
if(t.left == null && t.right == null) return val;
return sonNumber(val,t.left) + sonNumber(val,t.right);
}
}
124. 二叉树中的最大路径和
“路径 至少包含一个 节点,且不一定经过根节点”,那我们就可以考虑,把每个路径和,跟一个节点绑定起来,对每一个节点,计算出它所对应路径的和,然后取最大的那个路径和作为答案即可
那么如何选择一个节点所对应的路径?这里选择 以节点为根,延展至左右节点的路径为这个节点对应的路径
用一个 maxGain() 方法来计算一个结点对其上层父节点能带来的贡献。这个贡献指的就是该以该结点为根往下延伸所能得到的所有路径 (“从根节点往儿子节点延伸” 这一类型) 中的最大路径和
那么一个结点能得到的最大路径和就是左右儿子的贡献与该结点本身的值的和,不过考虑到结点的值可能为 0,所以一个结点的贡献也可能为 0,所以左右儿子的贡献必须大于 0 才会被拿来计算该结点的最大路径和
class Solution {
int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGain(root);
return maxSum;
}
public int maxGain(TreeNode node) {
if (node == null) {
return 0;
}
//计算左右子节点的最大贡献值
//只有在最大贡献值大于 0 时,才会选取对应子节点
int leftGain = Math.max(maxGain(node.left), 0);
int rightGain = Math.max(maxGain(node.right), 0);
//当前节点的最大路径和为该节点的值与该节点的左右子节点的最大贡献值之和
int priceNewpath = node.val + leftGain + rightGain;
//更新答案
maxSum = Math.max(maxSum, priceNewpath);
//返回节点对上层结点的最大贡献值
return node.val + Math.max(leftGain, rightGain);
}
}
236.二叉树的最近公共祖先
最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)
两个前提
- 所有 Node.val 互不相同
- p 和 q 均存在于给定的二叉树中
//递归方法,在root时,要找p跟q的lca,可以分别往左节点跟右节点找p或者q
//1 如果左右子树上都找到了,说明p跟q一个在左子树上一个在右子树,那么当前节点root就是lca;
//2 如果只在左右子树上一边找到了,说明p跟q都在这个子树里,lca自然也在这个子树里
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//首先找到null时返回null是必然的。然后如果找到了p或q,
//那就返回这个结点,所以写在一起就是下面这句
if(root == p || root == q || root == null) return root;
//分别找左边和右边
TreeNode l = lowestCommonAncestor(root.left,p,q);
TreeNode r = lowestCommonAncestor(root.right,p,q);
//因为p和q一定是存在的,所以要么是一个来自左子树一个来自右子树
//要么是都来自左子树或都来自右子树。反正l跟r肯定不会两个都是null
//如果l跟r都不为null,说明p和q一个来自左子树一个来自右子树,
//那么当前节点就是所求的最近公共祖先
if(l != null && r != null) return root;
//当l不为null,那么r肯定为null,所以p跟q都来自左子树,l为最近公共祖先
if(l != null) return l;
//r不为null时同理
if(r != null) return r;
return null;
}
剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
递归思路:基于先序遍历,对于当前节点,如果 p,q 中有一个节点的值小于等于当前结点的值,一个节点的值大于等于当前结点的值,那就说明当前节点就是 p 和 q 的最近公共祖先,因为我们可以发现,在整棵树中,大于其中一个节点小于另外一个节点的所有节点中,他们的最近公共祖先是最靠近根部的,所以在先序遍历中一定是第一个被访问的。
前提很重要,是基于先序遍历,并不是说在树中符合 p,q 中有一个节点的值小于等于当前结点的值,一个节点的值大于等于当前结点的值这个条件,这个节点就是它们的祖先;
否则如果两个结点的值都小于当前结点的值,那就往左儿子上递归;否则就是两个节点的值都大于当前结点的值,那就往右儿子上递归
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
if((p.val <= root.val && q.val >= root.val) || (p.val >= root.val && q.val <= root.val)) return root;
if(q.val < root.val && p.val < root.val) return lowestCommonAncestor(root.left,p,q);
else return lowestCommonAncestor(root.right,p,q);
}