二叉搜索树
文章代码及一些解释性词语来自:https://www.bilibili.com/video/BV1x7411L7Q7?p=73,视频中少了一些内容,这里我都做了一些我自己的解释
满足二叉树搜索树的条件:
- 非空左子树中任意一个节点的值都要小于根节点的值
- 右子节点中任意一个节点的值都要大于根节点的值
- 左右子树本身也都是二叉搜索树
如:
在这个二叉树中,因为6作为5的左子树,不能比5要大,所以6应该出现在5的右子树中
在这个二叉树中,则才是正确的二叉搜索树。
有的人会问,这两个不是一样的吗,注意:在二叉树中,严格区分左右节点,在图中就应该表示为向右的箭头指向右节点,反之则指向左节点。
3.1 了解二叉树的遍历
下面的遍历都已下图作为参考:
- 先序遍历
我用一句话概括就是:根左右 。
什么是根左右呢?即:首先从全局看,是有一个二叉树的,但是对于7这个节点来看,7可以看做是其子节点的根节点,以此类推。那么根左右的意思就是,每次遍历,都先遍历根节点、其次是左子节点、最后是右子节点。那么对于上面图中的例子,从大到小看,首先根是11
遍历顺序:11、
之后就是遍历左子节点。由于左子节点也是一个二叉树,他自身就是根节点,所以第二个遍历到的应该是7
遍历顺序:11、7
依次类推:11、7、5、3
之后由于三到了叶子节点了,遍历完之后,则需要回退到又5组成的二叉树,由于5的左节点都遍历完了,下面就需要遍历5的右子节点,由于没有,所以已经回退到7组成的二叉树,你要知道,每次回退上来,都代表着左子节点已经遍历完了,然后就是遍历右子节点,由于右子节点9她自身也是一个二叉树,所以他也要遵循根左右的顺序继续遍历,结果如下:
遍历顺序:11、7、5、3、9、8、10
到这里为止,全局的左子树已经遍历完了,下面就需要遍历全局的右子树了,操作还是依据根左右的方式,最终的结果如下:
遍历结果:11, 7, 5, 3, 9, 8, 10, 15, 13, 12, 14, 20, 18, 19, 25
- 中序遍历
我用一句话概括就是:左根右 。
方法跟上面是一样的,区别只在于先遍历左还是根还是右的区别,结果如下:
3, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 18, 19, 20, 25
在中序遍历中,你会发现,对于二叉搜索树而言 ,中序遍历就是升序排序
- 后序遍历
我用一句话概括就是:左右根 。
结果为:3, 5, 8, 10, 9, 7, 12, 14, 13, 19, 18, 25, 20, 15, 11
- 层次遍历
层次遍历没什么好说的,就是一层层的从左往右遍历,结果为:
11, 7, 15, 5, 9, 13, 20, 3, 8, 10, 12, 14, 18, 25, 19
了解了这些遍历之后,我们用代码来写一个 二叉搜索树 。
3.2 编写二叉搜索树
- 写出一个架构:
function BinarySearchTree() {
// 用于创建一个新的节点,每个节点都有一个指向左子节点的指针和一个指向右子节点的指针,还有一个保存自己值的key
function Node(key) {
this.key = key
this.left = null
this.right = null
}
// 指向根节点
this.root = null
}
- 定义一个插入的方法,传入一个key,在内部创建一个节点,并添加到指定位置
// 递归方法
function insertNode(node, newNode) {
if (newNode.key < node.key) {
if (!node.left) {
node.left = newNode
} else {
insertNode(node.left, newNode)
}
} else {
if (!node.right) {
node.right = newNode
} else {
insertNode(node.right, newNode)
}
}
}
BinarySearchTree.prototype.insert = function (key) {
// 1.创建一个新节点
let newNode = new Node(key)
// 2.判断根节点是否有元素,如果没有则表示这是插入的第一个元素,反之则需要找到指定位置插入
if (!this.root) {
this.root = newNode
} else {
let current = this.root
// 方式二:调用一个递归函数
insertNode(current, newNode)
}
}
- 根据key,查找到指定的元素,有则返回true,没有则返回false
BinarySearchTree.prototype.search = function (key) {
if (typeof key !== "number") return false
let current = this.root
while (current) {
if (key < current.key) {
current = current.left
} else if (key > current.key) {
current = current.right
} else {
return true
}
}
return false
}
- 先序、中序、后序遍历
// 调用的递归方法
/**
* 递归遍历先、中、后序的函数
* node:需要进行某种遍历的树结构
* type:按某种方式进行遍历。如:0 => 先序遍历,1 => 中序遍历,2 => 后序遍历
*/
function ergodic(node, type = 1) {
if (!node) return
let arr = [], left = [], right = [], root = []
root.push(node.key)
if (node.left) {
left.push(...ergodic(node.left, type))
}
if (node.right) {
right.push(...ergodic(node.right, type))
}
if (type === 0) {
arr.push(...root, ...left, ...right)
} else if (type === 2) {
arr.push(...left, ...right, ...root)
} else if (type === 1) {
arr.push(...left, ...root, ...right)
}
return arr
}
// 中序遍历
BinarySearchTree.prototype.inOrderTraverse = function () {
return ergodic(this.root)
}
// 先序遍历
BinarySearchTree.prototype.preOrderTraverse = function () {
return ergodic(this.root, 0)
}
// 后序遍历
BinarySearchTree.prototype.postOrderTraverse = function () {
return ergodic(this.root, 2)
}
- 查找最小值
BinarySearchTree.prototype.min = function () {
// 方式一:
// return ergodic(this.root)[0]
// 方式二:
let current = this.root
while (current.left) {
current = current.left
}
return current.key
}
- 查找最大值
BinarySearchTree.prototype.max = function () {
// 方式一:
/*let arr = ergodic(this.root)
return arr[arr.length - 1]*/
// 方式二:
let current = this.root
while (current.right) {
current = current.right
}
return current.key
}
- 根据key删除某一个节点
删除操作有点复杂,我们细细说:
首先删除有三种情况:
- 删除的节点刚好是叶子节点,则可以直接删除即可,伪代码如下:
// 找到需要被删除的节点的父节点,假如要删除的key为3,那么则需要找到5即可,通过判断5的left或者right指向的哪个节点的key为3,则删除即可
- 删除的节点有一个子节点,则需要将被删除节点的父节点的left或者right指向被删除节点的子节点
// 比如要删除5这个节点,则需要将7的left指向5的left
// 如果3是在5的右子树,则需要将7的left指向5的right
// 反之,如果是在7的右子树中删除一个只有一个子节点的元素,那就是改变的7的right即可
- 删除的节点有两个子节点
// 这种情况就比较复杂了,首先,假如我们需要删除一个7这个元素,则面对他的左右两个子树,需要选择一个合适的节点替换到7这个位置,同时还要保证删除后的结果还是一个二叉搜索树。假如我们在7的左子树中找,即在3和5中选一个替换,则只能选择5。如果用右子树中找一个替换,则在8/9/10中,只能是8
// 如果替换15呢,则我们如果在左子树中找,只能是14;而在右子树中,只能是18
---------------------------
// 下面我们来找一下规律,如果你仔细发现的话,你会看出来,其实就是如果在左子树中找的话,找的绝对是里面最大的值,而在右子树中找的话,找的绝对是里面最小的值。
----------------------------
// 其次,找到被替换的节点之后,还需要处理的就是该节点他的子节点要怎么处理?
// 其实很简单,如果是在右子节点中找的话,因为被替换的节点是右子节点树中最大的值,这代表则,这个被替换的元素肯定right指向为null,因为right指向的节点是要比他要大的,既然他已经是最大的了,就说明他的right指向为null,根据这个判断条件,我们就能知道被替换的元素肯定是一个只有一个子节点的元素。在左节点树中就是相反的,被删除的节点的left肯定指向为null,因为他自己已经是最小的了。所以我们只需要找到待替换节点的父节点,让该父节点指向待替换节点的left或者right就可以了。具体是left还是right,则看你是让左子树中节点替换还是右子树中节点替换。
说了那么多,上代码:
BinarySearchTree.prototype.remove = function (key) {
if (typeof key !== "number") return false
let current = this.root
let parent = null
// 在循环中找到需要被删除的元素,保存在current中,parent为current的父节点,注意,如果key直接等于根节点的话,parent=null
while (current.key !== key) {
parent = current
if (key < current.key) {
current = current.left
} else if (key > current.key) {
current = current.right
}
if (!current) return false
}
// 下面就是找到了节点需要删除的情况
// 1:删除的节点是叶子节点,则直接使用parent删除
if (!current.left && !current.right) {
// 1.1:如果该节点是根节点,同时也是叶子节点
if (current === this.root) {
this.root = null
}
// 1.2:如果该节点是parent的左孩子节点
else if (parent.left.key === key) {
parent.left = null
}
// 1.3:如果该节点是parent的右孩子节点
else if (parent.right.key === key) {
parent.right = null
}
}
// 2:删除的节点有一个子节点
else if (!current.left || !current.right) {
if (parent.left.key === key) {
parent.left = current.left? current.right : current.left
}
else if (parent.right.key === key) {
parent.right = current.left? current.right : current.left
}
}
// 3:删除的节点有两个子节点
else if (current.left && current.right) {
// 3.1:如果要删除的节点是根节点,同时也有两个子节点,区别对待根节点是因为如果是根节点,则parent为null
if (current === this.root) {
let child = current.left
let parentChild = null
while (child.right) {
parentChild = child
child = child.right
}
// parentChild为待替换节点的父节点,child为带替换节点
parentChild.right = child.left
this.root = child
child.left = current.left
child.right = current.right
}
// 3.2:不是根节点,而是其他的普通的有两个子节点的节点
else {
let child = current.left
let parentChild = null
while (child.right) {
parentChild = child
child = child.right
}
// 到这里说明找到了被替换的子节点了
// parentChild为待替换节点的父节点,parent为被替换节点的父节点,child为带替换节点
parentChild.right = child.left
parent.left.key === current.key? parent.left = child : parent.right = child
child.left = current.left
child.right = current.right
}
}
return true
}
到这里,二叉搜索树的相关代码就完成了,如果有兴趣的话您可以测试一下。