之前只用c语言实现过二叉树,最近在面试,也是为了锻炼自己的js水平,今天用了js实现排序二叉树,也算是对之前二叉树相关知识的一个回顾。
什么是二叉排序树(BST)
二叉排序树也被称为二叉搜索树,二叉排序树是具有下列性质的二叉树(可以是空树):
(1)若左子树不空,则左子树上所有节点的值均小于它的根节点的值;
(2)若右子树不空,则右子树上所有节点的值均大于它的根节点的值;
(3)左、右子树也分别为二叉排序树;
(4)没有键值相等的节点。
具体算法的思路与实现
1.定义二叉树相关的数据结构
排序二叉树是二叉树的一个特例,因此它也具有二叉树的数据结构,我们可以使用定义二叉树的方法来定义它。
function Node(val) { // 定义二叉树的结点类型
this.val = val
this.left = null
this.right = null
}
function BinaryTree(arr) { // 定义二叉树的数据结构,数据为数组arr中的数据
this.root = null
arr.forEach(item => {
this.insert(item) // 创建二叉树就是在不停的插入结点
})
return this
}
2.插入结点
我们使用insert()方法来插入结点,insertNode()方法是insert()方法的关键实现。插入结点的方法如下:
通过递归依次将待插入的节点与排序二叉树的部分节点进行大小比较。从根节点开始,遵循左子节点小于当前节点,右子节点大于当前节点的规则进行递归插入。比如:我们需要插入一个比父亲结点小的元素,如果父亲结点左孩子为空,那么我们把它直接插入到父亲结点的左边;否则,我们只能在父亲结点的左孩子的左孩子进行插入,以此类推。这段话可能有点绕口,下面解合代码以及注释应该就一目了然了。
BinaryTree.prototype.insert = function (val) { // 为二叉树新增结点
let newNode = new Node(val) // 定义要插入的结点
if (this.root === null) {
this.root = newNode
} else {
this.insertNode(this.root, newNode)
}
}
BinaryTree.prototype.insertNode = function (node, newNode) { // 在父亲结点左孩子或右孩子处插入结点
if (newNode.val < node.val) { // 新结点小于父亲结点,放左边
if (node.left === null) { // 没有左孩子,可以直接插入
node.left = newNode
} else {
// 如果有了左孩子,则在将该结点插入左孩子的左孩子,递归
this.insertNode(node.left, newNode)
}
} else { // 新结点大于父亲结点,放右边
if (node.right === null) { // 没有右孩子,可以直接插入
node.right = newNode
} else {
// 如果有了右孩子,则在将该结点插入右孩子的右孩子,递归
this.insertNode(node.right, newNode)
}
}
}
3.三种遍历方式
常见的三种遍历方式为:前序遍历、中序遍历、后序遍历。层次遍历以及上述方法的非递归实现在更新了栈和队列的数据结构之后再去讲,这里先忽略。
1.前序遍历的操作过程如下:
如果二叉树为空树,则什么都不做,否则。
(1)访问根结点。
(2)前序遍历左子树。
(3)前序遍历右子树。
作用:前序遍历可以用来复制二叉树,每个结点只用访问一遍,效率非常的高。
对应的代码如下:
BinaryTree.prototype.preOrder = function (node) { // 前序遍历二叉树
if (node) {
console.log(node.val)
this.preOrder(node.left)
this.preOrder(node.right)
}
}
2.中序遍历的操作过程如下:
如果二叉树为空树,则什么都不做,否则。
(1)中序遍历左子树。
(2)访问根节点。
(3)中序遍历右子树。
作用:中序遍历之后会得到一个升序排列的序列,因此它可以用来对无重复数据的数组进行排序,时间复杂度为O(logn)。
对应的代码如下:
BinaryTree.prototype.inOrder = function (node) { // 中序遍历二叉树
if (node) {
this.inOrder(node.left)
console.log(node.val)
this.inOrder(node.right)
}
}
3.后序遍历的操作过程如下:
如果二叉树为空树,则什么都不做,否则。
(1)后序遍历左子树。
(2)后序遍历右子树。
(3)访问根节点。
作用:操作系统中的相关的文件访问操作,因为它很容易计算路径。
对应代码如下:
BinaryTree.prototype.postOrder = function (node) { // 后序遍历二叉树
if (node) {
this.postOrder(node.left)
this.postOrder(node.right)
console.log(node.val)
}
}
4.查找二叉树中的最小与最大结点
这个其实非常好理解,因为二叉排序树总是父亲结点的值大于左孩子的值,且小于右孩子的值,那么当我们查找最大或最小值时,只需要一直沿着一侧查找就可以了。代码如下,也非常的简单:
// 查找二叉树中的最小值结点
BinaryTree.prototype.getMinNode = function (node) {
let p = node
while (p.left !== null) {
p = p.left
}
return p
}
// 查找二叉树中的最大值结点
BinaryTree.prototype.getMaxNode = function (node) {
let p = node
while (p.right !== null) {
p = p.right
}
return p
}
5.查找二叉树中的是否存在值为num的某个结点
这个也是利用了二叉排序树的特效,当num比当前结点值小时,只需要向左侧查找,反之向右侧查找即可,与第4点类似,代码如下:
// 查找二叉树中是否存在值为num的某个结点
BinaryTree.prototype.searchNode = function (node, num) {
if (node) {
if (num < node.val) {
return this.searchNode(node.left, num)
} else if (num > node.val) {
return this.searchNode(node.right, num)
} else {
return true
}
} else {
return false
}
}
6.删除值为val的结点
这个是二叉排序树的操作中比较难的一个,思路是:先在二叉树中找到值为val的结点,如果没找到,则返回false,如果找到了,则进行删除,并且返回true。
删除操作需要分四种情况考虑:
1.是叶子结点:这个非常简单,找到结点直接删除即可,它的删除不会改变二叉树其他结点的结构。
2.只有左孩子:这个也比较简单,只需要找到该结点,并且将该结点node的父结点指向node.left即可。
3.只有右孩子:与第2点类似,只是换成了right,这里不多赘述。
4.既有左孩子又有右孩子:这个就是比较难的点了,因为我们对该结点删除会改变其余结点的结构,我们使用的方法是:找到这个结点的最小后代结点aux,将aux的值与node进行交换,并删除最小结点。
代码如下:
BinaryTree.prototype.remove = function (node, val) { // 删除结点
if (node === null) {
return null
}
if (val < node.val) {
node.left = this.remove(node.left, val)
return node
} else if (val > node.val) {
node.right = this.remove(node.right, val)
return node
} else { // 找到了要删除的结点
if (node.left === null && node.right === null) { // 找到的结点是叶子结点
node = null
return node
}
if (node.right === null) { // 只有左孩子
node = node.left
return node
}
if (node.left === null) { // 只有右孩子
node = node.right
return node
}
// 以上条件都不符合,即存在左孩子和右孩子
let aux = this.getMinNode(node.right)
node.val = aux.val // 最小结点的值和node进行交换
node.right = this.remove(node.right, aux.key) // 删除最小结点
return node
}
}
结语
到此为止整个二叉排序树的算法就已经完成了,后续会补充层次遍历以及前、中、后三种遍历方法的非递归实践,如果有什么错误或者不好的地方,欢迎大家指出来。