LeetCode - 6. 二叉树(二)
刷题顺序来自:代码随想录
左叶子之和
404. 左叶子之和
计算给定二叉树的所有左叶子之和。
private int sum = 0;
public int sumOfLeftLeaves(TreeNode root) {
if(root == null) {
return sum;
}
if(root != null & root.left != null && root.left.left == null && root.left.right == null) {
sum += root.left.val;
}
sumOfLeftLeaves(root.left);
sumOfLeftLeaves(root.right);
return sum;
}
找树左下角的值
513. 找树左下角的值
给定一个二叉树的 根节点 root
,请找出该二叉树的 最底层 最左边 节点的值。假设二叉树中至少有一个节点。
迭代法
层级遍历,每次记录第一个节点的值。
递归法
遍历整颗树,每次遇到叶子节点,表示需要更新深度。记录第一个更新深度叶子节点的值。
int depth = 0;
int left = 0;
public int findBottomLeftValue(TreeNode root) {
findBottomLeft(root, 1);
return left;
}
private void findBottomLeft(TreeNode root, int currDepth) {
if(root == null) {
return;
}
// 如果是叶子节点,递归终止,查看是否要更新深度
if(root.left == null && root.right == null) {
// 如果需要更新,这表明是当前层遇到的第一个叶子节点(最左侧),则记录下它的值
if(depth < currDepth) {
depth = currDepth;
left = root.val;
}
return;
}
findBottomLeft(root.left, currDepth + 1);
findBottomLeft(root.right, currDepth + 1);
}
路径总和
112. 路径总和
给你二叉树的根节点 root
和一个表示目标和的整数 targetSum
。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum
。如果存在,返回 true
;否则,返回 false
。
boolean found = false; // 记录是否找到路径
public boolean hasPathSum(TreeNode root, int targetSum) {
pathSum(root, targetSum, 0);
return found;
}
// currSum记录当前节点之前路径的总和
private void pathSum(TreeNode root, int targetSum, int currSum) {
// 递归终止条件:当前节点为null,或者已经找到了一条正确的路径
if(root == null || found) return;
currSum += root.val; // 更新当前路径总和
if(root.left == null && root.right == null && targetSum == currSum) {
found = true;
return;
}
// 遍历整颗树
pathSum(root.left, targetSum, currSum);
pathSum(root.right, targetSum, currSum);
}
113. 路径总和 II
给你二叉树的根节点 root
和一个整数目标和 targetSum
,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
和前面的方法类似:
ArrayList<List<Integer>> res = new ArrayList<>(); // 结果
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
findPath(root, targetSum, new ArrayList<Integer>());
return res;
}
private void findPath(TreeNode root, int targetSum, List<Integer> path) {
if(root == null) return;
path.add(root.val); // 更新当前路径
if(root.left == null && root.right == null && targetSum == root.val) {
res.add(path);
return;
}
// 遍历整颗树
findPath(root.left, targetSum - root.val, new ArrayList<>(path)); // 深拷贝
findPath(root.right, targetSum - root.val, new ArrayList<>(path));
}
回溯
上面的方法速度慢,占用内存空间大的原因是:复制了多个path
数组。使用回溯法,只用一个数组表示路径,在经过当前节点之后,将节点从数组中移除。
ArrayList<List<Integer>> res = new ArrayList<>();
ArrayList<Integer> path = new ArrayList<>();
public List<List<Integer>> pathSum (TreeNode root,int targetSum) {
travesal(root, targetSum);
return res;
}
private void travesal(TreeNode root, int count) {
if(root == null) {
return;
}
path.add(root.val); // 当前节点加入路径
if(root.left == null && root.right == null && count == root.val) {
res.add(new ArrayList<>(path)); // 深拷贝
// return; // 注意这里不能return,因为path add的数据没有移除
}
travesal(root.left, count - root.val);
travesal(root.right, count - root.val);
path.remove(path.size()-1); // 回溯,当前节点移除出路径
}
由遍历结果构造二叉树
106. 从中序与后序遍历序列构造二叉树
根据一棵树的中序遍历与后序遍历构造二叉树。你可以假设树中没有重复的元素。
public TreeNode buildTree(int[] inorder, int[] postorder) {
TreeNode root = new TreeNode(postorder[postorder.length-1]);
build(inorder, postorder, root, 0, inorder.length, 0, postorder.length);
return root;
}
// 每次递归,建立root节点的左右儿子节点
// left和right指示了遍历结果的位置,左闭右开
private TreeNode build(int[] inorder, int[] postorder, TreeNode root, int inLeft, int inRight, int postLeft, int postRight) {
if(inRight - inLeft <= 1) return root;
// 找到root节点在中序遍历结果的索引
int rootIndex = inLeft;
for(int i = inLeft; i < inRight; i++) {
if(root.val == inorder[i]) {
rootIndex = i;
break;
}
}
// 建立儿子节点前,首先判断儿子节点是否存在
if(rootIndex != inLeft) {
root.left = new TreeNode(postorder[postLeft+rootIndex-inLeft-1]);
build(inorder, postorder, root.left, inLeft, rootIndex, postLeft, postLeft+rootIndex-inLeft);
}
if(rootIndex != inRight-1) {
root.right = new TreeNode(postorder[postRight-2]);
build(inorder, postorder, root.right, rootIndex+1, inRight, postLeft+rootIndex-inLeft, postRight-1);
}
return root;
}
105. 从前序与中序遍历序列构造二叉树
给定一棵树的前序遍历 preorder
与中序遍历 inorder
。请构造二叉树并返回其根节点。
与上一题类似:
public TreeNode buildTree(int[] preorder, int[] inorder) {
TreeNode root = new TreeNode(preorder[0]);
build(preorder, inorder, root, 0, preorder.length, 0, inorder.length);
return root;
}
private TreeNode build(int[] preorder, int[] inorder, TreeNode root, int preLeft, int preRight, int inLeft, int inRight) {
if(preRight-preLeft <= 1) return root;
// 找到root节点在中序遍历的索引
int rootIndex = inLeft;
for(int i = inLeft; i < inRight; i++) {
if(root.val == inorder[i]) {
rootIndex = i;
break;
}
}
if(rootIndex != inLeft) {
root.left = new TreeNode(preorder[preLeft+1]);
build(preorder, inorder, root.left, preLeft+1, preLeft+rootIndex-inLeft+1, inLeft, rootIndex);
}
if(rootIndex != inRight - 1) {
root.right = new TreeNode(preorder[preLeft+rootIndex-inLeft+1]);
build(preorder, inorder, root.right, preLeft+rootIndex-inLeft+1, preRight, rootIndex+1, inRight);
}
return root;
}
最大二叉树
654. 最大二叉树
给定一个不含重复元素的整数数组 nums
。一个以此数组直接递归构建的 最大二叉树 定义如下:
- 二叉树的根是数组
nums
中的最大元素。 - 左子树是通过数组中 最大值左边部分 递归构造出的最大二叉树。
- 右子树是通过数组中 最大值右边部分 递归构造出的最大二叉树。
public TreeNode constructMaximumBinaryTree(int[] nums) {
return construct(nums, 0, nums.length);
}
// 每次递归,构建最大值为根节点
private TreeNode construct(int[] nums, int left, int right) {
if(right <= left) return null;
// 找出最大值的索引
int maxIndex = left;
for(int i = left; i < right; i++) {
if(nums[i] > nums[maxIndex]) {
maxIndex = i;
}
}
TreeNode root = new TreeNode(nums[maxIndex]);
root.left = construct(nums, left, maxIndex);
root.right = construct(nums, maxIndex+1, right);
return root;
}
合并二叉树
617. 合并二叉树
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1 == null) {
return root2;
}
if(root2 == null) {
return root1;
}
root1.val += root2.val;
root1.left = mergeTrees(root1.left, root2.left);
root1.right = mergeTrees(root1.right, root2.right);
return root1;
}
二叉搜索树
700. 二叉搜索树中的搜索
给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。
public TreeNode searchBST(TreeNode root, int val) {
if(root == null || root.val == val) return root;
if(root.val < val) return searchBST(root.right, val);
else return searchBST(root.left, val);
}
98. 验证二叉搜索树
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
ArrayList<Integer> list;
public boolean isValidBST(TreeNode root) {
list = new ArrayList<Integer>();
inorder(root);
return isOrder(list);
}
// 中序遍历,结果存储到list
private void inorder(TreeNode root) {
if(root != null) {
inorder(root.left);
list.add(root.val);
inorder(root.right);
}
}
// 判断list是否是单调递增的数组
private boolean isOrder(ArrayList<Integer> list) {
if(list.size() <= 1) return true;
for(int i = 1; i < list.size(); i++) {
if(list.get(i) <= list.get(i-1)) {
return false;
}
}
return true;
}
530. 二叉搜索树的最小绝对差
给你一个二叉搜索树的根节点 root
,返回 树中任意两不同节点值之间的最小差值。
中序遍历
BST中序遍历结果存储为有序数组,找出相邻两个元素的最小值。
ArrayList<Integer> list = new ArrayList<>();
public int getMinimumDifference(TreeNode root) {
traversal(root);
int min = list.get(1) - list.get(0);
for(int i = 2; i < list.size(); i++) {
if(list.get(i) - list.get(i-1) < min) {
min = list.get(i) - list.get(i-1);
}
}
return min;
}
// 中序遍历
private void traversal(TreeNode root) {
if(root != null) {
traversal(root.left);
list.add(root.val);
traversal(root.right);
}
}
实际上不需要存到数组中,直接在遍历时判断即可。
public int getMinimumDifference(TreeNode root) {
traversal(root);
return min;
}
int min = Integer.MAX_VALUE;
TreeNode pre = null; // 记录前一个节点
private void traversal(TreeNode root) {
if(root != null) {
traversal(root.left);
if(pre != null) {
if(root.val - pre.val < min) {
min = root.val - pre.val;
}
}
pre = root;
traversal(root.right);
}
}
501. 二叉搜索树中的众数
给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。
假定 BST 有如下定义:
- 结点左子树中所含结点的值小于等于当前结点的值
- 结点右子树中所含结点的值大于等于当前结点的值
- 左子树和右子树都是二叉搜索树
中序遍历
进行两次遍历,第一次记录最大频率值,第二次记录众数。
也可以在一次遍历完成,但是需要多次清空数组,即每当发现一个频率更高的数字,数组内保存的数字需要清除。
public int[] findMode(TreeNode root) {
maxFreq = 0;
currFreq = 0;
pre = null;
traversal(root, false);
currFreq = 0;
pre = null;
list = new ArrayList<Integer>();
traversal(root, true);
int[] res = new int[list.size()];
for(int i = 0; i < list.size(); i++) {
res[i] = list.get(i);
}
return res;
}
int maxFreq;
int currFreq;
TreeNode pre;
ArrayList<Integer> list;
private void traversal(TreeNode root, boolean isFound) {
if(root != null) {
traversal(root.left, isFound);
if(pre != null && pre.val == root.val) {
currFreq++;
}
else {
currFreq = 1;
}
// 是否已经找到了最大频率,用于区分第一次和第二次遍历
if(isFound) {
if(currFreq == maxFreq) {
list.add(root.val);
}
}
else {
if(currFreq > maxFreq) {
maxFreq = currFreq;
}
}
pre = root;
traversal(root.right, isFound);
}
}
235. 二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
根据路径判断
这个方法速度较慢。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
ArrayList<TreeNode> pPath = findPath(root, p);
ArrayList<TreeNode> qPath = findPath(root, q);
// p和q的路径最后一个相同的节点即为最近公共祖先
TreeNode ancestor = root;
for(int i = 0; i < Math.min(pPath.size(), qPath.size()); i++) {
if(pPath.get(i) != qPath.get(i)) {
break;
}
ancestor = pPath.get(i);
}
return ancestor;
}
// 从root节点到target节点的路径
private ArrayList<TreeNode> findPath(TreeNode root, TreeNode target) {
ArrayList<TreeNode> list = new ArrayList<>();
if(root != null && target != null) {
list.add(root);
while(root != null && root.val != target.val) {
if(root.val < target.val) {
root = root.right;
}
else {
root = root.left;
}
list.add(root);
}
}
return list;
}
一次遍历
第一个分叉点(也是唯一一个交叉点)就是最近公共祖先。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while(root != null) {
if(root.val < p.val && root.val < q.val) { // p和q都在root的右边
root = root.right;
}
else if(root.val > p.val && root.val > q.val) { // p和q都在root的左边
root = root.left;
}
else { // 第一个分叉点就是最近公共祖先
return root;
}
}
return null;
}
236. 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
根据路径判断
这个方法速度较慢。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
isFound = false;
LinkedList<TreeNode> pPath = findPath(root, p, new LinkedList<TreeNode>());
isFound = false;
LinkedList<TreeNode> qPath = findPath(root, q, new LinkedList<TreeNode>());
// p和q的路径最后一个相同的节点即为最近公共祖先
TreeNode ancestor = root;
for(int i = 0; i < Math.min(pPath.size(), qPath.size()); i++) {
if(pPath.get(i) != qPath.get(i)) {
break;
}
ancestor = pPath.get(i);
}
return ancestor;
}
// 通过前序遍历和回溯,找到从root节点到target节点的路径
boolean isFound;
private LinkedList<TreeNode> findPath(TreeNode root, TreeNode target, LinkedList<TreeNode> path) {
if(root != null && !isFound) {
path.add(root);
if(root == target) {
isFound = true;
return path;
}
findPath(root.left, target, path);
findPath(root.right, target, path);
if(!isFound){
path.removeLast();
}
}
return path;
}
后序遍历
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) {
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 只有第一个分叉点才会左右都不为null
if (left != null && right != null) {
return root;
}
if (left == null) {
return right;
}
return left;
}
另一种后序遍历,原理类似,即后序遍历第一次碰到的交叉点(也是唯一一个交叉点)为最近公共祖先,交叉点有两种情况:
- 当前节点的左子树包含p或q,且右子树也包含p或q
- 当前节点为p或q,且左子树或右子树包含p或q
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
traversal(root, p, q);
return ancestor;
}
TreeNode ancestor;
// 节点root的子树是否包含p或q
private boolean traversal(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return false;
boolean left = traversal(root.left, p, q);
boolean right = traversal(root.right,p, q);
// 判断root是否是交叉点
if(left && right || ((left || right) && (root == p || root == q))) {
ancestor = root;
}
// 判断root是否包含p或q
if(left || right || root == p || root == q) {
return true;
}
return false;
}
701. 二叉搜索树中的插入操作
给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同
public TreeNode insertIntoBST(TreeNode root, int val) {
TreeNode node = new TreeNode(val);
if(root == null) return node;
// 比当前节点小就向左,大就向右,直到遇到空节点就可以进行插入操作
TreeNode curr = root;
while(true) {
if(curr.val > val) {
if(curr.left != null) {
curr = curr.left;
}
else {
curr.left = node;
break;
}
}
else if(curr.val < val) {
if(curr.right != null) {
curr = curr.right;
}
else {
curr.right = node;
break;
}
}
}
return root;
}
递归
public TreeNode insertIntoBST(TreeNode root, int val) {
if(root == null) return new TreeNode(val);
if(root.val > val) root.left = insertIntoBST(root.left, val);
if(root.val < val) root.right = insertIntoBST(root.right, val);
return root;
}
450. 删除二叉搜索树中的节点
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
- 首先找到需要删除的节点;
- 如果找到了,删除它。
public TreeNode deleteNode(TreeNode root, int key) {
if(root == null) return null;
if(root.val == key) { // 删除当前节点,返回新节点
return delRoot(root);
}
// 递归地向左右子树查找
// 如果root的左子节或右节点不需要被删除,则保持原状
// 如果需要被删除,则修改为删除后的新节点
else if(root.val > key) {
root.left = deleteNode(root.left, key);
}
else if(root.val < key) {
root.right = deleteNode(root.right, key);
}
return root;
}
// 删除根节点root的过程
private TreeNode delRoot(TreeNode root) {
// 如果root的左节点不为null, 则挑选左子树中的最右节点作为新的root
if(root.left != null) {
TreeNode curr = root.left;
if(curr.right == null) { // root的左节点就恰好为最右节点的情况
curr.right = root.right;
}
else {
// 找到root的左子树中的最右节点
TreeNode pre = curr;
while(curr.right != null) {
pre = curr;
curr = curr.right;
}
// 将找到的最右节点作为新的root
pre.right = curr.left;
curr.left = root.left;
curr.right = root.right;
}
return curr;
}
else { // root左子树为空,那么直接将右节点作为新的root
return root.right;
}
}
669. 修剪二叉搜索树
给你二叉搜索树的根节点 root
,同时给定最小边界low
和最大边界 high
。通过修剪二叉搜索树,使得所有节点的值在[low, high]
中。修剪树不应该改变保留在树中的元素的相对结构(即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在唯一的答案。
所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。
// 后序遍历,自底向上修剪
public TreeNode trimBST(TreeNode root, int low, int high) {
if(root == null) return null;
root.left = trimBST(root.left, low, high);
root.right = trimBST(root.right, low, high);
return remove(root, low, high);
}
// 由于是二叉搜索树,如果当前节点需要修剪,则需要将其左子树或右子树一起修剪
private TreeNode remove(TreeNode root, int low, int high) {
if(root == null) return null;
if(root.val < low) {
return root.right;
}
if(root.val > high) {
return root.left;
}
return root;
}
108. 将有序数组转换为二叉搜索树
给你一个整数数组 nums
,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
递归
public TreeNode sortedArrayToBST(int[] nums) {
return buildTree(nums, 0, nums.length);
}
// 每次递归根据数组最中间的数构建新节点
private TreeNode buildTree(int[] nums, int left, int right) {
if(left >= right) return null;
int mid = (left + right) / 2;
TreeNode root = new TreeNode(nums[mid]);
root.left = buildTree(nums, left, mid);
root.right = buildTree(nums, mid + 1, right);
return root;
}
538. 把二叉搜索树转换为累加树
给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点node
的新值等于原树中大于或等于 node.val
的值之和。
提醒一下,二叉搜索树满足下列约束条件:
- 节点的左子树仅包含键 小于 节点键的节点。
- 节点的右子树仅包含键 大于 节点键的节点。
- 左右子树也必须是二叉搜索树。
中序遍历
public TreeNode convertBST(TreeNode root) {
traversal(root);
sumList();
update(root);
return root;
}
// 将中序遍历的结果存储到数组list中
private ArrayList<Integer> list = new ArrayList<>();
private void traversal(TreeNode root) {
if(root != null) {
traversal(root.left);
list.add(root.val);
traversal(root.right);
}
}
// 将list数组倒序累加
private void sumList() {
for(int i = list.size()-2; i >= 0; i--) {
list.set(i, list.get(i) + list.get(i+1));
}
}
// 再次中序遍历,根据累加后的list数组更新树
private int index = 0;
private void update(TreeNode root) {
if(root != null) {
update(root.left);
root.val = list.get(index);
index++;
update(root.right);
}
}
反中序遍历
其实,只需要“右中左”的反中序遍历,记录上一个节点pre,每次将当前节点的值加上pre的值即可。
private TreeNode pre = null; // 记录上一个节点的值
public TreeNode convertBST(TreeNode root) {
if(root == null) return null;
convertBST(root.right);
if(pre != null) {
root.val += pre.val;
}
pre = root; // 更新pre的值
convertBST(root.left);
return root;
}