二叉查找树
定义:
一棵二叉查找树(BST)是一棵二叉树,其中每个结点都含有一个Comparable的键(以及相关联的值),且每个结点的键都大于其左子树任意节点的键而小于右子树任意节点的键。
一棵二叉查找树代表了一组键(及其对应值)的集合,而同一个集合可以用多棵不同的二叉查找树表示
如上图,当我们将一棵二叉查找树的所有键投影到一条直线上,保证一个结点的左子树中的键出现在它左边,右子树同理,就可以得到一条有序的键列
二叉查找树分析
使用二叉查找树的算法运行时间取决于树的形状,而树的形状取决于键插入的先后顺序。最好情况下二叉查找树是完全平衡的,对于含N个元素的二叉查找树,每个空结点与根结点的距离都是~lgN。在最坏情况下,二叉查找树是线性的。
但在一般情况下,二叉查找树往往更接近于最好情况
在由N个随机键构成的二叉查找树中,查找命中所需的比较次数为~2lnN(约1.39lgN)
一次结束与、于给定结点的命中查找所需比较次数为查找路径深度+1,将书中所有节点的深度加起来,就能得到一棵树的内部路径长度。由此,我们不难得到,平均比较次数=平均内部长度+1。
在由N个随机键构成的二叉查找树中,插入结点所需的比较次数也为~2lnN(约1.39lgN)
二叉查找树的实现
package cn.ywrby.Searches;
//二叉查找树的实现
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.StdIn;
public class BST<Key extends Comparable<Key>,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(root);}
public int size(Node node){
if(node==null) return 0;
else return node.N;
}
//获取值的方法
public Value get(Key key){
return get(root,key);
}
//利用递归不断比较目标键值与结点值的大小关系,并向对应方向的子树前进
public Value get(Node x,Key key){
if(x==null) return null;
int cmp=key.compareTo(x.key);
if(cmp>0) return(get(x.right,key));
else if(cmp<0) return (get(x.left,key));
else return x.val;
}
public void put(Key key,Value val){
//查找key,找到则更新该结点的值,否则创建新节点
root=put(root,key,val);
}
public 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.right=put(x.right,key,val); //递归实现
else if(cmp<0) x.left=put(x.left,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 min(x.right);
}
/*
* 向下取整和向上取整
* 在查找小于等于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;
}
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=ceiling(x.left,key);
if(t!=null) return t;
else return x;
}
/*
* 排名类算法rank()和select()
* rank()返回给定键的排名
* select()返回排名为k的结点
* 二者的实现都是利用了之前完善的节点数N
* 以及递归来实现的
* */
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.right,k-t-1);
else if(t>k) return select(x.left,k);
else return x;
}
public int rank(Key key){
return rank(root,key);
}
private int rank(Node x,Key key){
if(x==null)return 0;
int cmp=key.compareTo(x.key);
if(cmp<0) return rank(x.left,key);
else if(cmp>0) return 1+size(x.left)+rank(x.right,key);
else return size(x.left);
}
/*-----------------------------------------删除操作-----------------------------------------------*/
/*
* 在二叉查找树中,删除最大值和删除最小值这种情况是较好实现的
* 因为二者都处于二叉树尾端,一定至多只具有一条子树
* 例如最小值只可能有右子树或没有子树,最大值只可能有左子树或没有子树
* 所以实现删除最小值,只需要从根结点不断向下递归(或循环),得到不含左子树的最小结点 A
* 然后将该节点 A 的右子树 C 挂靠到 A 的父关系的根节点 B 上,就实现了删除最小值的操作,删除最大值的原理类似
* (由于没有链接指向被删除结点,垃圾回收机制会自动清理该变量)
* */
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=1+size(x.left)+size(x.right);
return x;
}
public void deleteMax(){
root=deleteMax(root);
}
private Node deleteMax(Node x){
if(x.right==null)return x.left;
x.right=deleteMax(x.right);
x.N=size(x.right)+size(x.left)+1;
return x;
}
/*
* 二叉查找树中较难实现的是删除普通键的操作,因为一般的键可能会具有两棵子树
* 如何将两棵子树高效的挂靠到一个根节点,是实现删除操作最重要的问题
* 其中的解决方法之一就是-----Hibbarb方法
* 1.将指向即将被删除结点的链接保存为t
* 2.将x指向它的后继节点min(t.right)
* 3.将x的右链接,指向deleteMin(t.right),也就是删除后继节点后的右子树
* 4.将x的左链接,指向t.left
* */
public void delete(Key key){
root=delete(root,key);
}
private Node delete(Node x,Key key){
if(key==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.left==null) return x.right;
if(x.right==null) return x.left;
Node t=x;
x=min(t.right);
x.right=deleteMin(t.right);
x.left=t.left;
}
x.N=1+size(x.right)+size(x.left);
return x;
}
//定义print方法遍历输出二叉树内容
public void print(){
print(root);
}
private void print(Node x){
if(x==null) return ;
print(x.left);
StdOut.printf("The key is %s,and the value is %d\n",x.key,x.val);
print(x.right);
}
public static void main(String[] args){
BST<String, Integer> st = new BST<String, Integer>();
for (int i = 0; !StdIn.isEmpty(); i++) {
String key = StdIn.readString();
st.put(key, i);
}
st.print();
}
}
有序性操作的性能分析:
在一棵二叉查找树中,所有操作在最坏情况下所需时间都和树的高度成正比
证明:树的所有操作都沿着树的一条路径或两条路径进行,根据定义,路径长度不可能大于树的高度。