二叉查找树实现有序符号表
二叉查找树的定义
一颗二叉查找树(BST)是一颗二叉树,其中每个节点都含有一个Comparable
的键(以及相关的值)且每个节点的键都大于其左子树中的任意节点的键而小于右子树的任意节点的键
二叉查找树实现有序符号表的优点
- 结合了链表插入的灵活性和有序数组查找的高效性
算法原理
-
查找
get(Node tree, Key key)
- 如果树是空的,则查找未命中
- 如果查找的键和根结点的键相等,查找命中
- 如果被查找的键比根节点的键小,递归查找左子树
- 如果被查找的键比根节点的键大,递归查找右子树
-
插入
put(Node tree, Key key, Value value)
插入的原理与查找一样简单,这种简洁性是二叉查找树的重要特性之一。插入的实现难度和查找差不多是二叉查找树的另一个更重要的特性。
- 如果树是空的,返回一个该键值对的新节点
- 如果被查找的键和根节点的键相同,更新根节点的值
- 如果被查找的键比根节点的键小,递归查找左子树,直到查找命中或结束于空链接
- 如果被查找的键比根节点的键大,递归查找右子树,直到查找命中或结束于空链接
在插入节点的同时,需要递归的维护每颗树的节点数量(符号表的大小)
二叉查找树的运行时间取决于树的形状,而树的形状取决于键被插入的先后顺序。
在由 N N N个随机键构造的二叉查找树种,查找命中平均所需的比较次数为 2 l n N ~2lnN 2lnN(约 1.39 l o g 2 N 1.39log_2N 1.39log2N)
在由 N N N个随机键构造的二叉查找树种,插入操作和查找未命中平均所需的比较次数为 2 l n N ~2lnN 2lnN(约 1.39 l o g 2 N 1.39log_2N 1.39log2N)
-
最大键
min(Node tree)
和最小键max(Node tree)
如果根节点的左链接为空,那么一个二叉查找树的最小键就是根节点;如果左链接非空,那么树种的最小键就是左子树的最小键。通过简单的递归或者循环就能实现这段描述。
找出最大键的方法类似,只是变为查找右子树而已。
-
向下取整
floor(Node tree, Key key)
- 如果给定的键 key 小于二叉查找树的根节点的键,那么小于等于 key 的最大键(floor)一定在根节点的左子树中
- 如果给定的键 key 大于二叉查找树的根节点的键,那么只有当根节点右子树中存在小于等于 key 的节点时,小于等于 key 的最大键(floor)才会出现在右子树中,否则根节点就是小于等于 key 的最大键
-
向上取整
ceiling(Node tree, Key key)
将向下取整逻辑中的“左”变成“右”,“小于”换成“大于”即可
-
选择操作
select(int k)
- 假设需要找到排名为
k
的键(即树中正好右k
个节点小于它的键) - 如果左子树的节点数
t
大于k
,那么就继续(递归地)在左子树中查找排名为k
的键 - 如果
t
等于k
,返回根节点的键 - 如果
t
小于k
,那么就(递归地)在右子树中查找排名为(k-t-1)
的键
- 假设需要找到排名为
-
排名
rank(Key key)
rank()
是select()
的逆方法- 如果给定的键和根节点的键相等,返回左子树的节点总数
t
- 如果给定的键小于根节点,返回该键在左子树中的排名(递归计算)
- 如果给定的键大于根节点,返回
t+1
(根节点)加上它在右子树中的排名(递归计算)
- 如果给定的键和根节点的键相等,返回左子树的节点总数
-
删除最小键
deleteMin()
递归的查找树的左子树,直到找到没有左子树的节点,将此节点的父级节点的左链接指向此节点的右子树(只需要在递归中返回它的右链接即可)。此时已经没有任何链接指向要被删除的节点,因此它会被垃圾回收机制回收。
-
删除最大键
deleteMax()
完全类似于
deleteMin()
-
删除操作
二叉查找树中最难实现的操作就是删除操作
当把二叉查找树投影到一条直线上时效果如下图:
对于只有一个子节点(或者没有子节点)的节点,我们可以使用类似于删除最小键或者最大键的方式来实现。如删除图中的节点H,只需要将节点R的左链接指向节点M即可。
对于有两个子节点的的节点,要删除它需要怎么做?
如需要删除图中的节点E,如何保证删除它后保证二叉查找树的特性即每个节点的键都大于其左子树中的任意节点的键而小于右子树的任意节点的键?
此时我们不看树而看投影 A C E H M R S X,删除E后,得到 A C _ H M R S X , 它依然是有序的!由此可以轻易想到让节点H去到原来节点E的位置即可实现删除E的同时依旧保证二叉查找树的特性。
节点H是节点E的后继节点,即节点H的右子树的最小节点
假设要删除二叉查找树中的节点
x
, 删除它需要一下四步:-
将指向即将被删除的节点的链接保存为
t
-
将
x
指向它的后继节点min(t.right)
-
将
x
的右链接(原本指向一颗所有节点都大于x.key
的二叉查找树)指向deleteMin(t.right)
,也就是在删除后所有节点仍然都大于x.key
的子二叉树 -
将
x
的左链接指向t.left
(其下所有的键都小于被删除的节点和它的后继节点)
-
性能分析
在一个二叉查找树中,所有操作在最坏的情况下所需的时间都和树的高度成成比
java实现
import java.util.NoSuchElementException;
/**
* 二叉查找树实现有序符号表
*/
public class BST<Key extends Comparable<Key>, Value> implements IOrderedSymbolTable<Key, Value> {
private Node root;
private class Node {
private Key key;
private Value value;
/**
* 左子树
*/
private Node left;
/**
* 右子树
*/
private Node right;
/**
* 节点总数
* size = left.size + right.size + 1
*/
private int size;
public Node(Key key, Value value) {
this.key = key;
this.value = value;
this.size = 1;
}
}
public BST() {
}
/**
* 返回最小的键
*
* @return 最小的键
*/
@Override
public Key min() {
if (isEmpty()) {
return null;
}
return min(root).key;
}
/**
* 返回最大的键
*
* @return 最大的键
*/
@Override
public Key max() {
if (isEmpty()) {
return null;
}
return max(root).key;
}