二叉搜索树-BST

基本知识以及操作:

  1. 提到 BST 就要想到中序遍历:
    1. BST的中序遍历是有序的
  2. 性质:
    1. 左子树所有的值都**(严格)小于**根节点
    2. 右子树所有的值都**(严格)大于**根节点
  3. 几乎完美的数据结构,复杂度为树高
  4. 最左边的叶子结点是最小值,最右边的叶子结点是最大值

那么对于一个数据结构,无非做的事情就是增删查改。

那么就是分成两步:1. 找到要操作的位置 (其实就是)2. 操作

要找到位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private TreeNode find(TreeNode root, int val) {
if (root == null) return null;
if (root.val == val) {
// 找到节点,返回
return root;
} else if (root.val < val) {
// 利用BST的性质,此时说明需要继续去右边找:
return find(root.right, val);
} else if (root.val > val) {
// 利用BST的性质,此时说明需要继续去左边找:
return = find(root.left, val);
}
}
// 还有一种补充的写法。不过干的事情其实是一样的,只不过返回位置不同
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;
}

关于删除操作后的维护:

需要分三种情况:

root 是叶子结点

直接删除叶子结点 即 直接return null 让上一层接收:

BST_删除叶子结点

root 左右有一个子节点为空

当左子树不为空:

直接把左子树拼接上去即可,即返回左子树让上一层接收

删除节点_左子树不为空

删除节点_左子树不为空_拼接后

当右子树不为空:

直接把右子树拼接上去即可,即返回右子树让上一层接收

删除节点_右子树不为空

root 左右均不为空

左右子树均不为空

那么情况会变的复杂一些,这是由于要找到能够接替被删除节点位置的节点。这时,我们会有两种选择:

  • 找左子树的最大值
  • 找右子树的最小值

左子树最大值

98. 验证二叉搜索树

中序遍历而不是前序遍历的原因是:

中序遍历的特性。中序遍历按照“左-根-右”的顺序访问树中的节点。对于二叉搜索树,中序遍历会按照升序的方式访问树中的所有节点。因此,我们可以通过检查在中序遍历过程中每个相邻节点之间的值的大小关系来验证这棵树是否是有效的二叉搜索树。如果遍历的过程中发现某个节点的值小于等于前一个节点的值,那么这棵树就不是一个有效的二叉搜索树。

相反,前序遍历按照“根-左-右”的顺序访问树中的节点,这种顺序无法直接用来验证二叉搜索树的性质。因为前序遍历不保证访问的节点值是按照升序排列的,所以使用前序遍历会使得验证二叉搜索树的过程变得更加复杂。

总之,使用中序遍历是因为它可以直接利用二叉搜索树的性质,按照升序访问树中的节点,从而简化了验证二叉搜索树的过程。

三个方法:

  1. 使用中序遍历的有序性质 + 辅助数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
List<Integer> inOrderResult = new ArrayList<>();
public boolean isValidBST(TreeNode root) {
traverse(root);
for (int i = 0, j = i + 1; j < inOrderResult.size();) {
if (inOrderResult.get(j) <= inOrderResult.get(i)) {
return false;
}
i++;
j++;
}
return true;
}

private void traverse(TreeNode root) {
if (root == null) return;

traverse(root.left);
inOrderResult.add(root.val);
traverse(root.right);
}
  1. 借用一个非常小的值 继续使用中序遍历 PS:这里使用long minValue的原因是有可能存在int的最小值从而使得无法正确判断当左叶子结点特别小(Int MinValue)的情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
long longMin = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
return traverse(root);
}
private boolean traverse(TreeNode root) {
if (root == null) return true;

boolean left = traverse(root.left);
if (root.val <= longMin) {
return false;
}
longMin = root.val;
boolean right = traverse(root.right);
return left && right;
}
  1. 双指针,注意previousNode应该作为全局变量从而每一个更新都能被正确更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TreeNode previousNode = null;
public boolean isValidBST(TreeNode root) {
return traverse(root);
}
private boolean traverse(TreeNode root) {
if (root == null) return true;

boolean left = traverse(root.left);
if (previousNode != null && previousNode.val >= root.val) {
return false;
}
previousNode = root;
boolean right = traverse(root.right);
return left && right;
}

作为参数传递的方法:

之前previousNode没有正确地更新的原因是:Java中的参数传递采用的是值传递(pass-by-value)。虽然Java中的对象是引用类型,但实际上传递的是引用的副本。因此,在函数traverse内部修改previousNode的值(例如previousNode = root;)不会影响到调用方的previousNode

要解决这个问题,可以将previousNode的引用包装在一个可变的容器类中,例如一个单元素的数组。这样,在递归调用中,previousNode的更新将会影响到调用方。以下是修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TreeNode[] previousNode = new TreeNode[1];

public boolean isValidBST(TreeNode root) {
return traverse(root, previousNode);
}

private boolean traverse(TreeNode root, TreeNode[] previousNode) {
if (root == null) return true;

boolean left = traverse(root.left, previousNode);
if (previousNode[0] != null && previousNode[0].val >= root.val) {
return false;
}
previousNode[0] = root;
boolean right = traverse(root.right, previousNode);
return left && right;
}

701. 二叉搜索树中的插入操作

插入操作和二叉树的构造类型题非常类似详情见 二叉树之构造类问题

不难,详情看代码注释即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public TreeNode insertIntoBST(TreeNode root, int val) {
return insert(root, val);
}

private TreeNode insert(TreeNode root, int val) {
if (root == null) return new TreeNode(val); // 这里指找到了插入位置,那么构造一个TreeNode,返回上去,让上一层接收
if (root.val < val) {
// 利用BST的性质,此时说明需要继续去右边找:
root.right = insert(root.right, val);
} else if (root.val > val) {
// 利用BST的性质,此时说明需要继续去左边找:
root.left = insert(root.left, val);
}
return root; // 将更新好的节点不断向上返回。
}

450. 删除二叉搜索树中的节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public TreeNode deleteNode(TreeNode root, int key) {
return delete(root, key);
}

private TreeNode delete(TreeNode root, int key) {
if (root == null) return null;
if (root.val > key) {
// 要删除的key在当前节点的左边
root.left = delete(root.left, key);
} else if (root.val < key) {
// 要删除的key在当前节点的右边
root.right = delete(root.right, key);
} else {
// 找到了要删除的点:
if (root.left == null) {
return root.right;
} else if (root.right == null) {
return root.left;
} else {
TreeNode leftTreeMaxNode = findLeftTreeMaxNode(root.left);
root.left = removeLeftTreeMaxNode(root.left);
leftTreeMaxNode.left = root.left;
leftTreeMaxNode.right = root.right;
root = leftTreeMaxNode;
}
}
return root;
}

// 找到左子树中的最大的节点:
private TreeNode findLeftTreeMaxNode(TreeNode root) {
while (root.right != null) {
root = root.right;
}
return root;
}

// 像移除链表中的最后一个元素一样移除root
private TreeNode removeLeftTreeMaxNode(TreeNode root) {
if (root.right == null) {
// 当二叉树走到头时,返回当前节点的左树,这是因为左树中还有节点,如果直接返回null则相当于删除了整个左子树
return root.left;
}
root.right = removeLeftTreeMaxNode(root.right);
return root;
}

669. 修剪二叉搜索树

利用BST的性质 递归的处理左树和右树

1、**root.val < lo,这种情况下 root 节点本身和 root 的左子树全都是小于 lo 的,都需要被剪掉**。

2、**root.val > hi,这种情况下 root 节点本身和 root 的右子树全都是大于 hi 的,都需要被剪掉**。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public TreeNode trimBST(TreeNode root, int low, int high) {
TreeNode rootTrimmed = buildTree(root, low, high);
return rootTrimmed;
}

private TreeNode buildTree(TreeNode root, int low, int high) {
if (root == null) return null;
if (root.val < low) {
// 注意这里不能直接返回右子树: return root.right 我们需要继续递归的处理子树才行,因为我们要的是最终的结果而不是一次性的结果
return buildTree(root.right, low, high);
}
if (root.val > high) {
// 注意这里不能直接返回左子树: return root.left 我们需要继续递归的处理子树才行,因为我们要的是最终的结果而不是一次性的结果
return buildTree(root.left, low, high);
}
root.left = buildTree(root.left, low, high);
root.right = buildTree(root.right, low, high);
return root;
}
}

108. 将有序数组转换为二叉搜索树

和利用前序和后序数组构造二叉树题目类型非常相似,确定好左右区间,确定好根节点即可:

关于 前序和后序数组构造二叉树题目 见 二叉树之构造类问题

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return buildTree(nums, 0, nums.length - 1);
}
private TreeNode buildTree(int[] nums, int left, int right) {
if (left > right) return null;
int mid = (left + right) >> 1;
TreeNode root = new TreeNode(nums[mid]);
root.left = buildTree(nums, left, mid - 1);
root.right = buildTree(nums, mid + 1, right);
return root;
}
}

538. 把二叉搜索树转换为累加树

利用BST性质,降序输出,这个也是利用了双指针的性质:

可以想象成一个数组:

6 7 8

需要一个 累加 数组,此时用双指针 pre cur 指向 7 8

那么问题就好解决了,同理,是用双指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
int pre = 0;
public TreeNode convertBST(TreeNode root) {
// 使用双指针
traverse(root);
return root;
}

private void traverse(TreeNode root) {
if (root == null) return;
traverse(root.right); // 先遍历右树,使得其按照降序输出
root.val += pre;
pre = root.val;
traverse(root.left);
return;
}
}

第二种解法:使用sum作为外部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
int sum = 0;
public TreeNode convertBST(TreeNode root) {
// 中序遍历可以按照升序排列来输出,
// 那么我们只需要做一些修改使其按照降序排列来输出 并 同时维护一个外部变量sum即可
traverse(root);
return root;
}

private void traverse(TreeNode root) {
if (root == null) return;
traverse(root.right); // 先遍历右树,使得其按照降序输出
sum += root.val;
root.val = sum;
traverse(root.left);
return;
}
}