学习数据结构和算法的里程是艰辛的,没有捷径,就是看书和看视频,再结合不断的敲代码去记忆,这里记录下红黑树的相关性质和Java代码实现。
红黑树也是一种二叉查找树,只不过它由2-结点和3-结点组成,通过2-结点和3-结点来保证平衡,结点的链接分为两种类型:红链接将两个2-结点连接起来构成一个3-结点,黑链接就是普通的链接。
什么是2-结点,3-结点?如下图(第一个为2-结点图,第二个为3-结点图)
其实就是由父结点指出的链接数量有多少个而定。
在红黑树中,我们将3-结点转为红链接表示:
一颗红黑树的结构:
它具有如下特点:
1.红链接均为左链接;
2.没有任何一个结点同时和两条红链接相连(会构成4-结点);
3.该树是完美黑色平衡的,即任意空链接到根结点的路径上黑链接的数量相同。
结点定义:
private class Node{
Key key;
Value value;
Node left,right;
int N;
boolean color;
public Node(Key key,Value value,int N,boolean color) {
this.key=key;
this.value=value;
this.N=N;
this.color=color;
}
}
成员变量:
private Node root;
private static final boolean RED=true;
private static final boolean BLACK=false;
它相比于普通二叉查找树来讲能实现更高效的平衡插入算法(维护树的平衡),原因在于当树出现不平衡时,可以进行左旋转、右旋转和颜色转换操作进行修复:
1.如果右子结点是红色的而左子结点是黑色的,进行坐旋转;
2.如果左子结点是红色的而且左子结点的左子结点也是红色的,进行右旋转;
3.如果做右子结点都为红色,进行颜色转换。
左旋转:
private Node rotateLeft(Node h) {
Node e=h.right;
h.right=e.left;
e.left=h;
e.color=h.color;
h.color=RED;
e.N=h.N;
h.N=size(h.left)+size(h.right)+1;
return e;
}
右旋转:
private Node rotateRight(Node h) {
Node e=h.left;
h.left=e.right;
e.right=h;
e.color=h.color;
h.color=RED;
e.N=h.N;
h.N=size(h.left)+size(h.right)+1;
return e;
}
颜色转换:
private void flipColors(Node h) {
h.color=RED;
h.left.color=BLACK;
h.right.color=BLACK;
}
最坏情况下的树的高度:一颗大小为N的红黑树的高度不会超过2lgN,最坏情况下一颗红黑树所对应的2-3树中最左边的结点全部是3-结点而其余为2-结点。最左边的路径长度是只包含2-结点的路径长度(~lgN)的两倍。
平均情况下树的高度:一颗大小为N的红黑树,根结点到任意结点的平均路径长度为~lgN。
由此可得最坏情况下红黑树的查找和插入运行时间的增长数量级为2lgN,平均情况下为lgN。
红黑树的插入算法:
新加入元素默认是红结点,加入完后再进行旋转或者改变颜色操作来维持平衡。
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,true);
int v=key.compareTo(h.key);
if(v<0) h.left=put(h.left,key,val);
else if(v>0) h.right=put(h.right,key,val);
else h.value=val;
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;
}
红黑树的查找算法:
public Value get(Key key) {
return get(root,key);
}
private Value get(Node x,Key key) {
if(x==null) return null;
int v=key.compareTo(x.key);
if(v<0) return get(x.left,key);
else if(v>0) return get(x.right,key);
else return x.value;
}
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);
}
public Key select(int k) {
return select(root,k).key;
}
private Node select(Node x,int k) {
if(x==null) return null;
int i=size(x.left);
if(i>k) return select(x.left,k);
else if(i<k) return select(x.right,k-i-1);
else return x;
}
public int rank(Key k) {
return rank(root,k);
}
private int rank(Node x,Key k) {
if(x==null) return 0;
int i=k.compareTo(x.key);
if(i<0) return rank(x.left,k);
else if(i>0) return size(x.left)+1+rank(x.right,k);
else return size(x.left);
}
红黑树的删除算法:
难点!!!
为了能在删除元素后维持红黑树的平衡特性,需要有非常大的开(dai)销(ma)去维护删除后的结构。
在删除元素时,最好的情况是当前结点为3-结点,那直接删除就好了,不会影响树的高度,如果是2-结点,需要从父结点或者左右子树中借结点变为3-结点再进行删除操作:
和父结点借结点变为红节点:
private void moveflipColors(Node e) {//和父节点借节点给子节点,以及还回去
e.color=!e.color;
e.left.color=!e.left.color;
e.right.color=!e.right.color;
}
在删除左子结点的操作中向右子结点借结点:
private Node moveRedLeft(Node e) {//把左子节点变成4-、3-节点
moveflipColors(e);// 先从父节点中借一个
if(isRed(e.right.left)) {//判断兄弟节点,如果是红节点,也从兄弟节点中借一个
e.right=rotateRight(e.right);//各种借的操作
e=rotateLeft(e);//....
moveflipColors(e);//借完左子节点就不需要父节点的了 反转还回去
}
return e;
}
删除红黑树中最小值:
public void deleteMin() {
if(!isRed(root.left)&&!isRed(root.right))
root.color=RED;
root=deleteMin(root);
root.color=BLACK;
}
private Node deleteMin(Node x) {
if(x.left==null) return null;//判断一个节点是否可以删除,就是看该节点的左子节点是否为null
if(!isRed(x.left)&&!isRed(x.left.left))// 判断x的左节点有没2-节点
x=moveRedLeft(x);//都是2-就需要转移生成非2-节点,借红节点,删除时保证树高不变
x.left=deleteMin(x.left);//最后会返回null上来删掉最小节点
return balance(x);// 解除临时组成的4-节点
}
删除元素后为保证平衡,需要检查结点:
private Node balance(Node x) {
if(isRed(x.right)) x=rotateLeft(x);
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;
}
在删除右子结点的操作中向左子结点借结点:
private Node moveRedRight(Node e) {
moveflipColors(e);
if(isRed(e.left.left)) {// 在这里对于兄弟节点的判断都是.left,
// 因为红色节点只会出现在左边
e=rotateRight(e);
moveflipColors(e);
}
return e;
}
删除红黑树中最大值:
public void deleteMax() {
if(!isRed(root.left)&&!isRed(root.right))
root.color=RED;
root=deleteMax(root);
root.color=BLACK;
}
private Node deleteMax(Node x) {
if(isRed(x.left)) {
/* 这里比deleteMin多了一步操作,因为右子节点从父节点中获得节点的时候,
* 我们需要将左边节点给于到右边节点,如果我们不移动的话,会破坏树的平衡
* 5,6
* 1,2 9 对于所展示的这个红黑树,如果不把5从左边移到右边的话,
* 我们会直接删除9,这样会导致树的不平衡,因为红节点总是在左边的,我们进行删除操作
* 的时候,直接将结点给予,只需要改变颜色即可,不需要移动
* 对于红黑树而言,6是黑结点,再删除的时候,是不需要移动的,
* 我们移动的是5这样的红结点
*/
x=rotateRight(x);
}
if(x.right==null) return null;
if(!isRed(x.right)&&!isRed(x.right.left))
x=moveRedRight(x);
x.right=deleteMax(x.right);
return balance(x);
}
删除操作(结合删除最小值和最大值的代码):
public void delete(Key key) {
if(!isRed(root.left)&&!isRed(root.right))
root.color=RED;
root=delete(root,key);
root.color=BLACK;
}
private Node delete(Node e,Key key) {
if(key.compareTo(e.key)<0) {
if(!isRed(e.left)&&!isRed(e.left.left)) e=moveRedLeft(e);
e.left= delete(e.left,key);
}else {
if(isRed(e.left)) e=rotateRight(e);
if(key.compareTo(e.key)==0&&e.right==null) return null;//
if(!isRed(e.right)&&!isRed(e.right.left)) e=moveRedRight(e);
if(key.compareTo(e.key)==0) {
Node min=min(e.right);
e.key=min.key;
e.value=min.value;
e.right=deleteMin(e.right);
}
else e.right=delete(e.right,key);
}
return balance(e);
}
可以看到删除代码的操作及其复杂和难以理解,主要原因是为了维持红黑树的数据结构。可能会有人不懂从左/右子树中借结点的代码和从父结点中借结点的代码,这里放三张网上找的图个人觉得很生动,结合代码看下就能明白了。
从父结点中借:
从右子结点中借:
从左子结点中借:
判断是否平衡:
// do all paths from root to leaf have same number of black edges?
private boolean isBalanced() {
int black = 0; // number of black links on path from root to min
Node x = root;
while (x != null) {
if (!isRed(x)) black++;
x = x.left;
}
return isBalanced(root, black);
}
// does every path from the root to a leaf have the given number of black links?
private boolean isBalanced(Node x, int black) {
if (x == null) return black == 0;
if (!isRed(x)) black--;
return isBalanced(x.left, black) && isBalanced(x.right, black);
}
}
返回存入元素数量:
public int size() {
return size(root);
}
private int size(Node n) {
if(n==null) return 0;
return n.N;
}