为了保证查找树的平衡性我们需要一些灵活性,这里我们运行一棵树的一个结点中可以保存多对键值,所以这里利用2-3查找树实现平衡查找
2-3平衡查找树
定义:一棵2-3查找树或为一棵空树,或由以下结点组成:
- 2-结点,含有一个键及其对应值,和两个链接,与一般二叉树中结点一致
- 3-结点,含有两个键(a,b)及其对应值,和三个链接,左链接指向小于a的所有键,中链接指向a到b之间的所有键,右链接指向大于b的所有链接
查找操作:
2-3查找树的查找操作与一般二叉树类似,首先在根节点的键中进行比较,有相同则返回,没有则根据比较结果进入对应子树,继续进行比较,并在子树中进行递归式继续查找,若最终查找到空链接,则查找未命中
插入操作:
2-3查找树的插入操作相比一般查找树多了许多条件
向2-结点中插入新键:
假如未命中的查找结束于2-结点,,只需要把2-结点转换为3-结点,将要插入的键放入其中即可
向一棵只含一个3-结点的树中插入新键:
可以先暂时将新键以同样的方式插入到唯一的一个节点中,形成一个4-结点(三个键值,四条链接),然后将它拆分成三个2-结点。其中,以中间节点为根节点,左节点为根节点的左子树,右节点为根节点右子树。
经过操作后并没有影响2-3查找树的平衡性以及有序性,空链接到根节点的距离还是完全相同的,但这个过程中树的深度+1
向一个父结点为2-结点的3-结点中插入新键:
我们仍然使着先构建起一个4-结点,但此时它的上方存在一个2-结点。所以我们不能再次将它拆分成三个2-结点,这会打破已经建立的平衡性。
所以我们将中间结点放入父结点(2-结点)中形成一个新的3-结点,然后将左节点连接到新形成的3-结点的中结点位置,将右节点连接到新形成的3-结点的右节点位置。
整个过程没有增加树的深度,并且维持了整体的有序性以及平衡性
向一个父结点为3-结点的3-结点中插入新键
我们仍然可以向上面一样操作,甚至不需要改动。
当中间结点加入到父结点后,形成了一个新的4-结点。我们只需要把它当作向3-结点中新插入一个键一样重复操作。递归的运行,知道上升过程中遇到一个2-结点结束我们的递归操作。
如果这个过程中没有遇到2-结点,那么会在根节点处形成4-结点,这就涉及了我们讨论的第一种情况,只需要将根节点拆分成3个2-结点就可以结束整个插入过程
以上讨论的四种插入方式,展示了面对所有插入可能时2-3查找树的插入方式,这其中最重要的就是它们在插入过程中都维护了整个树的有序性以及平衡性,杜绝了树的任意生长
在一棵大小为N的2-3树中,查找和插入操作访问的结点必然不超过lgN个
证明:显而易见,当一棵2-3树全为3-结点时,深度为 ( l o g 3 N ) 向 下 取 整 = ( l g N l g 3 ) 向 下 取 整 (log_3N)_{向下取整}=(\frac{lgN}{lg3})_{向下取整} (log3N)向下取整=(lg3lgN)向下取整。当全部都为2-结点时,深度为 ( l g N ) 向 下 取 整 (lgN)_{向下取整} (lgN)向下取整
通过以上结论我们可以得到2-3在最坏情况下仍然具有较好的性能
红黑二叉树
实现2-3二叉树的一种方式
替换3-结点
红黑二叉树的本质还是一个正常的二叉查找树,但它引入了一些额外的信息,通过一些处理构造了3-结点的存在。
引入的额外信息就是链接的颜色,每一条链接有两种可能颜色,黑色和红色。黑色链接就是普通链接。红色链接连接的两个结点形成一个整体看作3-结点。
这种操作使我们不用修改就能利用二叉查找树中的get方法
等价的定义
含有红黑链接并满足下列条件的二叉查找树称作红黑树:
- 红链接均为左链接
- 没有任何一个节点同时和两条红链接相连(避免4-及以上结点)
- 该树是完美黑色平衡的,也就是每一个空节点到根节点的路径上经过的黑链接数是完全相同的
满足这样定义的红黑树和对应的2-3数是一一对应的
旋转操作
在我们进行的某些操作中可能会插入右链接的红链接或者是连续的红链接,这时候我们就需要进行旋转,在操作结束前将他们修复
插入过程
向单个2-结点中插入新键
如果插入的是左键,直接就形成一个3-结点,如果插入的是右键,只需要经过一次旋转就可形成一个3-结点
向树底部的2-结点插入新键
我们利用红链接将要插入的结点与2-结点相连,如果是左链接直接形成3-结点,如果是右链接经过一次旋转即可形成3-结点
向一棵双键树(即一个3-结点)中插入新键
这又细分为三种情况:
- 插入的键大于原树中的两个键:这种情况最简单,只需要用红链接将其与根节点相连形成4-结点,再进行颜色转换即可
- 插入的键小于原树中的两个键:这样插入后会形成两个连续的红链接,所以连接后要将原树的两个节点进行旋转,得到类似第一种情况的结构,然后再转换颜色
- 新键介于原树中的两个键之间:这样连接后会形成两个连续的红链接,不过一个是左链接,一个是右链接,我们需要先将含插入键的链接旋转,得到类似第二种的情况,再旋转原树的两个结点,最后转换颜色
颜色转换
用来转换一个根节点的两个子节点都为红链接的情况,其将两个子节点全部设为黑链接,并将根节点设为红链接
以下操作成功处理了前文描述的所有情况,保证插入操作的顺利
- 如果右子节点是红色而左子节点是黑色,进行左旋转
- 如果左子节点以及左子节点的左子节点都是红色的进行右旋转
- 如果左右子节点都为红色,进行颜色转换
红黑树的实现
package cn.ywrby.Searches;
//红黑二叉树,平衡查找树实现
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
public class RedBlackBST<Key extends Comparable<Key>,Value > {
//利用布尔值定义结点链接的颜色
private static final boolean RED =true;
private static final boolean BLACK =false;
private Node root;
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;
}
}
public int size(){return size(root);}
public int size(Node node){
if(node==null) return 0;
else return node.N;
}
//判断结点颜色类型
private boolean isRed(Node x){
if(x==null) return false; //空节点和不存在的结点规定都是黑色
return x.color==RED;
}
/*
* 左旋转和右旋转
* 其中h为转换前的根节点
* x为h的子节点
* 左旋转过程:将x的左子树设为h的右子树,再将h整体设为x的左子树
* x就成为了新的根节点
* 且在旋转过程中没有破坏二叉树的有序性
*/
private Node rotateLeft(Node h){
Node x=h.right;
h.right=x.left;
x.left=h;
x.color=h.color;
h.color=RED;
x.N=h.N;
h.N=1+size(h.left)+size(h.right);
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=1+size(h.left)+size(h.right);
return x;
}
//颜色转换:将一个两个子节点均为红色节点的结点转为红色节点,同时将两个子节点都设为黑色
private void flipColors(Node h){
h.color=RED;
h.left.color=BLACK;
h.right.color=BLACK;
}
//插入算法
public void put(Key key,Value val){
root=put(root,key,val);
root.color=BLACK;
}
private Node put(Node h,Key key,Value val){
if(h==null) return new Node (key,val,1,RED);
int cmp=key.compareTo(h.key);
if(cmp<0)h.left=put(h.left,key,val);
else if(cmp>0)h.right=put(h.right,key,val);
else h.val=val;
//与普通的二叉查找树唯一区别就在于下面对于对应关系的保护
if(isRed(h.right)&&!isRed(h.left))h=rotateLeft(h); //处理右链接出现红链接的情况,进行左旋转
if(isRed(h.left)&&isRed(h.left.left))h=rotateRight(h); //处理临时的4-结点,也就是两条连续的红链接
if(isRed(h.left)&&isRed(h.right))flipColors(h); //处理两个子链接均为红链接情况,进行颜色转换
h.N=1+size(h.left)+size(h.right);
return h;
}
//获取值的方法,与普通的二叉查找树完全一致
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;
}
//定义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){
RedBlackBST<String, Integer> st = new RedBlackBST<String, Integer>();
for (int i = 0; !StdIn.isEmpty(); i++) {
String key = StdIn.readString();
st.put(key, i);
}
st.print();
}
}