二叉搜索树 js
1、概念
二叉搜索树又称二叉查找树、二叉排序树。它满足如下性质
- 它的所有左子树上面的节点均比它小
- 它的所有右子树上面的节点均比它大
- 它的左右子树也满足二叉搜索树的性质
2、二叉搜索树的数据结构
function TreeNode(data) {
this.data = data; // 实际的数据
this.lchild = null; // 左节点
this.rchild = null; // 右节点
}
3、二叉搜索树的操作
3.1 插入
插入的操作就是当根节点为null的时候,直接创建一个新的节点赋值给根节点;否则和根节点比较大小,比根节点大去右子树插入,比根节点小去左子树插入,循环执行直到插入成功!
// root是根节点
function insert(data) {
if(root === null) {
root = new TreeNode(data);
return SUCCESS;
}
let node = root;
while(1) {
if(node.data === data) {
return NODE_DATA_EXSIT;
}
// 值比父节点小,插入到左边
if(node.data > data) {
if(node.lchild === null) {
node.lchild = new TreeNode(data);
return SUCCESS;
}
node = node.lchild;
continue;
}
// 值比父节点大,插入到右边
if(node.data < data) {
if(node.rchild === null) {
node.rchild = new TreeNode(data);
return SUCCESS;
}
node = node.rchild;
continue;
}
}
}
3.2 查找&构造
构造这里不做赘述,构造其实就是一个循环的插入过程
查找这里不做赘述,根据二叉搜索树的性质即可
3.3 删除
谈到删除我希望可以先明白一个概念:中序遍历
树是一个适合递归处理的结构(大部分场景如此),中序遍历其实就是:左子树,当前节点,右子树的顺序。其中左子树和右子树又是一个树的中序遍历,所以所中序遍历是一个特别适合用递归去操作的遍历实现
删除分为如下3种情况
- 删除的节点没有子节点
- 删除的节点只有左节点或者右节点
- 删除的节点既有左节点又有右节点
假如我们要删除的节点是根节点(只是举例)。其中第一种情况很好办,只需要将根节点的父节点指向设置为空即可(不是根节点的时候将其父节点的指向设置为空即可)。第二种情况当只有左节点或者只有右节点的时候,只需要将节点的指向重新指向仅存的唯一的节点即可。我么重点需要讨论的是当左右子节点均存在的时候应该如何实现
我们刚刚说了中序遍历的过程,我们需要了解的一个性质是 根节点的前驱是一定没有右节点的
假设前驱节点是存在右节点的,那么遍历的顺序就是前驱节点、前驱节点的右节点、根节点,这样是违背了前驱节点的定义(因为我们所谓的前驱节点就应该是根节点的前一个!)
可能上述概念比较绕弯,但是一定要理解,因为前驱节点最多只有一个左节点,这样我们将前驱节点移动到根节点的位置,在将前驱节点删除即可(删除回到了问题本身,其实也是个递归,只不过这个递归肯定是2次而已)
同理,也可以将后继节点覆盖到根节点上,与前驱节点同理,这里不做赘述。
// data是要待删除的数据
function del(data) {
let prev = null; // 记录查找到的节点的前驱节点
let node = root;
while(node) {
if(node.data === data) { // 找到了要删除的节点
break;
}
// 去左子树找
if(node.data > data) {
prev = node;
node = node.lchild;
continue;
}
// 去右子树找
if(node.data < data) {
prev = node;
node = node.rchild;
continue;
}
}
// 没有找到待删除的节点
if(node == null) {
return DELETE_DATA_NOT_EXSIT;
}
// 删除的节点既没有左节点也没有右节点
if(!node.lchild && !node.rchild) {
if(prev == null) { // 根节点,这时候就把这棵树删了。。。
root = null;
return SUCCESS;
}
prev.lchild === node && (prev.lchild = null); // 是左节点就删除左节点
prev.rchild === node && (prev.rchild = null); // 是右节点就删除右节点
return SUCCESS;
}
// 删除的只有一个节点
if(node.lchild && !node.rchild) { // 有左节点
if(prev == null) {
root = node.lchild;
return SUCCESS;
}
prev.lchild === node && (prev.lchild = node.lchild);
prev.rchild === node && (prev.rchild = node.lchild);
return SUCCESS;
}
if(!node.lchild && node.rchild) { // 有右节点
if(prev == null) {
root = node.rchild;
return SUCCESS;
}
prev.lchild === node && (prev.lchild = node.rchild);
prev.rchild === node && (prev.rchild = node.rchild);
return SUCCESS;
}
// 接下来的情况就是既有左节点又有右节点了
let prev2 = node;
let node2 = node.rchild;
while(node2.rchild) { // 可以思考下这里为什么一直是rchild,如果要是取后继节点替换得话,这里还是rchild嘛,为什么?
prev2 = node2;
node2 = node2.rchild;
}
// 这个时候node2就是要替换得节点,先把数据替换了
node.data = node2.data; // 这里执行完毕代表已经把输入得data已经删除了,那么替换得node2也要删除
// prev.rchild是因为他肯定是右节点
// node2.lchild是如果删除的node2节点有左节点就把左节点给赋值到node2的父节点对应的节点(就是上一行的右节点)
// 没有的话就是null赋值,所以其实就是node.lchild(没有的左节点node2.lchild就是null)
prev.rchild = node2.lchild;
return SUCCESS;
}
其实主要考虑到删除节点的各种情况就好了,然后分情况处理即可
上述代码是前驱节点替换,爱动手的也可以试试后继节点替换~