@TOC
定义与概念
一颗二叉查找树(BST)是一颗二叉树,其中每个结点都含有一个Comparable的键(以及相关的值)且每个结点的键都大于其左子树的任意结点的键而小于右子树的任意结点的键
- 啰嗦一下:假设有一结点a,值比它小,且距离它最近的结点,是其左子树的最右边的结点。值比它大,且距离它最近的结点,是其右子树的最左边的结点。其实,二叉搜索树如果用中序遍历一下,就是一个从大到小的序列。且就模型而言,二叉查找树和快速排序超级无敌像。
基本实现
二叉查找树的建立
public class BST ,Value>{
private Node root; //二叉树的根节点
private class Node{
private Key key; //键
private Value val; //值
private Node left, right; //指向子树的链接
private int N; //以该结点为根的子树中的结点总数
public Node(Key key, Value val, int N){
this.key = key; this.val = val; this.N = N;
}
}
public int size(){
return size();
}
private int size(Node x){
if(x == null) return 0;
else return x.N;
}
}
查找
二叉树查找一个键的递归算法:
- 如果树是空的则查找未命中
- 如果被查找的键和根结点的键相等,查找命中,否则我们就递归地在适合的子树中继续查找
- 如果被查找的键较小就选择左子树,较大则选择右子树
- 罗嗦一下:java中可以使用Comparable 的 CompareTo()方法来进行比较操作【至于为啥不用equals呢,等之后知道再填上】
- 代码实现
public Value get(Key key){
return get(root,key);
}
private Value get(Node x, Key key){
if(x == null) return null;
int cmp = key.compareTo(x.key);
if(cmp < 0) return get(x.left,key);
else if(cmp > 0) return get(x.right, key);
else return x.val;
}
插入
插入操作思想:
- 如果树是空的,就返回一个含有该键值对的新结点
- 如果被查找的键小于跟根结点的键,我们会继续在左子树中插入该键,否则在右子树中插入该键
- 罗嗦一下:关于递归的一些事,递归调用前就好像沿着树向下走:它会将给定的键和每个结点的键相比较并根据结果向左或者向右移动到下一个结点。递归调用后就好像沿着树向上爬。对于get()方法,这对应着一系列的返回指令(return),对于put()方法,这意味着充值搜索路径上每个父结点指向子节点的链接,并增加路径上每个结点中的计数器的值【个人理解:其实也就是递归调用后,树叶的结点自下而上,一个一个连起来,直到根节点】
- 代码实现
public void put(Key key, Value val){
root = put(root, key, val);
}
private Node put(Node x, Key key, Value val){
// 如果key存在以x为根结点的子树中则更新它的值
// 否则将以key 和 val 为键值对的新节点插入到该子树中
if(x == null) return new Node(key, val, 1);
int cmp = key.compareTo(x.key);
if (cmp < 0) x.left = put(x.left, key, val);
else if(cmp > 0) x.right = put(x.right, key, val);
else x.val = val;
x.N = size(x.left) + size(x.right) + 1;
return x;
}
最大键和最小键
最小键的实现思想(最大键类似):
- 如果根节点的左连接为空,那么一课二叉查找树最小的键就是根结点
- 如果左连接非空,就一直向树的左子树进发,直到最低
- 代码实现:
public Key min(){
return min(root).key;
}
private Node min(Node x){
if(x.left == null) return x;
return min(x.left);
}
public Key max(){
return max(root).key;
}
private Node max(Node x){
if(x.right == null) return x;
return max(x.right);
}
向上取整和向下取整
向下取整的实现思想:
- 如果给定的键key小于二叉查找树的根结点的键,那么小于等于key的最大键floor(key)一定在根结点的左子树中
- 如果给定的键key大于二叉查找树的根结点的键,那么只有当根结点右子树中存在小于key的结点时,小于等于key的最大键才会出现在右子树中,否则根节点就是小于等于key的最大键
- 代码实现:
public Key floor(Key key){
Node x = floor(root, key);
if(x == null) return null;
return x.key;
}
private Node floor(Node x, Key key){
if(x == null) return null;
int cmp = key.compareTo(x.key);
if(cmp == 0) return x;
if(cmp < 0) return floor(x.left,key);
Node t = floor(x.right, key);
if(t != null) return t;
else return x;
}
//返回大于等于key的最小键
public Key ceiling(Key key){
Node x = ceiling(root, key);
if(x == null) return null;
return x.key;
}
private Node ceiling(Node x, Key key){
if(x == null) return null;
int cmp = key.compareTo(x.key);
if(cmp == 0) return x;
if(cmp < 0) return ceiling(x.right,key);
Node t = floor(x.left, key);
if(t != null) return t;
else return x;
}
选择操作
引言:使用每个结点中维护的子树结点计数器变量N来进行此操作,假设我们要找到排名为k的键
选择的实现思想:
- 如果左子树的节点数t > k,那么我们就继续递归地在左子树中查找排名为k的键
- 如果t = k,我们就返回根结点中的键
- 如果t < k,我们就递归地在右子树中查找排名为(k-t-1)的键
- 代码实现:
public Key select(int k){
return select(root, k).key;
}
private Node select(Node x, int k){
if(x == null) return null;
int t = size(x.left);
if(t > k) return select(x.left, k);
else if(t < k) return select(x.right, k);
else return x;
}
排名
引言:rank()是select()的逆方法,他会返回给定键的排名
排名的实现思想:
- 如果给定的键和根结点的键相等,我们返回左子树中的结点总数t
- 如果给定的键小于根结点,我们会返回该键在左子树中的排名(递归计算)
- 如果给定的键大于根结点,我们会返回 t+1(根结点)加上它在右子树中的排名(递归计算)
- 代码实现:
public int rank(Key key){
return rank(key,root);
}
private int rank(Key key, Node x){
if(x == null) return 0;
int cmp = key.compareTo(x.key);
if(cmp < 0) return rank(key, x.left);
else if(cmp > 0) return 1 + size(x.left) + rank(key, x.right);
else return size(x.left);
}
删除最大键和删除最小键
引言:此处的递归方法接受一个指向结点的链接,并返回一个指向结点的链接【说人话就是参数是一课树,然后返回的其实是一棵树】
删除最小值的实现思想:
- 不断深入根结点的左子树直到遇到一个空链接,然后将指向该结点的链接指向该结点的右子树(只需要在递归调用中【递归的走到最底下,准备要向上爬的那一步】返回它的右链接即可,而那个没有任何链接指向的结点,就会被垃圾收集器清理掉)
- 代码实现:
public void deleteMin(){
root = deleteMin(root);
}
private Node deleteMin(Node x){
if(x.left == null) return x.right;
x.left = deleteMin(x.left);
x.N = size(x.left) + size(x.right) + 1;
return x;
}
删除操作
删除实现的思想:
- 将指向即将被删除的结点的链接保存为t
- 将x指向它的后继结点min(t.right)
- 将x的右链接(原本指向一颗所有结点都大于x.key的二叉查找树)指向deleteMin(t.right),也就是在删除后所有结点仍然都大于x.key的子二叉查找树
- 将x的左链接(本为空),设为 t.left (其下所有的键都小于被删除的结点和它的后继结点)
- 啰嗦一下:
- 在递归调用后,我们会修正被删除的结点的父节点的链接。
- 为什么不能直接让新结点的右结点等于t.right呢? 因为此时的右子树t.right中,还保留有要替代结点(替代被删去的结点位置),只有用deleteMin()方法,将t.right子树清除那个结点才可以
- 关于性能问题:它的一个缺陷是可能会在某些实际应用中产生性能问题。这个问题在于选用后继结点的选择是一个随意的决定,且没有考虑树的对称性。
5.代码实现:
public void delete(Key key){
root = delete(root, key);
}
private Node delete(Node x, Key key){
if(x == null) return null;
int cmp = key.compareTo(x.key);
if(cmp < 0) x.left = delete(x.left, key);
else if(cmp > 0) x.right = delete(x.right, key);
else{
if(x.right == null) return x.left;
if(x.left == null) return x.right;
Node t = x; //创建一个t先保存要删除的结点
x = min(t.right); //让要删除的结点x,等于右子树中离它最近的结点x1,用min可得出
x.right = deleteMin(t.right); //将x1在原来的地方去掉,然后返回右边子树,赋给这个新的结点
x.left = t.left;
}
x.N = size(x.left) + size(x.right) + 1;
return x;
}
范围查找
范围查找实现思想:
- 使用树的中序遍历,然后用队列Queue挑选出符合范围要求的结点
代码实现:
public Iterable keys(){
return keys(min(),max());
}
public Iterable keys(Key lo, Key hi){
Queue queue = new Queue();
keys(root, queue, lo, hi);
return queue;
}
private void keys(Node x, Queue queue, Key lo, Key hi){
if(x == null) return;
int cmplo = lo.compareTo(x.key);
int cmphi = hi.compareTo(x.key);
if(cmplo < 0) keys(x.left, queue, lo, hi);
if(cmplo <= 0 && cmphi >= 0) queue.enqueue(x.key);
if(cmphi > 0) keys(x.right, queue, lo, hi);
}
性能分析
- 二叉查找树的基本实现的良好性能依赖于键的分布足够随机以消除长路径,最坏的情况——用例将所有的键按照顺序或者逆序插入符号表就会增加这种情况的出现的概率,而在没有明确的警告下来避免这种行为时,有些用例肯定会尝试这么做
最坏情况下的运行时间的增长数量级(N次插入之后) | 平均情况下的运行时间的增长数量级(N次插入随机键之后) |
---|---|
查找与插入 | 查找命中与插入 |
N |
参考:算法第四版