目录
一、二叉搜索树
二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树
二叉搜索树是一颗二叉树,可以为空;如果不为空,满足以下性质:
- 非空左子树的所有键值小于其根结点的键值。
- 非空右子树的所有键值大于其根结点的键值。
- 左、右子树本身也都是二叉搜索树。
举个例子:下图中第一个不是二叉搜索树,第二三个符合二叉搜索树的特征
二叉搜索树的特点:
相对较小的值总是保存在左结点上,相对较大的值总是保存在右结点上。查找效率非常高,这也是二叉搜索树中“搜索”一词的来源。
二、二叉搜索树的操作及实现
insert(key)
:向树中插入一个新的键。search(key)
:在树中查找一个键,如果结点存在,则返回true
;如果不存在,则返回false
。inOrderTraverse
:通过中序遍历方式遍历所有结点。preOrderTraverse
:通过先序遍历方式遍历所有结点。postOrderTraverse
:通过后序遍历方式遍历所有结点。min
:返回树中最小的值/键。max
:返回树中最大的值/键。remove(key)
:从树中移除某个键。
1.插入数据
//插入数据:对外给用户调用的方法
BinarySerachTree.prototype.insert = function(key){
//1.根据key创建节点
var newNode = new Node(key)
//2.判断根节点是否有值
if(this.root == null){
this.root = newNode
}else{
this.insertNode(this.root,newNode)
}
}
BinarySerachTree.prototype.insertNode = function(node,newNode){
if(newNode.key < node.key){//向左查找
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)
}
}
}
2.遍历二叉搜索树
此处树的遍历,针对所有的二叉树都是适用的,不仅仅是二叉搜索树。
树的遍历:
遍历一棵树是指访问树的每个结点(也可以对每个结点进行某些操作)。二叉树的遍历常见的有三种方式: 先序遍历、中序遍历、后续遍历。(还有程序遍历, 使用较少, 可以使用队列来完成)
(1).先序遍历
遍历过程为:
- ①访问根结点;
- ②先序遍历其左子树;
- ③先序遍历其右子树。
遍历过程:
代码实现:
//1.先序遍历
BinarySerachTree.prototype.preOrderTraversal = function (handler) {
this.preOrderTraversalNode(this.root, handler)
}
BinarySerachTree.prototype.preOrderTraversalNode = function (node, handler) {
if (node !== null) {
//1.处理经过的节点
handler(node.key)
//2.处理经过节点的左子节点
this.preOrderTraversalNode(node.left, handler)
//3.处理经过节点的右子节点
this.preOrderTraversalNode(node.right, handler)
}
}
测试代码:
//测试代码
var pkq = new BinarySerachTree()
pkq.insert(11)
pkq.insert(3)
pkq.insert(6)
pkq.insert(21)
pkq.insert(17)
pkq.insert(5)
//测试遍历
var resultString = ""
pkq.preOrderTraversal(function(key) {
resultString += key + " "
})
alert(resultString)
(2).中序遍历
遍历过程为:
- ①中序遍历其左子树;
- ②访问根结点;
- ③中序遍历其右子树。
遍历过程:
代码实现:
//2.中序遍历
BinarySerachTree.prototype.midOrderTraversal = function(handler){
this.midOrderTraversalNode(this.root,handler)
}
BinarySerachTree.prototype.midOrderTraversalNode = function(node,handler){
if(node !== null){
//1.处理左子树中的节点
this.midOrderTraversalNode(node.left,handler)
//2.处理节点
handler(node.key)
//3.处理右子树中的节点
this.midOrderTraversalNode(node.right,handler)
}
}
测试代码:
//2.中序遍历
var resultString = ""
pkq.midOrderTraversal(function(key) {
resultString += key + " "
})
alert(resultString)
(3).后序遍历
遍历过程为:
- ①后序遍历其左子树;
- ②后序遍历其右子树;
- ③访问根结点。
遍历过程:
代码实现:
//3.后序遍历
BinarySerachTree.prototype.postOrderTraversal = function(handler){
this.postOrderTraversalNode(this.root,handler)
}
BinarySerachTree.prototype.postOrderTraversalNode = function(node,handler){
if(node !== null){
//1.处理左子树中的节点
this.postOrderTraversalNode(node.left,handler)
//2.处理右子树中的节点
this.postOrderTraversalNode(node.right,handler)
//3.处理节点
handler(node.key)
}
}
测试代码:
//3.后序遍历
resultString = ""
pkq.postOrderTraversal(function(key) {
resultString += key + " "
})
alert(resultString)
3.最大值和最小值
依次向左找到最左边的结点就是最小值,代码依次向右找到最右边的结点就是最大值。
//寻找最值
//1.寻找最大值
BinarySerachTree.prototype.max = function(){
//1.获取根节点
var node = this.root
//2.依次向右不断地查找,直到节点为null
var key = null
while(node !== null){
key = node.key
node = node.right
}
return key
}
//2.寻找最小值
BinarySerachTree.prototype.min = function(){
//1.获取根节点
var node = this.root
//2.依次向左不断地查找,直到节点为null
var key = null
while(node !== null){
key = node.key
node = node.left
}
return key
}
4.搜索特定的值
二叉搜索树获取最值和搜索特定的值的效率都非常高。下面分别使用递归方法和循环方法来实现。关于如何选择递归方法和循环方法,其实递归和循环之间可以相互转换。大多数情况下,递归调用可以简化代码,但是也会增加空间的复杂度。循环空间复杂度较低,但是代码会相对复杂。
递归方法:
//搜索特定的值
BinarySerachTree.prototype.search = function (key) {
return this.searchNode(this.root, key)
}
BinarySerachTree.prototype.searchNode = function (node, key) {
//1.如果传入的node为null,那么就退出递归
if (node == null) {
return false
}
//2.判断node节点的值和传入的key的大小
if (node.key > key) {//2.1.传入的key较小,向左边继续查找
return this.searchNode(node.left, key)
} else if (node.key < key) {//2.2.传入的key较大,向右边继续查找
return this.searchNode(node.right, key)
} else {//2.3.相同,说明找到了key
return true
}
}
循环方法:
//搜索某一个特定的值,用for循环完成
BinarySerachTree.prototype.search = function(key){
//1.获取根节点、
var node = this.root
//2.循环搜索key
while(node !== null){
if(key < node.key){
node = node.left
}else if(key > node.key){
node = node.right
}else{
return true
}
}
return false
}
5.二叉搜索树的删除
删除节点要先查找节点,有三种情况:
1.该节点是叶子结点
2.该节点有一个子节点
3.该节点有两个子节点
(1).查找到要删除的节点
BinarySerachTree.prototype.remove = function(key){
//1.寻找要删除的节点
//1.1.定义变量,保存一些信息
var current = this.root
var parent = null
var isLeftChild = true
//1.2.开始寻找删除的节点
while(current.key != key){
parent = current
if(key<current.key){
isLeftChild = true
current = current.left
}else{
isLeftChild = false
current = current.right
}
//某种情况:已经找到了最后的节点,依然没有找到==key
if(current == null) return false
}
(2).删除的节点是叶子节点
//2.1.删除的节点是叶子节点(没有子节点)
if(current.left == null && current.right == null){
if(current == this.root){
this.root = null
}else if(isLeftChild){
parent.left = null
}else{
parent.right = null
}
}
(3).删除的节点有一个子节点
//2.2.删除的节点有一个子节点
else if(current.right == null){
if(current == this.root){
this.root = current.left
}else if(isLeftChild){
parent.left = current.left
}else{
parent.right = current.left
}
}else if(current.left == null){
if(current == this.root){
this.root = current.right
}else if(isLeftChild){
parent.left = current.right
}else{
parent.right = current.right
}
}
(4).删除的节点有两个子节点
删除有两个节点的规律:
如果要删除的节点有两个子节点,甚至子节点还有子节点,这种情况下需要从下面的子节点中找到一个节点,来替换当前的节点。
要找到的这个节点应该是current节点下面所有节点中最接近current节点的。要么比current节点小一点点,要么比current节点大一点点。
比current小一点点的节点,一定是current左子树的最大值;比current大一点点的节点,一定是current右子树的最小值。
在二叉搜索树中,这两个特别的节点,有两个特别的名字:前驱、后继。比current小一点点的节点,称为current节点的前驱;比current大一点点的节点,称为current节点的后继。
所以,先找到这样的节点(前驱或者后继)
寻找后继的代码实现:
//找后继的方法
BinarySerachTree.prototype.getSuccssor = function (delNode) {
//1.定义变量,保存找到的后继
var successor = delNode
var current = delNode.right
var successorParent = delNode
//2.循环查找
while (current != null) {
successorParent = successor
successor = current
current = current.left
}
//3.判断寻找的后继节点是否直接就是delNode的right节点
if (successor != delNode.right) {
successorParent.left = successor.right
successor.right = delNode.right
}
return successor
}
删除有两个子节点的代码:
//2.3.删除的节点有两个子节点
else {
// 1.获取后继节点
var successor = this.getSuccessor(current)
// 2.判断是否是根节点
if (current == this.root) {
this.root = successor
} else if (isLeftChild) {
parent.left = successor
} else {
parent.right = successor
}
// 3.将删除节点的左子树赋值给successor
successor.left = current.left
}
三、二叉搜索树的缺陷
二叉搜索树可以快速地找到给定关键字的数据项,并且可以快速的插入和删除数据项,但是它有一个缺陷,例如,在一棵初始化为9,8,12的二叉树中插入7,6,5,4,3时,就会造成不均匀分布。
较好的二叉搜索树数据应该是左右均匀分布的,分布得不均匀的树,称之为非平衡树。对于一棵平衡二叉树来讲,查找和插入等效率是O(logN),对于一棵非平衡二叉树,相当于一个链表,查找效率变成了O(N)
常见的平衡树有AVL树和红黑树