简单介绍
二叉查找树(Binary Search Tree),又叫做二叉排序树或二叉搜索树,它有以下几个性质:
- 每一个节点上最多有两个子节点。
- 任意节点左子树上的值都小于当前节点。
- 任意节点右子树上的值都大于当前节点。
也就是说,对二叉搜索树中序遍历,就能得到一个有序的数列
代码实现
构建二叉搜索树
前置思考
构建二叉树,要考虑往树中插入节点的几种情况:
- 当这是一颗空树时,那只需要构建一个节点放入插入的值,并且这个节点就是树的根节点
- 如果这不是一颗空树,那需要从根节点一步步开始对比,找到这个节点应该插入的位置
- 【当前节点】的值比【对比节点】大,那【当前节点】在【对比节点】的右子树
- 【对比节点】的右节点为空,则【对比节点】的右节点 === 【当前节点】
- 【对比节点】的右节点不为空,则【对比节点】的右节点作为下一个对比节点,继续往下对比
- 【当前节点】的值比【对比节点】小,那【当前节点】在【对比节点】的左子树
- 【对比节点】的左节点为空,则【对比节点】的左节点 === 【当前节点】
- 【对比节点】的左节点不为空,则【对比节点】的左节点作为下一个对比节点,继续往下对比
- 【当前节点】的值比【对比节点】大,那【当前节点】在【对比节点】的右子树
将上诉情况转换成流程图如下:
graph TD
A[开始插入节点] --> B{是否为空树?}
B -->|是| C[创建新节点<br>作为根节点]
C --> D[插入完成]
B -->|否| E[当前对比节点 = 根节点]
E --> F{新节点值 <<br>对比节点值?}
F -->|是| G{对比节点的<br>左子节点为空?}
G -->|是| H[新节点 = 对比节点的<br>左子节点]
H --> D
G -->|否| I[对比节点 = 左子节点]
I --> F
F -->|否| J{对比节点的<br>右子节点为空?}
J -->|是| K[新节点 = 对比节点的<br>右子节点]
K --> D
J -->|否| L[对比节点 = 右子节点]
L --> F
模拟构建
模拟一下用【98,12,9,2,13,134,100,156】这个数组构建一颗二叉搜索树的过程:
第一步:插入98:此时是空树,创建节点为根节点
第二步:插入12:12<98, 且98的左节点为空,于是12成为98的左节点
第三步:插入9:9<98, 98的左节点不为空
继续拿12对比,9<12,且12的左节点为空,9成为12的左节点
第四步:插入2:2<98, 98的左节点不为空
继续拿12对比,2<12,且12的左节点不为空
继续拿9对比,2<9,且9的左节点为空,2成为9的左节点
第五步:插入13:13<98, 98的左节点不为空
继续拿12对比,13>12,且12的右节点为空,13成为12的右节点
第六步:插入134:134>98,且98的右节点为空,134成为98的右节点
第七步:插入100:100>98,98的右节点不为空
继续拿134对比,100<134,且134的左节点为空,100成为134的左节点
第八步:插入156:156>98,98的右节点不为空
继续拿134对比,156>134,且134的的右节点为空,156成为134的右节点
代码实现
function TreeNode(val){
this.val = val
this.left = this.right = null
}
function insertBST(root, val) {
if (root === null) {
return new TreeNode(val);
}
// 查找一个可以插入的空位置
if (val < root.value) {
// 如果左子树为空,则插入新节点
if (root.left === null) {
root.left = new TreeNode(val);
} else {
// 否则继续在左子树中查找
root.left = insertBST(root.left, val);
}
} else {
// 如果右子树为空,则插入新节点
if (root.right === null) {
root.right = new TreeNode(val);
} else {
// 否则继续在右子树中查找
root.right = insertBST(root.right, val);
}
}
// 返回更新后的根节点
return root;
}
从二叉搜索树中查找某个值
前置思考
查找比较简单,从根节点开始一步步往下找:
- 第一步判断目前的【查找节点】的值是否跟要找的值一致,是的话返回该节点
- 否则,进入第二步:判断【查找值】是大于还是小于【查找节点的值】
- 【查找值】>【查找节点的值】,递归,往【查找节点】的右节点去找
- 【查找值】<【查找节点的值】,递归,往【查找节点】的左节点去找
代码实现
// BST查找数据
function findBST(root,val){
if(root == null) return null;
if(val === root.val) return root
if(val < root.val){
return findBST(root.left,val)
}else{
return findBST(root.right,val)
}
}
从二叉搜索树中删除某个值
前置思考
针对待删除结点的子节点个数的不同,我们将它分为三种情况加以处理:
- 删除的节点没有子节点,直接删除,只需要将父节点中指向该节点的链接设置为 null 就可以了。
- 删除的节点只有一个子节点(只有左子节点或只有右子节点),只需要更新父节点指向待删除结点的子节点即可
- 删除的节点有两个子节点, 那删除之后,怎样才能维持住这颗二叉树是二叉搜索树才是关键。
- 关键是找前驱节点或者后继节点。(后面会介绍为啥这才是关键)
- 前驱节点指的是小于该键的最大键,后继节点指的是大于该键的最小键
- 通过中序遍历,在得到的序列中位于该点左侧的就是前驱节点,右侧的就是后驱节点。
模拟删除
删除的节点没有子节点:节点2
删除的节点只有一个子节点:节点9
删除的节点有两个子节点:节点12
到这里你是不是以为只需要找【被删除节点】的其中一个子节点顶替【被删除的节点】就可以了?
那结合下面的案例进行分析,需要考虑下面两个问题:
- 假设我们拿【被删除节点】的左节点顶替上去,那【被删除节点】的右节点需要放到哪里去?
- 假设我们拿【被删除节点】的右节点顶替上去,那【被删除节点】的左节点需要放到哪里去?
通过上面的分析,发现,最终还是要找前驱节点或者后继节点才能解决问题。
需要这么麻烦,无非就是【顶替节点】的子节点已经被占用了,无法把另一边插入进去充当子节点
那如果我们不拿【被删除节点】的去顶替,直接就找【被删除节点】的【前驱节点】或者【后继节点】顶替,就不会出现这个问题了?因为他们一定是【叶子节点】,子节点都是null,可以插入子节点。
可以发现,操作更便捷简单了:
- 把被删除节点的值替换成【前驱节点】或者【后继节点】的值
- 然后问题就转变成删除这个【前驱节点】或者【后继节点】,就将问题转变成了删除叶子节点了
代码实现
查找前驱节点(小于该节点的最大值):
function findPredecessor(root,targetValue){
if(!node || root.val === node.val) return null;
let current = root;
let predecessor = null;
// 找到目标值所处的节点是哪个
while (current !== null) {
// 如果当前节点的值大于目标值,则继续向左查找前驱节点
if (targetValue < current.value){
current = current.left;
}else if (targetValue > current.value){
// 当前节点可能是目标节点的前驱节点,先记录下来
predecessor = current;
// 继续往右子树查找
current = current.right;
}else {
// 找到了目标节点
break;
}
}
if (current === null) {
return null; // 没有找到目标节点
}
// 如果目标节点有左子树,找到左子树中的最右节点
if (current.left !== null) {
let leftSubtree = current.left;
while (leftSubtree.right !== null) {
leftSubtree = leftSubtree.right;
}
return leftSubtree;
}
// 如果目标节点没有左子树,返回之前记录的前驱节点
return predecessor;
}
查找后继节点(大于该节点的最小值):
// 查找后继结点: 大于targetValue的最小值
function findSuccessor(root, targetValue) {
if(!node || root.val === node.val) return null;
let current = root;
let successor = null;
while (current !== null) {
if(targetValue < current.value){ // 往左查找
// 当前节点可能是目标节点的后继节点
successor = current;
current = current.left;
}else if (targetValue > current.value) { // 继续往右
current = current.right;
}else{
// 找到目标节点
break;
}
}
if (current === null) {
return null; // 没有找到目标节点
}
// 如果目标节点有右子树,找到右子树中的最左节点
if (current.right !== null) {
let rightSubtree = current.right;
while (rightSubtree.left !== null) {
rightSubtree = rightSubtree.left;
}
return rightSubtree;
}
// 如果目标节点没有右子树,返回之前记录的后继节点
return successor;
}
删除操作:
//删除
function deleteBST(root,key){
if (root === null) {
return root;
}
let current = root;
let parent = null;
let isLeftChild = false;
// 查找要删除的节点
while (current !== null && current.val !== key) {
parent = current;
if (key < current.val) {
current = current.left;
isLeftChild = true;
} else {
current = current.right;
isLeftChild = false;
}
}
// 如果没有找到要删除的节点
if (current === null) {
return root;
}
// 情况1:要删除的节点是叶子节点
if (current.left === null && current.right === null) {
if (isLeftChild) {
parent.left = null;
} else {
parent.right = null;
}
}// 情况2:要删除的节点只有一个子节点
else if (current.right === null) { // 只有左子节点
if (isLeftChild) {
parent.left = current.left;
} else {
parent.right = current.left;
}
}else if (current.left === null) { // 只有右子节点
if (isLeftChild) {
parent.left = current.right;
} else {
parent.right = current.right;
}
}else{
// 情况3:要删除的节点有两个子节点
// 找到右子树的最小节点
const successor = findMin(current.right);
// 保存后继节点的值
const successorValue = successor.val;
// 递归删除后继节点
root = deleteBST(root,successorValue);
// 用后继节点的值替换当前节点的值
current.value = successorValue;
}
return root;
}
// 辅助函数:找到以给定节点为根的子树中的最小值节点
function findMin(node) {
while (node && node.left !== null) {
node = node.left;
}
return node;
}