这部分主要记录搜索树的题型和写题方法。
1、二叉搜索树的属性
2、二叉搜索树的修改与构造
目录
一、二叉搜索树的属性
1.2、是否是二叉搜索树
对应题目98. 验证二叉搜索树 - 力扣(LeetCode)
思路一
搜索树的特点就是根节点的值大于左子树的所有值,小于右子树的所有值。
即中序有序。
那么判断一颗树是否是二叉搜索树就比较好想了,直接中序遍历树,看看是否是递增有序的。
遍历过程中不断保存最小值,一旦当前节点的值小于最小值,说明中序不有序,则一定不是搜索树。
代码如下
class Solution {
long min = Long.MIN_VALUE; // 一定要用long类型
public boolean isValidBST(TreeNode root) {
return isValidBST1(root);
}
public boolean isValidBST1(TreeNode root) {
if (root == null) return true;
boolean left = isValidBST1(root.left);
if (root.val > min) min = root.val;
else return false;
boolean right = isValidBST1(root.right);
return left && right;
}
}
思路二
第二种思路是使用前序遍历,在遍历左右子树的过程中不断更新最大值和最小值的边界,一旦当前节点越界,则说明不是二叉搜索树。
代码如下
class Solution {
public boolean isValidBST(TreeNode root) {
long min = Long.MIN_VALUE; // 一定要用long类型
long max = Long.MAX_VALUE;
return isValidBST(root, min, max);
}
public boolean isValidBST(TreeNode root, long min, long max) {
if (root == null) return true;
// 越界判断 要加= ,因为重复也是不行的
if (root.val <= min || root.val >= max) return false;
// 向左遍历就更新右边界,向右遍历就更新左边界
// 左右子树都要满足条件 才能是搜索树
return isValidBST(root.left, min, root.val) && isValidBST(root.right, root.val, max);
}
}
1.3、二叉搜索树的搜索
对应题目700. 二叉搜索树中的搜索 - 力扣(LeetCode)
二叉搜索树的搜索因为其有序性,会比一般二叉树遍历要更高效。
当目标值比当前节点值小,说明在左子树,就往左遍历;当目标值比当前节点值大,说明在右子树,就往右遍历。
这样可以一定程度上剪枝,不用遍历整棵树。
代码如下
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
if (root == null) return root;
if (root.val == val) return root;
if (root.val > val) return searchBST(root.left, val); // 向左
if (root.val < val) return searchBST(root.right, val); // 向右
return null;
}
}
1.4、二叉搜索树的最小绝对差
对应题目530. 二叉搜索树的最小绝对差 - 力扣(LeetCode)
这道题可以直接中序遍历树,得到的数组是有序的,直接在数组上找最小绝对差。
但最佳的办法是直接在树上进行搜索,思路与在数组上操作是一样的。
每次遍历需要保存前一个节点来进行计算。
保存前一个节点的方法也很简单,直接在中处理逻辑完之后保存当前节点就行。
本题的中处理逻辑是更新最小的绝对差,因为最小绝对差一定是出现在中序遍历时相邻的节点上
if (pre != null) diff = Math.min(diff, root.val - pre.val);
pre = root; // 保存上一个节点
代码如下
可以保存前一个节点,也可以直接保存前一个节点的数值。
class Solution {
int diff = Integer.MAX_VALUE; // 记录最小差
//int preN = -1; // 记录上一节点的数值
TreeNode pre = null; // 记录上一节点的指针
public int getMinimumDifference(TreeNode root) {
traversal(root);
return diff;
}
public void traversal(TreeNode root) {
if (root == null) return;
// 中序遍历
traversal(root.left);
// if (preN != -1 && root.val - preN < diff) diff = root.val - preN;
// preN = root.val;
if (pre != null) diff = Math.min(diff, root.val - pre.val);
pre = root;
traversal(root.right);
}
}
1.5、二叉搜索树的众数
对应题目501. 二叉搜索树中的众数 - 力扣(LeetCode)
这道题,有一个比较直接的做法,就是用一个map保存各节点数值的出现次数,然后根据频次排序,最后取首个或多个数。
代码如下
class Solution {
HashMap<Integer, Integer> map = new HashMap(); // 保存频次
public int[] findMode(TreeNode root) {
traversal(root); // 记录各数的频率
Set<Map.Entry<Integer,Integer>> entries = map.entrySet();
// 使用优先队列排序
PriorityQueue<Map.Entry<Integer, Integer>> que = new PriorityQueue<>((o1, o2) -> o2.getValue() - o1.getValue());
for (Map.Entry<Integer, Integer> e : entries) {
que.add(e);
}
ArrayList<Integer> list = new ArrayList(); // 保存结果
Map.Entry<Integer, Integer> e1 = que.remove(); // 最多的那个
list.add(e1.getKey());
// 看看后面还有没有次数一样多的
while (!que.isEmpty() && que.peek().getValue() == e1.getValue()) {
list.add(que.remove().getKey());
}
int[] res = new int[list.size()];
for (int i = 0; i < list.size(); i++) res[i] = list.get(i);
return res;
}
public void traversal(TreeNode root) {
if (root == null) return;
traversal(root.left);
map.put(root.val, map.getOrDefault(root.val, 0) + 1);
traversal(root.right);
}
}
但也可以直接在遍历树的时候进行统计。
因为重复的数字只会出现在中序遍历时的相邻节点上,因此使用中序遍历,保存一个全局最大频率maxCount,并且在遍历时不断计数count,同时需要保存前一个节点的指针pre。
一旦当前节点值与上一个节点的值相同,进行计数count++;否则就重置为1,意味着进入下一个数的计数。
那么什么时候更新maxCount?当count > maxCount时,意味着要更新maxCount了,同时要清空结果集,加入当前的节点数值,因为前面添加过的众数已经作废。
如果有多个数的频次重复,即count = maxCount,那么就把当前节点数值加入结果集里。
代码如下
class Solution {
int count = 0;
int maxCount = 0;
TreeNode pre = null;
ArrayList<Integer> resList = new ArrayList();
public int[] findMode(TreeNode root) {
traversal(root);
// 转成数组
int[] res = new int[resList.size()];
for (int i = 0; i < resList.size(); i++) res[i] = resList.get(i);
return res;
}
public void traversal(TreeNode root) {
if (root == null) return;
traversal(root.left);
// 中处理逻辑
if (pre != null && root.val != pre.val) count = 1;
else count++;
// 加入结果集时机
// 若当前count比maxCount大 或等于maxCount 说明count肯定是最大的 要加入了
// 同时如果count比maxCount大 说明之前加如结果集的都是作废的 要清空再加入
// 相等的话就不用清空 继续加
if (count > maxCount) {
maxCount = count;
resList.clear();
resList.add(root.val);
}else if (count == maxCount) {
resList.add(root.val);
}
pre = root; // 记录前一个节点
traversal(root.right);
}
}
二、二叉搜索树的修改与构造
2.2、二叉搜索树的插入
对应题目701. 二叉搜索树中的插入操作 - 力扣(LeetCode)
搜索树在插入的时候可能是要对树进行变换的,因为要维持树的有序性。
但是其实完全可以不用变换。
二叉树前序搜索时,会根据数值大小来判断遍历左子树还是右子树以达到剪枝的目的。
那么一旦遇到空节点,直接将新节点插入就可以了。
这样是一定可以保证新的树是有序的,那么就不需要对树进行重构变换了。
代码如下
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) return new TreeNode(val); // 遇到空的直接插入就完事
if (val > root.val) root.right = insertIntoBST(root.right, val);
else root.left = insertIntoBST(root.left, val);
return root;
}
}
2.3、二叉搜索树的删除
对应题目450. 删除二叉搜索树中的节点 - 力扣(LeetCode)
删除相对添加来说就会难一些,因为要考虑多种情况,不同的情况可能是要进行树变换的。
那么当遇到要删除的节点时,想要删除该节点,需要考虑以下三种情况:
1、该节点左右孩子都为空,那么直接删除该节点就行。
2、该节点左右孩子只有一个为空,那么就以不为空的孩子作为根节点返回。
3、该节点左右孩子都不为空,将左孩子作为右孩子的最左边的叶子节点的左节点,将右孩子返回。
第三种情况可能有点绕,其实分开看,右孩子的最左边的叶子节点,它一定是以右孩子为根节点的树中,数值最小的,那么将左孩子作为它的左节点,是一定可以保证有序的,因为左孩子小于右孩子为根的所有节点。
那么三种情况分析完了,这道题也差不多可以解出来。
本题使用前序最直观。
代码如下
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null) return root;
if (root.val == key) { // 找到要删除的节点
// 三种情况 都为空 其中一个为空 都不为空
if (root.left == null) return root.right;
else if (root.right == null) return root.left;
else { // 左右孩子都不为空
TreeNode node = traversal(root.right); // 获取右孩子最左下的值
node.left = root.left; // 把root的左孩子放到右孩子的最左下边
return root.right;
}
}else if (root.val < key) root.right = deleteNode(root.right, key);
else root.left = deleteNode(root.left, key);
return root;
}
// 找最最左下角的节点 不一定最底层 一定最左
public TreeNode traversal(TreeNode node) {
if (node.left == null) return node;
return traversal(node.left);
}
}
2.4、二叉搜索树的修剪
对应题目669. 修剪二叉搜索树 - 力扣(LeetCode)
修剪其实也就是删除节点,可以直接套用LC_450删除节点的思路,不过一定要使用后序自底向上遍历,如果使用前序,删除根节点时会直接返回,导致漏删一些节点。
代码如下
class Solution {
public TreeNode trimBST(TreeNode root, int low, int high) {
if (root == null) return root;
// 一定要自底向上 如果要删根节点 可能会漏删
root.right = trimBST(root.right, low, high);
root.left = trimBST(root.left, low, high);
if (root.val < low || root.val > high) { // 找到要删除的节点
// 四种情况 都为空 其中一个为空 都不为空
if (root.left == null) return root.right;
else if (root.right == null) return root.left;
else { // 左右孩子都不为空
TreeNode node = traversal(root.right); // 获取右孩子最左下的值
node.left = root.left; // 把root的左孩子放到右孩子的最左下边
return root.right;
}
}
return root;
}
// 找最最左下角的节点 不一定最底层 一定最左
public TreeNode traversal(TreeNode node) {
if (node.left == null) return node;
return traversal(node.left);
}
}
但其实也可以换种思路,因为不是只删除一个节点。
一旦当前节点需要删除,可以直接返回删除后的左孩子或右孩子。
如果root.val > high,那么就向左递归,返回满足条件的左孩子。
如果root.val < low,那么就向右递归,返回满足条件的右孩子。
如果root.val在范围内,那么就递归获取左右子树。
代码如下
class Solution {
public TreeNode trimBST(TreeNode root, int low, int high) {
if(root == null) return root;
// 换种思路
// 如果比low小 则直接往右找 比high大 往左找
// 这样就相当于删掉当前节点了
if (root.val < low) return trimBST(root.right, low, high);
else if (root.val > high) return trimBST(root.left, low, high);
root.left = trimBST(root.left, low, high);
root.right = trimBST(root.right, low, high);
return root;
}
}
2.5、二叉搜索树的构造
对应题目108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode)
可以回顾根据数组构造二叉树的过程,
题目为:106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)
记录的笔记:(116条消息) 代码随想录 二叉树模块小结(1)_散谎的博客-CSDN博客
大致的思路就是找分割点,分割左右数组,再递归创建左右子树。
本大题不需要担心是否平衡的问题,如果从数组中间取索引作为分割点,那么最后的树一定是平衡的。
代码如下
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return traversal(nums, 0, nums.length);
}
public TreeNode traversal(int[] nums, int start, int end) {
if (start >= end) return null;
int index = (end - start) / 2 + start; // 分割边界 要加上start 因为是找下标 很关键
TreeNode root = new TreeNode(nums[index]); // 根节点
if (end - start == 1) return root; // 根节点 直接返回
// 分割左右子数组 左闭右开
int leftStart = start;
int leftEnd = index; // 选过了该节点
int rightStart = index + 1;
int rightEnd = end;
root.left = traversal(nums, leftStart, leftEnd);
root.right = traversal(nums, rightStart, rightEnd);
return root;
}
}