二叉查找树
一种能够将链表插入的灵活性与有序数组查找的高效性结合起来的符号表实现。
public class BST<Key extends Comparable<Key>,Value>{
private class Node{
Key key;
Value val;
Node left,right;
int N; //以该结点为根的子树中的结点总数
public Node(Key key,Value val,int N) {
this.key=key;
this.val=val;
this.N=N;
}
}
private Node root;
public int size() {
return size(root);
}
private int size(Node x) {
if(x==null) return 0;
return x.N;
}
}
查找
在符号表中查找一个键可能得到两种结果。如果含有该键的结点存在于表中,查找就命中了,返回相应的值。否则查找未命中。
查找的递归实现
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;
}
查找的非递归实现
private Value get(Key key) {
Node x = root;
while(x!=null) {
int cmp = key.compareTo(x.key);
if(cmp==0) return x.val;
else if(cmp>0) x=x.right;
else x=x.left;
}
return null;
}
插入的递归实现
查找key,找到则更新它的值,否则为它创建一个新的结点。
public void put(Key key,Value val) {
root = put(root,key,val);
}
private Node put(Node x,Key key,Value 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;
}
二叉查找树的查找代码几乎和二分查找一样简单。
二叉查找树的另一个重要特性就是插入的实现难度和查找差不多。
可以将递归调用前的代码想象成沿着树向下走:它会将给定的键和每个结点的键相比较并根据结果向左或者向右移动到下一个结点。
将递归调用后的代码想象成沿着树向上爬。对于get方法,这对应着一系列的返回指令,对于put方法,这意味着重置搜索路径上每个父结点指向子结点的链接,并增加路径上每个结点中的计数器的值。
使用二叉查找树的算法的运行时间取决于树的形状,而树的形状又取决于键被插入的顺序。
最好的情况下,一棵含有N个结点的树是完全平衡的,每条空链接和根结点的距离是lgN
最坏情况下,搜索路径上有N个结点。
平衡查找树
一棵含有N个结点的树,我们希望树的高度为lgN,这样就能保证所有查找都能在~lgN次比较内结束。
但在动态插入中保证树的完美平衡的代价太高了,因此稍稍放松完美平衡的要求,这就是2-3查找树。
2-3查找树
一棵2-3查找树或为一棵空树,或由以下结点组成:
1. 2结点,含有一个键和两条链接,左链接指向的2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点。
2. 3结点,含有两个键和三条链接,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。
2-3树能够在插入新结点后继续保持平衡。
在2-3查找树中插入新结点时,如果未命中的查找结束于一个2结点,只要把这个2结点换为3结点,将要插入的键保存在其中即可。如果未命中的查找结束于一个3结点,为了将新键插入,使之成为一个临时4结点,再将4结点分解。
向一棵只含有一个3结点的树中插入新键
向一个父结点为2结点的3结点中插入新键
向一个父结点为3结点的3结点中插入新键
标准二叉查找树由上向下生长。
与此不同,2-3树的生长是由下向上的。
二叉查找树中,按照升序插入10个键会得到高度为9的一棵最差查找树。
而使用2-3树,树的高度是2。
一棵含有N个结点的2-3树,高度在log3N和log2N之间。也就是说在一棵大小为N的2-3树中,查找和插入操作访问的结点必然不超过lgN个。
我们可以用不同的数据类型表示2结点和3结点并写出变换需要的代码,但维护两种不同类型的结点,将被查找的键和结点中每个键进行比较,将链接和其他信息从一种结点复制到另一个结点等等,产生的额外开销可能会使算法比标准的二叉查找树更慢。
平衡一棵树是为了消除最坏情况,但是我们希望这种保障需要的代价越少越好。因此使用红黑二叉树。
红黑二叉树
用标准的二叉查找树(全部是2结点)和一些额外信息(替换3结点)来表示2-3树。
红链接将2个2结点连接起来构成一个3结点,黑链接则是2-3树中的普通链接。
红黑树的另一种定义:含有红黑链接并满足下列条件的二叉查找树
红链接均为左链接
没有任何一个结点同时和两条红链接相连
该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同。
满足这样定义的红黑树和相应的2-3树是一一对应的。
如果指向结点的链接是红色的,该变量为true,黑色则为false
private static final boolean RED = true;
private static final boolean BLACK= false;
private class Node{
Key key;
Value val;
Node left,right;
int N;
boolean color;
public Node(Key key,Value val,int N,boolean color){
this.key=key;
this.val=val;
this.N=N;
this.color=color;
}
}
左旋转
private Node rotateLeft(Node h){
Node x=h.right;
x.color=h.color;
h.color=RED;
h.right=x.left;
x.left=h;
x.N=h.N;
h.N=size(h.left)+size(h.right)+1;
return x;
}
右旋转
private Node rotateRight(Node h){
Node x=h.left;
h.left=x.right;
x.right=h;
x.color=h.color;
h.color=RED;
x.N=h.N;
h.N=size(h.left)+size(h.right)+1;
return x;
}
颜色转换
向3结点中插入新键
- 新键大于原树中的两个键,被连接到右链接。颜色转换。
- 新键小于原树中的两个键,被连接到最左边的空链接,产生了两条连续的红链接。将上层红链接右旋转得到第一种情况。
- 新键在原树中的两个键之间,产生两条连续的红链接,一条红色左链接一条红色右链接。将下层红链接左旋转得到第二种情况。
红黑树的插入算法
public void put(Key key,Value val){
root=put(root,key,val);
root.color=BLACK;
}
private Node put(Node x,Key key,Value val){
if(x==null) return new Node(key,val,1,RED);
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;
//如果右子结点是红色的而左子结点是黑色的,进行左旋转
if(!isRed(x.left) && isRed(x.right))
x=rotateLeft(x);
//如果左子结点是红色的,且它的左子结点也是红色的,进行右旋转
if(isRed(x.left) && isRed(x.left.left))
x=rotateRight(x);
//如果左右子结点都是红色的,进行颜色转换
if(isRed(x.left) && isRed(x.right))
flipColors(x);
x.N=size(x.left)+size(x.right)+1;
return x;
}