1. 概述
最基本的数据结构是向量和链表,为了将二者的优势结合起来,我们引入了二叉树,可以认为二叉树是列表在维度上的拓展。而今天要介绍的二叉搜索树(BST)则是在形式上借鉴了二叉树,同时也巧妙借鉴了有序向量的优势和特点。
2. 关键概念
2.1. 寻关键码访问(call-by-key)
关键码:
大小可比较
相等可比对
2.2. 词条(entry)
形式:<key , value>
节点-词条-关键码
每一个节点对应一个词条,每个词条对应一个关键码
3. 特点
微观上处处满足顺序性,宏观上整体满足单调性
顺序性:左子树中所有节点必须不大于根节点,右子树中所有节点必须不小于根节点,
单调性:BST的中序遍历(垂直投影)序列必然单调非降
4. 实现
继承自二叉树(binTree),具体实现可看我的上篇文章:
田浩:JavaScript数据结构 —— 二叉树ES6实现zhuanlan.zhihu.com接口:
- 查找search
- 插入insert
- 删除remove
实现:
class BST extends BinTree {
constructor(rootNode) {
super(rootNode);
// 记忆热点,总是指向命中节点(命中节点可能是null,即假想的哨兵节点)的父亲
this._hot = null;
}
search(xNdode, e) {
}
}
4.1 查找
思路:减而治之,逐步比较,缩小查找范围,直至最终达到平凡的情况。可以看做在效仿有序向量二分查找。
实现:递归,这尾递归实际上很容易借助栈结构变为迭代形式。由递归改为迭代,时间复杂度相同,但是递归调用会执行包含通用的逻辑,每一帧的复杂度较高,所以迭代形式在实际中更高效。这里有必要对其进行改写。内部设置hot变量,为了记录查找到的节点的父亲节点,如果查找失败,我们可以假象存在这样一个节点,_hot依然指向假象节点的父亲节点,这一点很重要,将会为后边的插入算法提供极大地便利
代码:
// 以xNode为根的子树中,查找特定的关键码e
search(e, xNode = this._root) {
// 将hot置为null,以防上次查找结果的干扰
this._hot = null;
return this._searchIn(e, xNode);
}
// 以xNode为根的子树中,查找特定的关键码e
// 时间复杂度为O(h),h为树的高度
_searchIn(e, xNode) {
// 递归基
if (!xNode || e === xNode.data) {
return xNode;
}
// 记忆当前节点
this._hot = xNode;
// 尾递归,很容易改成迭代版本
return this._searchIn(e, (e < xNode.data ? xNode.lChild : xNode.rChild));
}
复杂度:时间复杂度为O(h),h为子树高度,因为没递归一次,查找高度都会下降1层。
4.2 插入
思路:调用search接口,记录_hot的位置,将待插入数字封装成新的节点插入。
实现:
insert(e) {
// 查找e,记录this._hot的的位置 ,O(h)
let xNode = this.search(e, this._root);
let eNode = null;
// 禁止雷同元素,所以在查找失败时,才进行插入操作
if (!xNode) {
// 创建eNode,其父亲为this._hot
eNode = new BinNode(e, this._hot);
// 将eNode作为this._hot的左/右孩子
e < this._hot.data ? this._hot.lChild = eNode : this._hot.rChild = eNode;
this._size++;
// 更新数高度,O(h)
this._updateHeightAbove(eNode);
}
return eNode;
}
复杂度:整体复杂度来自search和_updateHeightAbove,前文讲过,他们复杂度都不会超过O(h)。
4.3 删除
相较与插入接口的实现,删除接口要稍微复杂(复杂来源于节点的删除过程)些。他们同样都需要先调用search接口记录_hot的位置。
实现:
主算法:
remove(x) {
// 查找e,记录this._hot的的位置 ,O(h)
let xNode = this.search(x, this._root);
// 如果不存在返回false
if (!xNode) {
return false;
}
this._removeAt(xNode);
this._size--;
// 更新数高度,O(h)
this._updateHeightAbove(this._root);
return true;
}
_removeAt实现,要区分几种情况:
- 第一种:待删除节点只有一个分支,简单的将其删除,并将用他唯一而孩子取而代之。整棵树依然是BST。实际上这种方法里隐含了无分支情况。
if (!xNode.rChild) {
newNode = xNode.lChild;
}
else if (!xNode.lChild) {
newNode = xNode.rChild;
}
- 第二种情况,双分支:使用法宝——化繁为简,将此种情况转化为单分支情况。找到待删除元素的直接后继(即中序遍历下紧邻的下一个元素),然后将待删除元素和直接后继交换,他的直接后继一定是一个单分支(参考中序遍历规则以及直接后继实现),此时此刻问题已被转化为第一种情况。如下图:
// 直接后继 复杂度O(h)
oldNode = oldNode.succ();
// 交换值
let tempData = xNode.data;
xNode.data = oldNode.data;
oldNode.data = tempData;
// xnode直接后继现在是xNode的值,他只能有右孩子。
// f接下来将oldNode删除,并让他的右孩子代替其位置
newNode = oldNode.rChild;
分支之后是统一的删除待删除节点和拼接新的节点的逻辑:
// 记录_hot
this._hot = oldNode.parent;
if (newNode) {
newNode.parent = this._hot;
if (newNode.data < this._hot.data) {
this._hot.lChild = newNode;
}
else {
this._hot.rChild = newNode;
}
}
// 处理无子节点情况
if (!newNode) {
oldNode.parent.isRChild(oldNode) ? oldNode.parent.rChild = null : oldNode.parent.lChild = null;
}
复杂度:整体复杂度来自succ和_updateHeightAbove,他们复杂度都不会超过O(h)。
至此,已经实现了二叉搜索树的全部接口。具体实现代码以及配套测试代码可见:
https://github.com/tianhao351/javascript-data-structures/blob/master/src/bst/BST.jsgithub.com5. 总结
目前实现的二叉搜索树不管是静态操作(查找)还是动态操作(插入和删除)都很高效,即时间复杂度均为O(h)。
然而就最坏意义而言,bst可能退化成一维链表(每个节点均只有一个子节点),h正比于n。
就平均意义而言,我们将待插入节点的所有随机插入bst,h正比于根号n。
至此,我们还无法对bst的高度h做有效的控制,所得到的性能也无法令我们满意。如果我们能够将h有效的控制,使bst能够适度平衡,高度将渐进的不超过h(log n)。具体请看下集:
声明:
本文完全原创,转载请联系我本人并标明出处。