二叉搜索树
二叉搜索树(Binary Search Tree,简写 BST)
BST 特性
1、对于 BST 的每一个节点 node
,左子树节点的值都比 node
的值要小,右子树节点的值都比 node
的值大。
2、对于 BST 的每一个节点 node
,它的左侧子树和右侧子树都是 BST。
从做算法提的角度来看 BST,除了它的定义,还有一个重要的性质:BST 的中序遍历结果是有序的(升序)
也就是说,如果输入一棵 BST,以下代码可以将 BST 中每个节点的值升序打印出来:
void traverse(TreeNode root) {
if(root == null) {
return;
}
traverse(root.left);
// 中序遍历代码位置
print(root.val);
traverse(root.right);
}
第一题:寻找第 K 小的元素
我们根据上面的性质,我们可以进行一下中序遍历,就可以直接找出第 K 小的元素
int res = 0;
int index = 0;
int kthSmallest(TreeNode root, int k) {
traverse(root, k);
return res;
}
void traverse(TreeNode root, int k) {
if (root == null) {
return;
}
traverse(root.left, k);
index++;
if(index == k) {
res = root.val;
return;
}
traverse(root.right, k);
}
第二题:转化累加树
题目要求:给出 搜索树的根节点,该树的节点值各不相同,请你将其转化为累加树,使每个节点 node
的值等于原树中大于或等于 node.val
的值之和。
比如图中的节点 5,转化成累加树的话,比 5 大的节点有 6,7,8,加上 5 本身,所以累加树上这个节点的值应该是 5+6+7+8=26。
这个问题并不复杂,就是累加计算大于该节点的所有数字之和。
TreeNode convertBST(TreeNode root) {
build(root);
return root;
}
int sum = 0;
public void build(TreeNode root) {
if (root == null) {
return;
}
build(root.right);
sum += root.val;
root.val = sum;
build(root.left);
}
这道题目,核心还是 BST 的中序遍历特性,只不过我们修改了递归顺序,降序遍历 BST 的元素值,从而契合题目累加树的要求。
有了 BST 的这种特性,就可以在二叉树中做类似二分搜索的操作,搜索一个原树的效率很高。
一棵合法的二叉树:
对于 BST 相关的问题,可能会经常看到类似下面这样的代码逻辑
void BST(TreeNode root, int target) {
if(root.val == target) {
// 找到目标,做点什么
}
if(root.val < target) {
BST(root.right, target);
}
if(root.val > target) {
BST(root.left, target);
}
}
这个代码框架其实和二叉树的遍历框架差不多,无非就是利用了 BST 左小右大的特征而已。
第三题:判断 BST 的合法性
按照我们的思路:节点的左小右大的特征写代码
boolean isValidBST(TreeNode root) {
if(root == null) {
return true;
}
if(root.left != null && root.val <= root.left.val) {
return false;
}
if(root.right != null && root.val >= root.right.val) {
return false;
}
return isValidBST(root.left) && isValidBST(root.right);
}
上面的代码看似没有问题,实际上是错误的,看下图中的 6 和 10
出现问题的原因在于,对于每个节点 root
,代码之检查了它的左右孩子节点是否符合左小右大的原则;但是根据BST的定义,root
的整个左子树都要小于 root.val
,整个右子树都要大于 root.val
。
正确代码
boolean isValidBST(TreeNode root) {
return isValidBST(root, null, null);
}
boolean isValidBST(TreeNode root, TreeNode min, TreeNode max) {
if(root == null) {
return true;
}
// 若 root.val 不符合 max 和 min 的限定,说明是不合法 BST
if(min != null && root.val <= min.val) {
return false;
}
if(max != null && root.val >= max.val) {
return false;
}
// 限定左子树的最大值是 root.val, 右子树的最小值是 root.val
return isValidBST(root.left, min, root) &&
isvalidBST(root.right, root, max);
}
我们通过辅助函数,增加函数参数列表,在参数中携带额外信息,将这种约束传递给子树的所有节点,这也是二叉树算法的一个技巧
第四题:在 BST 中搜索元素
如果在一棵普通的二叉树中,可以这样写代码
TreeNode searchBST(TreeNode root, int target) {
if(root == null) {
return null;
}
if(root.val == target) {
return root;
}
searchBST(root.left, target);
searchBST(root.right, target);
return left != null ? left : right
}
这样写的话就穷举了所有节点,适用于所有普通二叉树
利用 BST 特性,加一些判断,在 BST 中进行搜索元素
TreeNode searchBST(TreeNode root, int target) {
if(root == null) {
return null;
}
// 去右子树所搜
if(root.val < target) {
searchBST(root.right, target);
}
if(root.val > target) {
SearchBST(right.left, target);
}
return root;
}
第五题:在 BST 中插入一个数
TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val);
}
if (root.val > val) {
return insertIntoBST(root.left, val);
}
if (root.val < val) {
return insertIntoBST(root.right, val);
}
return root;
}
第六题:在 BST 中删除一个数
首先是找到目标数字,然后进行删除。
代码框架
TreeNode deleteNode(TreeNode root, int key) {
if (root.val == key) {
// 找到位置,进行删除
}
if (root.val < key) {
return deleteNode(root.right, key);
}
if (root.val > key) {
return deleteNode(root.left, key);
}
return root;
}
找到目标节点了,比如说是 A
,如果这个节点没有左右子节点,那么直接删除即可,如果这个节点有一个子结点,那么将子节点来代替自己的位置即可,如果有两个,就需要进行判断处理了。
情况1:A
恰好是末端节点,两个子节点都为空,那么它可以直接被删掉
if (root.left == null && root.right == null) {
return null;
}
情况2:A
只有一个非空子节点,那么它要让这个孩子接替自己的位置
if(root.left == null) {
return root.right;
}
if(root.right == null) {
return root.left;
}
情况3:A
有两个子节点,为了不破坏 BST 的性质,A
必须找到左子树中最大的哪个节点,或者右子树中最小的哪个节点来代替自己。
if(root.left != null && root.right != null) {
// 找到右子树的最小节点
TreeNode minNode = getMin(root.right);
// 把 root 改成 minNode
root.val = minNode.val;
// 转而去删除 minNode
root.right = deleteNode(root.right, minNode.val);
}
if(root.left != null && root.right != null) {
// 找到左子树的最大节点
TreeNode maxNode = getMax(root.left);
// 把 root 改成 maxNode
root.val = maxNode.val;
// 转而去删除 maxNode
root.left = deleteNode(root.left, maxNode.val);
}
三种情况,完整代码
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.left = root.right;
root = minNode;
} else if(root.val > key) {
return deleteNode(root.left, key);
} else if(root.val < key) {
return deleteNode(root.right, key);
}
return root;
}
TreeNode getMin(TreeNode node) {
while(node.left != null) {
node = node.left;
}
return node;
}