1.二叉搜索树特点:
- 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 任意节点的左、右子树也分别为二叉查找树;
- 没有键值相等的节点。
2.为什么要使用二叉搜索树:
选择二叉搜索树而不是那些基本的数据结构,是因为在二叉搜索树上进行查找非常快,为二叉搜索树添加或删除元素 也非常快。
3.(练习题)来判断以下树是否为二叉搜索树:
A:
B:
答案请写再评论区哦~
二叉树有哪些常见的操作呢:
- insert(key):向树中插入一个新得键。
- search(key):再树中找到一个值,如果存在返回true,不存在返回false.
- inOrderTraverse:中序遍历所有节点。
- preOrderTraverse:先序遍历所有节点。
- postOrderTraverse:后序遍历所有节点。
- min:返回树中最小值。
- max:返回树中最大值。
- remove(key):从树中移除某个键。
难点!!!!
4.接下来,我们用代码来实现上列常见的操作:
4.1:insert(key)
分析:首先第一步我们是要判断这个树存不存在,不存在的话,这个值就为root了。如果存在呢,就要开始比较大小。首先会去和根比较,如果比根小,那么我们需要先判断根的左边是否有值,如果没有值,root的left就可以直接指向left了。如果有值,那么我们继续比较,重复以上的操作,我们最终会找一个节点的left或者right为null,那么指向这个新值就好。
简易流程图:
代码:
function binarySearchTree() {
// 设置root为null
this.root = null;
// 创建节点的函数
function vNode(key) {
this.left = null;
this.right = null;
this.key = key;
}
// 插入
binarySearchTree.prototype.insert = function (key) {
// 1.创建节点
let newNode = new vNode(key);
// 判断根节点
if (this.root === null) {
this.root = newNode;
} else {
// 执行插入递归函数
this.insertNode(this.root, newNode);
}
};
// 插入的递归函数
binarySearchTree.prototype.insertNode = function (node, newNode) {
// 向左查找
if (node.key > newNode.key) {
// 判断节点是否为空
if (!node.left) {
node.left = newNode;
} else {
// 不为空继续递归比较
this.insertNode(node.left, newNode);
}
} else {
if (!node.right) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
}
};
}
测试代码:
let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(6);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
console.log(BSTTest);
结果:
符合我们的预期。
4.2:search(key)
分析:其实这个思路和上面插入非常像,插入我们是要确定这个值的位置在哪,而search则是找到这个值。所以我们再比较两个值的时候,加一个相等判断的特殊逻辑就可以实现search。
流程图:
绿色部分就是相比较插入新增的地方,其实原理是一样的。这里我们用while来实现以下。
代码:
binarySearchTree.prototype.search = function (key) {
// current 作为目标节点
let current = this.root;
if(!current){
return false;
}
if (current.key == key) {
return true;
}
// 当目标节点为null时,结束循环,查找结束,返回false.
while (current) {
// 目标节点和搜索节点相等时
if (key === current.key) {
return true;
}
if (key > current.key) {
current = current.right;
} else {
current = current.left;
}
}
return false;
};
测试代码:
let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(6);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
console.log(BSTTest);
console.log(BSTTest.search(1)); // false;
console.log(BSTTest.search(8)); // true;
console.log(BSTTest.search(7)); // true;
结果:
4.3:preOrderTraverse
遍历过程:
- 先访问根节点。
- 遍历其左子节点,左节点为空时,遍历其右子节点。
- 遍历其右子节点,右子节点为空时,遍历结束。
前序分析:
对于前序遍历而言,当我们访问到这个节点的值时,我们就要抛出。
示例图:红字为输出顺序
代码:
// 先序遍历
binarySearchTree.prototype.preOrderTraversal = function (node) {
this.preOrderTraversalNode(this.root);
};
// 遍历递归函数
binarySearchTree.prototype.preOrderTraversalNode = function (node) {
if (node !== null) {
// 遍历输出
console.log(node.key);
this.preOrderTraversalNode(node.left);
this.preOrderTraversalNode(node.right);
}
};
测试代码:
let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(6);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
console.log(BSTTest);
BSTTest.preOrderTraversal();
结果:
结合示例图与代码分析:(精华部分,看不懂的再学学递归函数)
回到上面的示例图来看,当我们节点2打印完成后,此时是不在执行递归函数了。你以为递归已经结束了吗,其实没有,我们会回到上面一个递归中,也就是节点为5的时候,这个时候,this.preOrderTraversalNode(node.left)这个函数我们已经执行完毕了,我们要开始执行下面这个递归函数了,也就是this.preOrderTraversalNode(node.right)。当我们打印3完成之后呢,5的这个节点两个递归函数都已经执行完毕了。这个时候我们又回到上层递归中节点为10的时候,这个时候,this.preOrderTraversalNode(node.left)这个函数我们已经执行完毕了,我们要开始执行下面这个递归函数了,也就是this.preOrderTraversalNode(node.right),然后继续遍历。
4.4inOrderTraverse(中序遍历所有节点)
遍历过程:
- 遍历其左子节点,左节点为空时,遍历其右子节点。
- 先访问根节点。
- 遍历其右子节点,右子节点为空时,遍历结束。
流程图:
中序分析:
当左子节点遍历完成后,再准备遍历右子节点之前,我们将其值抛出。对比前序遍历代码,我们只需要将打印的值放在两个递归函数之间就好了。
中序代码:
// 中序遍历
binarySearchTree.prototype.preOrderTraversal = function (node) {
this.preOrderTraversalNode(this.root);
};
// 遍历递归函数
binarySearchTree.prototype.preOrderTraversalNode = function (node) {
if (node !== null) {
this.preOrderTraversalNode(node.left);
// 遍历输出
console.log(node.key);
this.preOrderTraversalNode(node.right);
}
};
测试代码:
let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(6);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
console.log(BSTTest);
BSTTest.preOrderTraversal();
测试结果:
4.5:postOrderTraverse(后序遍历)
这个后续遍历就用一句话来总结吧:当一个节点左节点和右节点都遍历完成后,将其抛出。自然而然,我们只需要将打印的值放在两个递归函数之后就好了,那就不细说了。
4.6:min
分析:返回最小的值,其实我们一样都能看出只用找到最左边的值就好了。
binarySearchTree.prototype.min = function () {
let current = this.root;
while (current.left) {
current = current.left;
}
return current.key;
};
4.7max
分析:找到最右边的值就是最大值了。
binarySearchTree.prototype.max = function () {
let current = this.root;
while (current.right) {
current = current.right;
}
return current.key;
};
4.8:remove(key)
第一种情况:删除的节点没有子节点。
比如这些2,3,11,20.这些节点。
分析:
首先我们要找到这个节点,找到后判断其没有子节点。然后将其父节点的这个节点指向null.
流程图:
代码:
binarySearchTree.prototype.delete = function (key) {
let current = this.root;
let parent = null;
let isLeft = false;
if (key === current.key) {
this.root = null;
return;
}
while (current) {
// 找到这个节点
if (key === current.key) {
let isDeleteNode = current;
// 1.如果没有子节点
if (!current.left && !current.right) {
// 判断此节点是左边还是右边
if (isLeft) {
parent.left = null;
return true;
} else {
parent.right = null;
return true;
}
}
if (key > current.key) {
isLeft = false;
parent = current;
current = current.right;
} else {
isLeft = true;
parent = current;
current = current.left;
}
}
return false;
};
测试代码:
let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(6);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
BSTTest.delete(4);
console.log(BSTTest);
结果:
节点值为4已经成功删除了。
第二种情况:删除的节点只有一个根节点。
我们要删除节点5和15.
分析:
当我们要删除的节点有一个子节点时,只需要将这个删除节点的子节点替换到自己的位置即可。
代码:
binarySearchTree.prototype.delete = function (key) {
let current = this.root;
let parent = null;
let isLeft = false;
if (key === current.key) {
this.root = null;
return;
}
while (current) {
// 找到这个节点
if (key === current.key) {
let isDeleteNode = current;
// 1.如果没有子节点
if (!current.left && !current.right) {
// 判断此节点是左边还是右边
if (isLeft) {
parent.left = null;
return true;
} else {
parent.right = null;
return true;
}
}
// 新增 ===================================================
// 有一个子节点
else if (!current.left || !current.right) {
// 本左 子左 只写了一种情况还有三种先省略
if (current.left && isLeft) {
parent.left = current.left;
return true;
}
}
if (key > current.key) {
isLeft = false;
parent = current;
current = current.right;
} else {
isLeft = true;
parent = current;
current = current.left;
}
}
return false;
};
测试代码:
let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(6);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
BSTTest.delete(7);
console.log(BSTTest);
测试结果:
第三种情况:删除的节点有两个子节点。
我们要删除类似6,9,15这些节点.
分析:
当我们要删除这些节点的时候,其实是很麻烦的。所以我们要找到一些规律。我们要从这个节点的下面,找到一个节点来代替他的位置。那么这个节点就有一个特点,要么比删除的节点大一点,要么小一点,要无线接近与这个节点,我们就可以来替代被删除节点的位置了。这两个节点有个特别的名字。
- 前驱:比该节点小一点点的节点。
- 后继:比该节点大一点点的节点。 比如说我们要删除节点6,我们要找它的后继节点,看示例图一眼就能发现是节点7,此时我们就可以将节点7放到节点6的位置上去。这时又多了一种情况,当后继节点有右节点的时候,我们还需要把这个节点放到之前节点7的位置。
删除节点6后的示例图:
代码:
binarySearchTree.prototype.delete = function (key) {
let current = this.root;
let parent = null;
let isLeft = false;
if (key === current.key) {
this.root = null;
return;
}
while (current) {
// 找到这个节点
if (key === current.key) {
let isDeleteNode = current;
// 1.如果没有子节点
if (!current.left && !current.right) {
// 判断此节点是左边还是右边
if (isLeft) {
parent.left = null;
return true;
} else {
parent.right = null;
return true;
}
}
// 新增
// 有一个子节点
else if (!current.left || !current.right) {
// 本左 子左 只写了一种情况还有三种先省略
if (current.left && isLeft) {
parent.left = current.left;
return true;
}
}
// 新增的 ====================================================================
// 有两个子节点
// 找到前驱后继节点 替换删除的
else if (current.left && current.right) {
// 找后继
let houji = current;
// 后继父节点
houjiParent = current;
// 寻找后继节点
houji = houji.right;
while (houji.left) {
houjiParent = houji;
houji = houji.left;
}
//判断后继节点是否为要删除节点的right
// 如果是的话 就不需要找houjiParent这个节点了 houjiParent就等于删除的节点
if (isDeleteNode.right !== houji) {
houjiParent.left = houji.right;
houji.right = isDeleteNode.right;
}
// 判断后继节点有没有子节点
if (!houji.right) {
// 查找的是左节点
if (isLeft) {
parent.left = houji;
} else {
parent.right = houji;
}
}
// 有子节点
else {
// 查找的是左节点
if (isLeft) {
parent.left = houji;
} else {
parent.right = houji;
}
}
// 替换完成之后 后继节点的左节点要指向删除节点的左节点 最终完成替换
houji.left = current.left;
}
// ============================================================
if (key > current.key) {
isLeft = false;
parent = current;
current = current.right;
} else {
isLeft = true;
parent = current;
current = current.left;
}
}
return false;
};
测试代码:
let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(5);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
BSTTest.delete(5);
console.log(BSTTest);
执行结果:
以上就是整个删除的操作了,其实就分三种情况而已。最难的一种找到后继节点也挺简单的。实在不理解,找个本子画一画就能懂了。还不懂的留言哈,帮你解答。
感言
其实在学这个地方的时候,最惊艳我的地方就是遍历那一块。仅仅改变打印位置就是不一样的顺序。递归的魅力还是值得我去好好学学的。
彩蛋
最为前端er,其实二叉搜索树再开发时是基本用不上的。但是树的这个结构我们并不模式,尤其是dom树。作为两大框架Vue和React来讲,diff算法是一道大坎。期待我下一章把,手写diff算法。