最主要的目的是将一个键和一个值联系起来。用例能够将一个键值对插入符号表并希望在之后能够从符号表的所有键值对中按照键直接找到对应的值,即以键值对为单元的数据结构。
性能:N方
代码
public int rank(Key key){
int lo = 0,hi = N-1;
while(lo <= hi){
int mid = lo + (hi - lo)/2;
int cmp = key.compareTo(keys[mid]);
if(cmp < 0) hi = mid - 1;
else if(cmp > 0) lo = mid + 1;
else return mid;
}
return -1;
}
性能:在N个键的有序数组中进行二分法查找最多需要(lgN+1)次比较(无论是否成功)
一个二叉查找树(BST)是一颗二叉树,其中每个节点都含有一个Comparable的键(以及相关联的值),且每个结点的键都大于其左子树中的任意节点的键而小于右子树的任意节点的键。
get()查找,put()插入,
节点结构:键值,左连接,右链接,计数器
使用二叉查找树的算法的运行时间取决于树的形状。在由N个随机键构成的二叉树中,查找命中平均所需的比较次数为~2lnN(约为1.39lgN)
private void print(Node x){
if(x == null) return;
print(x.left);
System.out.println(x.key);
print(x.right);
}
普通二叉查找树的高度跟键的插入顺序有关,对于足够大的N,这个值趋近于2.99lgN。但我们仍然无法保证二叉查找树的性能。而平衡二叉树就是解决了这一问题。
一颗2-3查找树或是一颗空树,或由以下节点组成:
注意:以上变换都不会影响树的完美平衡性,因为除了根结点的4-分解情况之外,树的高度都不会增加,根结点的4-分解会使树的整体高度加1。
在一棵大小为N的2-3树中,查找和插入操作访问的结点必然不超过lgN(N个结点的2-3树的高度在log(3)N=(lgN/lg3)和lgN之间。)
连续插入10个元素2-3树的生长情况:图3.3.10
2-,3-结点的变换操作麻烦,需要处理的情况比较多,要维护两种不同的结点,难以把结点做简化的一致的抽象。
幸运的是你将看到,我们只需要一点点代价就能用一种统一的方式完成所有变换。
旋转时,把红节点的父链接看成一条红线,红线上的内侧子结点(即红色左链接的右子结点或红色右链接的左子结点)可以在红线上沿着重力的方向自由滑动。
红黑二叉查找树背后的基本思想是用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。我们将树中的链接分为两种类型:红链接将两个2-结点连接起来构成一个3-结点,黑链接则是2-3树中的普通链接。确切地说,我们将3-结点表示为由一条左斜的红色链接(两个2-结点中小的是大的的左子结点)相连的两个2-结点,如下图3.3.12。我们将用这种方式表示2-3树的二叉查找树成为红黑二叉查找树。
红黑树的另一种定义是含有红黑链接并满足下列条件的二叉查找树:
如果将一棵红黑树中的红链接画平,那么所有的空链接到根结点的距离都将是相同的。如果我们将由红链接相连的节点合并,得到的就是一棵2-3树。图3.3.13
方便起见,因为每个结点都只会有一条指向自己的链接(从它的父节点指向它),我们将链接的颜色保存在表示结点的Node数据类型的red成员中。具体见代码
public class Node,Value>{
Key key;
Value value;
Node left,right;
int N;
boolean red;
public Node(Key key,Value value,int N,boolean red){
this.key = key;
this.value = value;
this.N = N;
this.red = red;
}
private boolean isRed(Node x){
if(x == null){
return false;
}
return x.red;
}
}
修复红黑树,使得红黑树中不存在红色右链接或两条连续的红链接。
Node rotateLeft(Node h){
Node x = h.right;
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = true;
x.N = h.N;
h.N = 1 + size(h.left) + size(h.right);
return x;
}
将红色的左链接转化为红色的右链接,代码与左旋完全相同,只要将left换成right即可。如图3.3.17
代码
Node rotateRight(Node h){
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = true;
x.N = h.N;
h.N = 1 + size(h.left) + size(h.right);
return x;
}
在插入新的键时,我们可以使用旋转操作帮助我们保证2-3树和红黑树之间的一一对应关系,因为旋转操作可以保持红黑树的两个重要性质:有序性和完美平衡性。也就是说,我们在红黑树中进行旋转时无需为树的有序性或者完美平衡性担心。下面我们来看看应该如何使用旋转操作来保持红黑树的另外两个重要性质:不存在两条连续的红链接和不存在红色的右链接。我们先用一些简单的情况热热身。
一棵只含有一个键的红黑树只含有一个2-结点。插入另一个键之后,我们马上就需要将他们旋转。如果新键小于老键,我们只需要新增一个红色的节点即可,新的红黑树和单个3-结点完全等价。如果新键大于老键,那么新增的红色节点将会产生一条红色的右链接。我们需要使用parent = rotateLeft(parent);来将其旋转为红色左链接并修正根结点的链接,插入才算完成。两种情况均把一个2-结点转换为一个3-结点,树的黑链接高度不变,如图3.3.18和3.3.19
这种情况又可分为三种子情况:新键小于树中的两个键,在两者之间,或是大于树中的两个键。每种情况中都会产生一个同时链接到两条红链接的结点,而我们的目标就是修正这一点。
如图3.3.21,我们专门用一个方法flipColors()来转换一个结点的两个红色字结点的颜色。除了将子结点的颜色由红变黑之外,我们同时还要将父节点的颜色由黑变红。这项操作最重要的性质在于它和旋转操作一样是局部变换,不会影响整棵树的黑色平衡性。根据这一点,我们马上就能在下面完整实现红黑树。
颜色转换会使根结点变为红色,我们在每次插入操作后都会将根结点设为黑色。
现在假设我们需要在树的底部的一个3-结点下加入一个新结点。前面讨论过的三种情况都会出现,如图3.3.22所示。颜色转换会使指向中结点的链接变红,相当于将它送入了父结点。这意味着在父结点中继续插入一个新键,我们也会继续用相同的办法解决这个问题。
2-3树中的插入算法需要我们分解3-结点,将中间键插入父结点,如此这般知道遇到一个2-结点或是根结点。总之,只要谨慎地使用左旋,右旋,颜色转换这三种简单的操作,我们就能保证插入操作后红黑树和2-3树的一一对应关系。在沿着插入点到根结点的路径向上移动时在所经过的每个结点中顺序完成以下操作,我们就能完成插入操作:
从上到下查找,由下至上进行平衡变换,如下代码
public class RedBlackBST{
private Node root;
private boolean isRed(Node h);
private Node rotateLeft(Node h);
private Node rotateRight(Node h);
private void flipColors(Node h);
private int size(Node node);
public void put(Node freshNode){
//查找key,找到则更新其值,否则为它新键一个结点
root = put(root,freshNode);
root.red = false;
}
private Node put(Node h,Node freshNode){
if(h == null){//标准插入操作,和父结点用红链接相连
return new Node(freshNode.key,freshNode.value,1,true);
}
int cmp = freshNode.key.compareTo(h.key);
if(cmp < 0) h.left = put(h.left,freshNode);
else if(cmp > 0) h.right = put(h.right,freshNode);
else h.value = freshNode.value;
if(isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);
if(isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
if(isRed(h.left) && isRed(h.right)) flipColors(h);
h.N = size(h.left) + size(h.right) + 1;
return h;
}
}
要描述删除算法,首先要回到2-3树。和插入操作一样,我们也可以定义一系列局部变换来在删除一个结点的同时保持树的完美平衡性。这个过程比插入一个结点更加复杂,因为我们不仅要在(为了删除一个结点而)构造临时4-结点时沿着查找路径向下进行变换,还要在分解遗留的4-结点时沿着查找路径向上进行变换(同插入操作)。
作为第一轮热身,我们先学习一个沿着查找路径既能向上也能向下进行变换的稍简单的算法:2-3-4树的插入算法,2-3-4树中允许存在我们以前见过的4-结点。它的插入算法沿着查找路径向下进行变换是为了保证当前结点不是4-结点(这样树底才有空间来插入新的键),沿着查找路径向上进行变换是为了将之前创建的4-结点配平,如图3.3.25所示。
向下的变换和我们在2-3树中分解4-结点所进行的变换完全相同。如果根结点是4-结点,我们就将它分解成三个2-结点,使得树高加1。在向下查找的过程中,如果遇到一个父结点为2-结点的4-结点,我们将4-结点分解为两个2-结点并将中间键传递给他的父结点,使得父结点变为一个3-结点;如果遇到一个父结点为3-结点的4-结点,我们将4-结点分解为两个2-结点并将中间键传递给它的父结点,使得父结点变为一个4-结点;我们不必担心会遇到父结点为4-结点的4-结点,因为插入算法本身就保证了这种情况不会出现。到达树的底部之后,我们也只会遇到2-结点或者3-结点,所以我们可以插入新的键。要用红黑树实现这个算法,我们需要:
在第二轮热身中我们要学习2-3树中删除最小键的操作。我们注意到从树底部的3-结点中删除键是很简单的,但2-结点则不然。从2-结点中删除一个键会留下一个空结点,一般我们会将它替换为一个空链接,但这样会破坏树的完美平衡。所以我们需要这样做:为了保证我们不会删除一个2-结点,我们沿着左链接向下进行变换,确保当前结点不是2-结点(可能是3-结点,也可能是临时的4-结点)。首先根结点可能有两种情况。如果根是2-结点且它的两个子结点都是2-结点,我们可以直接将这三个结点变为一个4-结点;否则我们需要保证根结点的左子结点不是2-结点,如有必要可以从它右侧的兄弟结点“借”一个键来。以上情况如图3.3.26所示。
在沿着左链接向下的过程中,保证以下情况之一成立:
在查找路径上进行和删除最小键相同的变换同样可以保证在查找过程中任意当前结点均不是2-结点。如果被查找的键在树的底部,我们可以直接删除它。如果不在,我们需要将它和它的后继结点交换,就和二叉树一样。因为当前结点必然不是2-结点,问题已经转化为在一颗根结点不是2-结点子树中删除最小键,我们可以在这个子树中使用前问所述的算法。和以前一样,删除之后我们需要向上回溯并分解余下的4-结点。
红黑树的性质
重要结论:所有基于红黑树的符号表实现都能保证操作的运行时间为对数级别。
一颗大小为N的红黑树的高度不会超过2lgN。这个上界是比较保守的,实际上,一颗大小为N的红黑树中,根结点到任意结点的平均路径长度为~1.001lgN。
各种符号表的性能总结