简单
108. 将有序数组转换为二叉搜索树
【参考:108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode)】
- 递归
中序遍历是升序的,因此本题等同于根据中序遍历的序列恢复二叉搜索树。因此我们可以以升序序列中的任一个元素作为根节点,以该元素左边的升序序列构建左子树,以该元素右边的升序序列构建右子树,这样得到的树就是一棵二叉搜索树啦~ 又因为本题要求高度平衡,因此我们需要选择升序序列的中间元素作为根节点奥~
在构造二叉树的时候尽量不要重新定义左右区间数组,而是用下标来操作原数组。
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return build(nums, 0, nums.length - 1);
}
// 将闭区间 [left, right] 中的元素转化成 BST,返回根节点
TreeNode build(int[] nums, int left, int right) {
if (left > right) {
// 区间为空
return null;
}
// 构造根节点
// BST 节点左小右大,中间的元素就是根节点
// 如果数组长度为偶数,中间位置有两个元素,取靠左边的。
int mid = (left + right) / 2;
TreeNode root = new TreeNode(nums[mid]);
// 递归构建左子树
root.left = build(nums, left, mid - 1);
// 递归构造右子树
root.right = build(nums, mid + 1, right);
return root;
}
}
迭代法可以通过三个队列来模拟,一个队列放遍历的节点,一个队列放左区间下标,一个队列放右区间下标。
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
if (nums.length == 0) return null;
//根节点初始化
TreeNode root = new TreeNode(-1);
Queue<TreeNode> nodeQueue = new LinkedList<>();
Queue<Integer> leftQueue = new LinkedList<>();
Queue<Integer> rightQueue = new LinkedList<>();
// 根节点入队列
nodeQueue.offer(root);
// 0为左区间下标初始位置
leftQueue.offer(0);
// nums.size() - 1为右区间下标初始位置
rightQueue.offer(nums.length - 1);
while (!nodeQueue.isEmpty()) {
TreeNode currNode = nodeQueue.poll();
int left = leftQueue.poll();
int right = rightQueue.poll();
int mid = left + ((right - left) >> 1);
// 将mid对应的元素给中间节点
currNode.val = nums[mid];
// 处理左区间
if (left <= mid - 1) {
// 先新建一个节点,赋值-1,真实值要等到下一次while循环后再赋值
currNode.left = new TreeNode(-1);
nodeQueue.offer(currNode.left);
leftQueue.offer(left);
rightQueue.offer(mid - 1);
}
// 处理右区间
if (right >= mid + 1) {
currNode.right = new TreeNode(-1);
nodeQueue.offer(currNode.right);
leftQueue.offer(mid + 1);
rightQueue.offer(right);
}
}
return root;
}
}
235. 二叉搜索树的最近公共祖先
【参考:235. 二叉搜索树的最近公共祖先 - 力扣(LeetCode)】
labuladong
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) return null;
if (p.val > q.val) {
// 保证 p.val <= q.val,便于后续情况讨论
return lowestCommonAncestor(root, q, p);
}
if (root.val >= p.val && root.val <= q.val) {
// p <= root <= q
// 即 p 和 q 分别在 root 的左右子树,那么 root 就是 LCA
return root;
}
if (root.val > q.val) {
// p 和 q 都在 root 的左子树,那么 LCA 在左子树
return lowestCommonAncestor(root.left, p, q);
} else {
// p 和 q 都在 root 的右子树,那么 LCA 在右子树
return lowestCommonAncestor(root.right, p, q);
}
}
}
【参考:二叉搜索树的最近公共祖先(3种解决方式) - 二叉搜索树的最近公共祖先 - 力扣(LeetCode)】
p,q的大小未定,所以直接使用if判断很麻烦
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//如果根节点和p,q的差相乘是正数,说明这两个差值要么都是正数要么都是负数,
//也就是说他们肯定都位于根节点的同一侧,就继续往下找
while ((root.val - p.val) * (root.val - q.val) > 0)
root = p.val < root.val ? root.left : root.right;
//如果相乘的结果是负数,说明p和q位于根节点的两侧,如果等于0,说明至少有一个就是根节点
return root;
}
}
501. 二叉搜索树中的众数 (难)
【参考:501. 二叉搜索树中的众数 题解 - 力扣(LeetCode)】
【参考:代码随想录# 501.二叉搜索树中的众数】
class Solution {
List<Integer> result = new ArrayList<Integer>();
int count=0; // 统计频率
int maxCount=0; // 最大频率
TreeNode pre=null;// 前一个节点
public int[] findMode(TreeNode root) {
dfs(root);
int[] mode = new int[result.size()];
for (int i = 0; i < result.size(); ++i) {
mode[i] = result.get(i);
}
return mode;
}
public void dfs(TreeNode cur) {
if (cur == null) return;
dfs(cur.left); // 左
// 中
if(pre==null){ // 第一个节点
count=1;
}else if(pre.val==cur.val){ // 与前一个节点的数值相同
count++;
}else{ // 与前一个节点的数值不同
count = 1;
}
pre = cur; // 更新上一个节点
if (count == maxCount) { // 和最大频率相同,加入
result.add(cur.val);
}
if (count > maxCount) { // 大于最大频率
maxCount = count; // 更新最大频率
result.clear(); // 清空数据
result.add(cur.val);// 加入
}
dfs(cur.right); // 右
}
}
530. 二叉搜索树的最小绝对差 ***
【参考:530. 二叉搜索树的最小绝对差 - 力扣(LeetCode)】
【参考:「代码随想录」带你学透二叉树!530. 二叉搜索树的最小绝对差:【有序数组】详解 - 二叉搜索树的最小绝对差 - 力扣(LeetCode)】
技巧:在递归中记录前一个节点
class Solution {
int result = Integer.MAX_VALUE;// Java整形无引用传递,故采用公共变量
TreeNode pre;// 记录上一个遍历的节点
public int getMinimumDifference(TreeNode root) {
traverse(root);
return result;
}
public void traverse(TreeNode root) {
if (root == null) return;
traverse(root.left);
if(pre!=null){ // 排除pre第一次为null
result=Math.min(result,root.val-pre.val);// root.val>pre.val
}
pre=root;
traverse(root.right);
}
}
- 中序遍历BST = 遍历单调递增的有序数组
只需寻找数组中相邻两个数之间的最小绝对差即可
class Solution {
public int getMinimumDifference(TreeNode root) {
ArrayList<Integer> list = new ArrayList<>();
inTraversal(root, list);
int result = Integer.MAX_VALUE;
for (int i = 1; i < list.size(); i++) {
result= Math.min( result,list.get(i)-list.get(i-1));
}
return result;
}
// 中序遍历
public void inTraversal(TreeNode root, List list) {
if (root == null)
return;
inTraversal(root.left, list);
list.add(root.val);
inTraversal(root.right, list);
}
}
700. 二叉搜索树中的搜索
【参考:700. 二叉搜索树中的搜索 - 力扣(LeetCode)】
- DFS递归
// 自己写的
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
// 根节点比较
if(root.val==val){
return root;
}
// 去右子树遍历
if(root.val<val && root.right!=null){
return searchBST(root.right,val);
}
// 去左子树遍历
if(root.val>val && root.left!=null){
return searchBST(root.left,val);
}
// 没找到
return null;
}
}
// labuladong
class Solution {
public TreeNode searchBST(TreeNode root, int target) {
if (root == null) {
return null;
}
// 去左子树搜索
if (root.val > target) {
return searchBST(root.left, target);
}
// 去右子树搜索
if (root.val < target) {
return searchBST(root.right, target);
}
return root;
}
}
- 迭代法
【参考:代码随想录# 700.二叉搜索树中的搜索】
// Carl
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
while(root!=null){
if(root.val==val) return root;
else if(root.val<val) root=root.right;
else if(root.val>val) root=root.left;
}
return null;
}
}
938. 二叉搜索树的范围和 ***
【参考:938. 二叉搜索树的范围和 - 力扣(LeetCode)】
BFS
class Solution {
public int rangeSumBST(TreeNode root, int low, int high) {
if(root==null) return 0;
int result=0;
Queue<TreeNode> queue=new LinkedList<>();
queue.add(root);//首先将根节点入队
while(!queue.isEmpty()){ //队列非空时循环继续
int size=queue.size();//队列的长度
while(size>0){ // 遍历已经在队列的节点
TreeNode cur=queue.poll();
//节点的值在low和high之间时,需要加上该节点值
if(cur.val>=low&&cur.val<=high){
result+=cur.val;
}
// 以及左右子树入队,为空就不入队
if(cur.left!=null){
queue.add(cur.left);
}
if(cur.right!=null){
queue.add(cur.right);
}
size--;
}
}
return result;
}
}
递归
// 遍历的思路
class Solution {
public int rangeSumBST(TreeNode root, int low, int high) {
//找边界
if(root == null){
return 0;
}
//左子树
int leftSum = rangeSumBST(root.left, low, high);
//右子树
int rightSum = rangeSumBST(root.right, low, high);
int result = leftSum + rightSum;
//判断根节点
if(root.val >= low && root.val <= high){
result += root.val;
}
return result;
}
}
// 分解问题的思路
class Solution2 {
// 定义:输入一个 BST,计算值落在 [low, high] 之间的元素之和
public int rangeSumBST(TreeNode root, int low, int high) {
if (root == null) return 0;
if (root.val < low) {
// 目标区间在右子树
return rangeSumBST(root.right, low, high);
} else if (root.val > high) {
// 目标区间在左子树
return rangeSumBST(root.left, low, high);
} else {
// 以 root 为根的这棵 BST 落在 [low, high] 之间的元素之和,
// 等于 root.val 加上左右子树落在区间的元素之和
return root.val
+ rangeSumBST(root.left, low, high)
+ rangeSumBST(root.right, low, high);
}
}
}
中等
98. 验证二叉搜索树
【参考:98. 验证二叉搜索树 - 力扣(LeetCode)】
【参考:手把手带你刷二叉搜索树(第二期) :: labuladong的算法小抄】
初学者做这题很容易有误区:BST 不是左小右大么,那我只要检查 root.val > root.left.val 且 root.val < root.right.val 不就行了?
这样是不对的,因为 BST 左小右大的特性是指 root.val 要比左子树的所有节点都更大,要比右子树的所有节点都小,你只检查左右两个子节点当然是不够的。
正确解法是通过使用辅助函数,增加函数参数列表,在参数中携带额外信息,将这种约束传递给子树的所有节点,这也是二叉搜索树算法的一个小技巧吧。
class Solution {
public boolean isValidBST(TreeNode root) {
return isValidBST(root,null,null);
}
/* 限定以 root 为根的子树节点必须满足 max.val > root.val > min.val */
public boolean isValidBST(TreeNode root,TreeNode minNode, TreeNode maxNode) {
if(root==null) return true;
// 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST
if(minNode!=null && root.val <= minNode.val) return false;
if(maxNode!=null && root.val >= maxNode.val) return false;
// 限定左子树的最大值是 root.val,右子树的最小值是 root.val
return isValidBST(root.left,minNode,root)
&& isValidBST(root.right,root,maxNode);
}
}
// 官方
class Solution {
public boolean isValidBST(TreeNode root) {
return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
public boolean isValidBST(TreeNode node, long lower, long upper) {
if (node == null) {
return true;
}
if (node.val <= lower || node.val >= upper) {
return false;
}
return isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper);
}
}
- 中序遍历BST = 遍历单调递增的有序数组
只需查看数组是否单调递增即可
class Solution {
public boolean isValidBST(TreeNode root) {
ArrayList<Integer> list = new ArrayList<>();
inTraversal(root, list);
for (int i = 1; i < list.size(); i++) {
if (list.get(i - 1) >= list.get(i)) {
return false;
}
}
return true;
}
// 中序遍历
public void inTraversal(TreeNode root, List list) {
if (root == null)
return;
inTraversal(root.left, list);
list.add(root.val);
inTraversal(root.right, list);
}
}
【参考:中序遍历轻松拿下,🤷♀️必须秒懂! - 验证二叉搜索树 - 力扣(LeetCode)】
把树看成是只有3个节点的满二叉树
class Solution {
long pre = Long.MIN_VALUE; // 记录前一个节点的最小值
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
// 访问左子树
if (!isValidBST(root.left)) {
return false;
}
// 访问当前节点:
// 如果当前节点小于等于中序遍历的前一个节点,说明不满足BST,返回 false;否则继续遍历。
if (root.val <= pre) {
return false;
}
pre = root.val;
// 访问右子树
return isValidBST(root.right);
}
}
参考用栈实现中序遍历 第94题
class Solution {
// 迭代
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode pre = null; // 记录前一个节点
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;// 左
}
// 中,处理
TreeNode pop = stack.pop();
if (pre != null && pop.val <= pre.val) {
return false;
}
pre = pop;
root = pop.right;// 右
}
return true;
}
}
701. 二叉搜索树中的插入操作
【参考:701. 二叉搜索树中的插入操作 - 力扣(LeetCode)】
【参考:手把手带你刷二叉搜索树(第二期) :: labuladong的算法小抄】
一旦涉及「改」,函数就要返回 TreeNode 类型,并且对递归调用的返回值进行接收。
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
// 找到空位置插入新节点
if(root==null){
TreeNode node=new TreeNode(val);
return node;
}
if(root.val > val){
root.left=insertIntoBST(root.left,val);
}
if(root.val < val){
root.right=insertIntoBST(root.right,val);
}
return root;
}
}
递归函数不用返回值也可以,找到插入的节点位置,直接让其父节点指向插入节点,结束递归,也是可以的。
class Solution {
TreeNode parent;
public TreeNode insertIntoBST(TreeNode root, int val) {
if(root==null){
parent=new TreeNode(val);
}
traversal(root,val);
return root;
}
public void traversal(TreeNode cur,int val){
// 找到空位置插入新节点
if(cur==null){
TreeNode node=new TreeNode(val);
if(val > parent.val){
parent.right=node;
}else{
parent.left=node;
}
return;
}
parent=cur;// 更新父节点
if(cur.val > val)
traversal(cur.left,val);
if(cur.val < val)
traversal(cur.right,val);
return;
}
}
可以看出还是麻烦一些的。
我之所以举这个例子,是想说明通过递归函数的返回值完成父子节点的赋值是可以带来便利的。
450. 删除二叉搜索树中的节点(难)
【参考:450. 删除二叉搜索树中的节点 - 力扣(LeetCode)】
【参考:手把手带你刷二叉搜索树(第二期) :: labuladong的算法小抄】
假如要删除的节点为叶节点,直接删除
假如要删除的节点左节点不为空但右节点为空,则直接用左节点替换当前节点
假如要删除的节点右节点不为空但左节点为空,则直接用右节点替换当前节点
最后要删除的节点左右均不为空,寻找右子树中最左节点,用这个最左节点(就是后继节点)替换当前节点,并且将最左节点从原位置删除。
// labuladong
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null)
return null;
if (root.val == key) {
// 这两个 if 把情况 1 和 2 都正确处理了
if (root.left == null)
return root.right;
if (root.right == null)
return root.left;
// 处理情况 3
// 获得右子树最小的节点
TreeNode minNode = getMin(root.right);
// 删除右子树最小的节点
root.right = deleteNode(root.right, minNode.val);
// 用右子树最小的节点替换 root 节点
minNode.left = root.left;
minNode.right = root.right;
root = minNode;
} else if (root.val > key) {
root.left = deleteNode(root.left, key);
} else if (root.val < key) {
root.right = deleteNode(root.right, key);
}
return root;
}
TreeNode getMin(TreeNode node) {
// BST 最左边的就是最小的
while (node.left != null)
node = node.left;
return node;
}
}
【参考:秒懂就完事了! - 删除二叉搜索树中的节点 - 力扣(LeetCode)】
这个浅显易懂
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) return nullptr;
if (key > root->val) root->right = deleteNode(root->right, key); // 去右子树删除
else if (key < root->val) root->left = deleteNode(root->left, key); // 去左子树删除
else{ // 当前节点就是要删除的节点
if (! root->left) return root->right; // 情况1,欲删除节点无左子
if (! root->right) return root->left; // 情况2,欲删除节点无右子
TreeNode* node = root->right; // 情况3,欲删除节点左右子都有
while (node->left) // 寻找欲删除节点右子树的最左节点
node = node->left;
node->left = root->left; // 将欲删除节点的左子树成为其右子树的最左节点的左子树
root = root->right; // 欲删除节点的右子顶替其位置,节点被删除
}
return root;
}
};
669. 修剪二叉搜索树 (难)
【参考:669. 修剪二叉搜索树 - 力扣(LeetCode)】
只看代码,其实不太好理解节点是符合移除的,这一块大家可以自己在模拟模拟!
// labuladong
class Solution {
// 定义:删除 BST 中小于 low 和大于 high 的所有节点,返回结果 BST
public TreeNode trimBST(TreeNode root, int low, int high) {
if (root == null) return null;
// 递归右子树 并返回右子树中符合条件的头节点
if (root.val < low) {
// 直接返回 root.right
// 等于删除 root 以及 root 的左子树
return trimBST(root.right, low, high);
}
// 递归左子树 并返回左子树中符合条件的头节点
if (root.val > high) {
// 直接返回 root.left
// 等于删除 root 以及 root 的右子树
return trimBST(root.left, low, high);
}
// 闭区间 [lo, hi] 内的节点什么都不做
root.left = trimBST(root.left, low, high);
root.right = trimBST(root.right, low, high);
return root;
}
}
【参考:修剪二叉搜索树 - 修剪二叉搜索树 - 力扣(LeetCode)】评论区
清晰明了
public TreeNode trimBST(TreeNode root, int low, int high) {
if (root == null) {
return null;
}
if (root.val < low) {
//因为是二叉搜索树,节点.left < 节点 < 节点.right
//节点数字比low小,就把左节点全部裁掉.
root = root.right;
//裁掉之后,继续看右节点的剪裁情况.剪裁后重新赋值给root.
root = trimBST(root, low, high);
} else if (root.val > high) {
//如果数字比high大,就把右节点全部裁掉.
root = root.left;
//裁掉之后,继续看左节点的剪裁情况
root = trimBST(root, low, high);
} else {
//如果数字在区间内,就去递归左右子节点.
root.left = trimBST(root.left, low, high);
root.right = trimBST(root.right, low, high);
}
return root;
}
剑指 Offer II 054. 所有大于等于节点的值之和
【参考:剑指 Offer II 054. 所有大于等于节点的值之和 - 力扣(LeetCode)】
【参考:所有大于等于节点的值之和 - 所有大于等于节点的值之和 - 力扣(LeetCode)】
- 方法一:
反序中序遍历
本题中要求我们将每个节点的值修改为原来的节点值加上所有大于它的节点值之和。这样我们只需要 反序中序遍历 该二叉搜索树,记录过程中的节点值之和,并不断更新当前遍历到的节点的节点值,即可得到题目要求的累加树。
class Solution {
int sum=0; // 全局变量
public TreeNode convertBST(TreeNode root) {
dfs(root);
return root;
}
// 反中序遍历
void dfs(TreeNode root){
if(root== null) return ;
dfs(root.right);
// 记录过程中的节点值之和
sum+=root.val; // 大于或等于,这里是等于
root.val=sum;
dfs(root.left);
}
}
面试题 04.06. 后继者
【参考:面试题 04.06. 后继者 - 力扣(LeetCode)】
class Solution {
ArrayList<TreeNode> list=new ArrayList<>();
public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
if(root==null) return null;
inorder(root);
int i=0;
for(;i<list.size();i++){
if(p==list.get(i)) break;
}
if(i==list.size()-1) return null;
return list.get(i+1);
}
// 中序遍历
void inorder(TreeNode root){
if(root==null) {
return;
}
inorder(root.left);
list.add(root);
inorder(root.right);
}
}
【参考:后继者 - 后继者 - 力扣(LeetCode)】
https://leetcode.cn/problems/successor-lcci/solution/hou-ji-zhe-by-leetcode-solution-6hgc/1564899
二分搜索的树版本
class Solution {
public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
int target = p.val;
TreeNode cur = root;
TreeNode ans = null;
while (cur != null){
if (cur.val > target){
ans = cur;
cur = cur.left;
}
else{
cur = cur.right;
}
}
return ans;
}
}
99. 恢复二叉搜索树
【参考:99. 恢复二叉搜索树 - 力扣(LeetCode)】
【参考:三种解法+详细图解 99. 恢复二叉搜索树 - 恢复二叉搜索树 - 力扣(LeetCode)】
得益于二叉搜索树左小右大的特性,一个重要性质是:二叉搜索树的中序遍历结果有序。
题目说有两个节点的值反了,也就是说中序遍历结果不再是有序的,有两个元素的位置反了。
那么我们找到破坏有序性的这两个元素,交换它们即可
class Solution {
ArrayList<TreeNode> list=new ArrayList<>();
public void recoverTree(TreeNode root) {
dfs(root);
TreeNode x=null,y=null;
for(int i=0;i<list.size()-1;i++){
// 逆序
if(list.get(i).val > list.get(i+1).val){
// 第一次逆序
if(x==null){
x=list.get(i);
}
y=list.get(i+1);// 多次直到最后一次逆序
}
}
// 交换
if(x!=null && y!=null){
int temp=x.val;
x.val=y.val;
y.val=temp;
}
}
public void dfs(TreeNode root){
if(root==null) return;
// 中序
dfs(root.left);
list.add(root);
dfs(root.right);
}
}