二叉搜索树(Binary Search Tree,BST)就是为了实现快速,高效的查找,插入,删除数据而设计的。高效的原因依赖于本身的特殊结构。
特点
- 左右子树也分别是二叉搜索树
- 左子树的所有节点 key 值都小于它的根节点的 key 值
- 右子树的所有节点 key 值都大于他的根节点的 key 值
- 二叉搜索树可以为一棵空树
- 一般来说,树中的每个节点的 key 值都不相等,但根据需要也可以将相同的 key 值插入树中
操作
插入
- 如果为空树则将插入节点作为根节点。
- 如果不为空树则从根节点开始,比较插入节点与根节点的 key 值,值相同则不做任何处理直接返回,大于则继续比较右子节点 R,小于则继续比较左子节点 L。
- 右子节点 R 与插入节点比较,插入节点的 key 值大的话则继续往 R 节点的右子节点比较,小于的话则继续往R节点左子节点比较。
- 以此类推不断往下寻找,直到找到左子节点指针或右子节点指针为空的节点,将插入节点放进去。
步骤:
- 创建 D 节点并与根节点比较
- D 小于 E,于是往左子节点继续比较
- D 大于 C,应该往右子节点方向,而此时 C 节点的右子节点指针为空,D 节点可以放置进去
- 同样的,对于 H 节点,先创建 H 节点并与根节点比较
- H 大于 E,于是往右子节点继续比较
- H 大于 G,应该往右子节点方向,而此时 G 节点的右子节点指针为空,H 节点可以放置进去
查询
- 从根节点开始,比较查询节点与根节点的 key 值,值相同则表示找到该节点,直接返回,大于则继续往右子节点R查找,小于则继续往左子节点L查找。
- 右子节点R与查询节点比较,查询节点的 key 值大的话则继续往R节点的右子节点查找,小于的话则继续往R节点左子节点查找。
- 以此类推不断往下寻找,直到找到节点的 key 值与查询节点的相同,则表示查找成功,如果最终找不到则说明不存在该节点。
步骤:
- B 与根节点的 key 值比较
- B 小于 E,往左子节点继续寻找
- B 小于 C,往左子节点继续寻找
- B 大于 A,往右子节点继续寻找,两者相等,找到
- 继续查询 key 值为 G 的节点,与根节点比较
- G 大于 E,往右子节点,两者相等,找到
删除
删除操作分三种情况进行:
- 如果删除的节点为叶子节点,即它的左子节点指针和右子节点指针都为空时,则可以直接删掉该节点,并不会影响整棵树的结构。
- 如果删除的节点只有一个子节点(左子节点或右子节点),则直接将子节点提升到被删除的节点位置。
- 如果删除的节点有两个子节点,此时需要找到删除节点的中序后继或中序前驱来填补删除节点,中序后继其实就是所有大于删除节点中最小的那个,而中序前驱就是所有小于删除节点中最大的那个,因为二叉搜索树经过中序遍历后是一个递增序列,所以后继就是删除节点的后面那个节点,大于且大得最少的那个,比如
1 2 3 4 5
中4就是3的后继。前驱就是删除节点前面那个节点,比如1 2 3 4 5
中2就是3的前驱。
参考资料
Javascript代码初始化二叉树:
class Node {
constructor(data) {
this.data = data
this.left = null
this.right = null
}
}
class BST {
constructor() {
this.root = null
}
// 插入
insert(data) {
}
// 查找
search(data) {
}
// 查找最大值
findMax() {
}
// 查找最小值
findMin() {
}
// 删除最小值
removeMin() {
if (this.root) this.root = this._removeMin(this.root)
}
// 删除最大值
removeMax() {
if (this.root) this.root = this._removeMax(this.root)
}
// 删除指定节点
remove(data) {
}
}
插入:
从根节点开始,依次比较要插入的数据和节点的大小关系。 如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位 置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点数值 小,并且节点的左子树为空,就将新数据插入到左子节点的位置;如果不为空,就再递归遍历左 子树,查找插入位置。
// 插入递归版本
insert(data) {
if (!this.root) return (this.root = new Node(data))
function _insert(data, node) {
if (data < node.data) {
if (!node.left) node.left = new Node(data)
else _insert(data, node.left)
} else {
if (!node.right) node.right = new Node(data)
else _insert(data, node.right)
}
}
_insert(data, this.root)
}
// 插入非递归版本
insertWhile(data) {
if (!this.root) return (this.root = new Node(data))
// 从根节点开始比较
let node = this.root
while (true) {
// 大于往右侧
if (data > node.data) {
if (!node.right) {
node.right = new Node(data)
break
}
node = node.right
}
// 小于往左侧
if (data < node.data) {
if (!node.left) {
node.left = new Node(data)
break
}
node = node.left
}
}
}
查找:
先取根节点,如果它等于我们要查找的数 据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数 据比根节点的值大,那就在右子树中递归查找。
// 查找:递归版本
search(data, node = this.root) {
if (!node) return null
if (node.data === data) return node
if (data > node.data) return this.search(data, node.right)
else return this.search(data, node.left)
}
//查找:非递归版本
searchWhile(data, node = this.root) {
while (node && node.data !== data) {
if (data > node.data) {
node = node.right
} else if (data < node.data) {
node = node.left
}
}
return node
}
查找搜索二叉树中的最大值、最小值:
因为在删除指定节点的时候需要用到,所以这里把_findMax,_findMin方法作为私有方法标记下。
对于以任意节点为根的二叉树,最大值永远是右子树的最右子节点。最小值永远是左子树的最左子节点。
递归代码实现:
// 查找最大值
findMax() {
if (this.root) return this._findMax(this.root)
}
// 查找以某个节点为根的树的最大值
_findMax(node) {
if (!node.right) return node
return this._findMax(node.right)
}
// 查找最小值
findMin() {
if (this.root) return this._findMin(this.root)
}
// 查找以某个节点为根的树的最小值
_findMin(node) {
if (!node.left) return node
return this._findMin(node.left)
}
非递归代码实现:
// 查找最大值
findMax() {
let node = this.root
while(node){
if(!node.right) break
node = node.right
}
return node
}
// 查找最小值
findMin() {
let node = this.root
while(node){
if(!node.left) break
node = node.left
}
return node
}
删除最大、最小值:
递归代码实现:
递归的思路是:每传入一个节点,删除以该节点为根的二叉树的最小值,并返回删除后的新的二叉搜索树的根。
// 删除最小值
removeMin() {
if (this.root) this.root = this._removeMin(this.root)
}
// _removeMin函数是删除以node为根的二叉搜索树的最小节点
// 返回删除node最小节点后新的二分搜索树的根
_removeMin(node) {
// 如果没有左子节点,直接返回右子节点,为null一样适用
if (!node.left) return node.right
node.left = this._removeMin(node.left)
return node
}
// 删除最大值
removeMax() {
if (this.root) this.root = this._removeMax(this.root)
}
_removeMax(node) {
if (!node.right) return node.left
node.right = this._removeMax(node.right)
return node
}
非递归代码实现:
要删除最小节点,关键是找到要删除节点的父节点,然后把父节点的left(最小值)或right(最大值)指向最大值的左子节点或最小值的右子节点。
removeMin() {
// 关键是找到要删除的最小值的父节点,然后让父节点 p.left = null
if(!this.root) return null
if (!this.root.left) return this.root = this.root.right
let node = this.root,
p = this.root
while (node) {
if (!node.left) break;
// p缓存父节点
p = node
node = node.left
}
p.left = node.right
}
removeMax() {
if(!this.root) return null
if(!this.root.right) return this.root = this.root.left
let node = this.root,p = this.root
while(node){
if(!node.right) break;
// p缓存父节点
p = node
node = node.right
}
p.right = node.left
}
删除:
二叉查找树的查找、插入操作都比较简单,但删除操作就比较复杂 。针对要删除 节点的子节点个数的不同,需分三种情况来处理。
第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的 指针置为 null。比如图中的删除节点 55。
第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要 更新父节点中,指向要删除节点的指针(可能是指向左子节点的指针,也可能是指向右子节点的指针),让它指向要删除节点的子节点就可以了。比如图中的删 除节点 13。
第三种情况是,如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右 子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯 定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则 来删除这个最小节点。比如图中的删除节点 18。
实际上,关于二叉查找树的删除操作,还有个非常简单、取巧的方法,就是单纯将要删除的节点 标记为“已删除”,但是并不真正从树中将这个节点去掉。这样原本删除的节点还需要存储在内存 中,比较浪费内存空间,但是删除操作就变得简单了很多。而且,这种处理方法也并没有增加插 入、查找操作代码实现的难度。
删除代码递归实现:
思路类似删除最大最小值,函数_remove接受一个节点node,返回值是删除指定节点后返回的新的以node为根的二叉树
// 删除指定节点
remove(data) {
if (this.root) this.root = this._remove(this.root, data)
}
_remove(node, data) {
if (!node) return null
// 小于node节点
if (data < node.data) {
node.left = this._remove(node.left)
return node
} else if (data > node.data) {
node.right = this._remove(node.right)
return node
} else {
// 如果找到了指定的节点,需要根据该节点的子节点情况删除
// 如果node没有子节点也会走第一个if,node.right===null同样适合
if (!node.left) {
return node.right
} else if (!node.right) {
return node.left
} else {
// 如果自右子节点都存在,需要找到左子树中的最大子节点(前驱节点)
// 或者右子树中的最小子节点(后继节点)
// 这里找右子树中的最小子节点,successorNode叫后继节点
let successorNode = this._findMin(node.right)
// 删除右子树中的最小节点,并将返回的新二叉树返回,赋值给后继节点的右指针
successorNode.right = this._removeMin(node.right)
// 后继节点的左指针指向找到的原node节点的左指针
successorNode.left = node.left
return successorNode
}
}
}
非递归实现:
第一步:找到该节点,及该节点的父节点,并且记录下父节点的方向。
第二步:根据该节点的子节点情况进行删除
remove(data) {
// 先要找到该节点,和该节点的父节点
let node = this.root,
// 记录父节点
p = this.root,
directive = null
while (node && node.data !== data) {
p = node
if (data < node.data) {
node = node.left
directive = 'left'
} else {
node = node.right
directive = 'right'
}
}
if (!node) return '没有该节点'
// 找到node后,需要根据node子节点的不同情况处理
if (!node.left) {
p[directive] = node.right
} else if (!node.right) {
p[directive] = node.left
} else {
// 如果有两个子节点
// 这里找左子树的最大值
let maxNode = node.left,
maxNodeParent = node
while (maxNode) {
if (!maxNode.right) break;
maxNodeParent = maxNode
maxNode = maxNode.right
}
// 说明要删除的node的左子节点就是我们要找的最大值
if (maxNodeParent === node) {
p[directive] = maxNode
maxNode.right = node.right
} else {
maxNodeParent.right = null
p[directive] = maxNode
maxNode.right = node.right
maxNode.left = node.left
}
}
}
二叉搜索树的遍历:
前序中序后序的本质都是深度优先,只是操作的具体位置不同而已。
前序中序后序遍历的概念已经在前面有提到。需要记得两点的是:二叉搜索树的中序遍历其实就是从小到大排序的结果。后序适合一些特殊的操作,比如树的释放,因为释放都需要先释放子节点,再释放父节点,刚好是满足的。
preTraversal(node = this.root) {
if (node) {
console.log(node.data);
this.preTraversal(node.left)
this.preTraversal(node.right)
}
}
midTraversal(node=this.root) {
if(node){
this.midTraversal(node.left)
console.log(node.data);
this.midTraversal(node.right)
}
}
postTraversal(node=this.root) {
if(node){
this.postTraversal(node.left)
this.postTraversal(node.right)
console.log(node.data);
}
}
二分搜索数还有其他的操作,比如求某个数的floor,ceil?
重复二叉搜索树如何处理?
上面的操作方法都是基于二叉树中不存在健值相同的情况,如果相同的话呢?这个时候我们可能就需要把这些方法都稍作调整。
思路:
- 对于插入的话,我们可以把相同的数据都存储在一个节点上,这样的话,我们的节点上存储的就不再是简单的数字,必须是一个对象。
- 仍然每个节点只存储一个数据,插入的时候,如果碰到相同的值,就把这个数据插入到右子树中,就是相当于把相等的值当大于这个节点的值处理。如果要查找的话,也一样,遇到相同的节点,我们要继续在右子树中查找,直到找到所有等于这个值的节点为止。删除也是。
二叉搜索树时间复杂度:
二叉搜索树的时间复杂度的分析其实跟我们在快排排序的分析很类似,都是类似的递归树的分析,如果二叉树根节点的左右子树不平衡,就会影响到时间复杂度的计算,最差情况退化为链表,时间复杂度变成O(n)。满二叉树、完全二叉树就是O(logn)。其实时间复杂度就是跟树的高度成正比。所以在实际应用中,这也是平衡二叉树会比较受欢迎的原因。
完整代码:
class Node {
constructor(data) {
this.data = data
this.left = null
this.right = null
}
}
class BST {
constructor() {
this.root = null
}
// 插入递归版本
insert(data) {
if (!this.root) return (this.root = new Node(data))
function _insert(data, node) {
if (data < node.data) {
if (!node.left) node.left = new Node(data)
else _insert(data, node.left)
} else {
if (!node.right) node.right = new Node(data)
else _insert(data, node.right)
}
}
_insert(data, this.root)
}
// 插入非递归版本
insertWhile(data) {
if (!this.root) return (this.root = new Node(data))
// 从根节点开始比较
let node = this.root
while (true) {
// 大于往右侧
if (data > node.data) {
if (!node.right) {
node.right = new Node(data)
break
}
node = node.right
}
// 小于往左侧
if (data < node.data) {
if (!node.left) {
node.left = new Node(data)
break
}
node = node.left
}
}
}
// 查找:递归版本
search(data, node = this.root) {
if (!node) return null
if (node.data === data) return node
if (data > node.data) return this.search(data, node.right)
else return this.search(data, node.left)
}
//查找:非递归版本
searchWhile(data, node = this.root) {
while (node && node.data !== data) {
if (data > node.data) {
node = node.right
} else if (data < node.data) {
node = node.left
}
}
return node
}
// 查找最大值
findMax() {
if (this.root) return this._findMax(this.root)
}
_findMax(node) {
if (!node.right) return node
return this._findMax(node.right)
}
// 查找最小值
findMin() {
if (this.root) return this._findMin(this.root)
}
_findMin(node) {
if (!node.left) return node
return this._findMin(node.left)
}
// 删除最小值
removeMin() {
if (this.root) this.root = this._removeMin(this.root)
}
// _removeMin函数是删除以node为根的二叉搜索树的最小节点
// 返回删除node最小节点后新的二分搜索树的根
_removeMin(node) {
// 如果没有左子节点,直接返回右子节点,为null一样适用
if (!node.left) return node.right
node.left = this._removeMin(node.left)
return node
}
// 删除最大值
removeMax() {
if (this.root) this.root = this._removeMax(this.root)
}
_removeMax(node) {
if (!node.right) return node.left
node.right = this._removeMax(node.right)
return node
}
// 删除指定节点
remove(data) {
if (this.root) this.root = this._remove(this.root, data)
}
_remove(node, data) {
if (!node) return null
// 小于node节点
if (data < node.data) {
node.left = this._remove(node.left)
return node
} else if (data > node.data) {
node.right = this._remove(node.right)
return node
} else {
// 如果找到了指定的节点,需要根据该节点的子节点情况删除
if (!node.left) {
return node.right
} else if (!node.right) {
return node.left
} else {
// 如果自右子节点都存在,需要找到左子树中的最大子节点(前驱节点)
// 或者右子树中的最小子节点(后继节点)
// 这里找右子树中的最小子节点,successorNode叫后继节点
let successorNode = this._findMin(node.right)
// 删除右子树中的最小节点,并将返回的新二叉树返回,赋值给后继节点的右指针
successorNode.right = this._removeMin(node.right)
// 后继节点的左指针指向找到的原node节点的左指针
successorNode.left = node.left
return successorNode
}
}
}
}
let bst = new BST()
bst.insertWhile(34)
bst.insertWhile(32)
bst.insertWhile(56)
bst.insertWhile(43)
bst.insertWhile(51)
bst.insertWhile(45)
bst.insertWhile(25)
bst.insertWhile(67)
bst.insertWhile(40)
bst.insertWhile(39)
bst.insertWhile(33)
console.log(bst)
console.log(bst.search(10))
console.log(bst.findMin())
console.log(bst.findMax())
// 删除最大、最小值
bst.removeMin()
bst.removeMax()
console.log(bst)
// 删除指定节点
bst.remove(56)
console.log(bst)