一、在讲解红黑树之前,我们要先知道一个树叫:”2-3树“
每一个东西的诞生都是因为之前的方法无法满足某些需求了,所以新的东西被研发出来了。首先我们看这个例子:向二叉树中插入9,8,7,6,5,4,3,2,1。如果这样插入的话,那么可以说
这就不是二叉树了,这是一个链表,因为后面插入的数据都比前一个小,那么就一直是左子树。二叉树之所以被发明出来就是为了查找快,但是这种情况查询的速度就不行了,于是,我们想:
能不能解决这样的问题呢?所以有了2-3树,(一个结点有一个子树就叫它几杠结点,在2-3树中,可以一个结点有三个子树,中间的结点比父节点(有两个数据)的第一个数据大比第二个数据小)
当我们插入结点时,还是比根节点小则向左找……,不同的是:插入结点时,找到叶子节点后,如果叶子节点是二杠结点,则升为三杠结点;若为三杠顶点,则升为四杠顶点,但是2-3树中不允许四杠结点存在
就会把这个四杠结点拆分。拆分的具体实现是:将该四杠结点的中间的数据向上提,如果上面有节点,上面的结点同样实现上述步骤,一直到根节点,如果根节点插入后也变成了四杠节点,
那么同理,还是将该结点的中间数据向上提,即树高度+1;
我们其实就是要,减小树的深度
二、2-3树的性质:
因为2-3的二杠结点和三杠节点,所以使得:
1.所有结点到根节点的距离相等
2.除非插入时根节点由三杠结点转为四杠结点,否则树的高度不会变
3.二叉树和2-3树的最大区别是:二叉树的自顶向下生长的,2-3树是自底向上生长的
2-3树的实现非常复杂,在这里不做实现,但是,2-3树的思想非常重要!在红黑树、B树、B+树中,都用到了2-3树的思想
这篇文章,我们实现红黑树:正常的二叉树与两个子树的连接都是黑色的,红黑树就是要实现2-3树,但是由于三杠结点等实现复杂,所以它是这么实现的:它用每个结点和其特殊标记(红色连接)
来代表这个三杠节点,那么由红色连接的两个结点(可以看成是一个结点)就共同组成了一个三杠结点(在画图时我们可以把红链接化成平线,黑链接向下指)。红黑树是二叉树的一个扩展,所以它当然也满足二叉树的性质:左子节点的key小于根节点的key,右子节点的key大于根节点的key
简单示意图:
平衡化操作:即当红黑树经过增删改查的操作后,红黑树可能就不满足红黑树的定义了,我们需要对该树进行操作,使之依旧满足红黑树的条件
1.左旋:
2.右旋:
3.颜色反转:
将右旋过后的(上图)E结点的下面两节点的颜色都变成黑色,E结点本身变成红色
三、上代码:(结点类是红黑树类中的内部类)
public class RedBlackTree<K extends Comparable<K>, V> {
//根节点
private Node<K, V> root;
//结点个数
private int N;
//直接true、false可读性不高
private static final boolean RED=true;
private static final boolean BLACK=false;
private class Node<K, V> {
private K key;
private V value;
private Node<K, V> left;
private Node<K, V> right;
//我们将指向该结点的连接定义成颜色。值得注意的是:在红黑树中,根节点是一个比较特殊的结点,它的连接一定是黑色的,因为没有任何结点指向它/
private boolean color;
public Node(K key, V value, Node<K, V> left, Node<K, V> right, boolean color) {
this.key=key;
this.value=value;
this.left=left;
this.right=right;
this.color=color;
}
}
//构造方法
public RedBlackTree() {
root = null;
N=0;
}
//返回结点个数
public int size() {
return N;
}
//判断树是否为空
public boolean isEmpty() {
return N==0;
}
/**
* @param h 结点
* @return 判断指向该结点的连接是否为红色
*/
public boolean isRed(Node<K, V> h) {
//如果为null,我们认为其连接是黑色的
if(h==null) return false;
return h.color==RED;
}
//因为红黑树本身一个种平衡树。所以我们要进行树的平衡化。使整个树平衡(意思就是:在数进行增删改查操作时不破坏红黑树的规则):
/**
* @param h h结点的右连接为红,我们需要将该红链接改到左面
* @return Node 返回更改后满足条件的结点(更改后的结点并不是h了,是h的右子节点)
* 左旋目的:是为了有4-结点时操作
*
* 由于传进来的这个结点应该还是其父节点的,所以我们要返回修改后的结点作为其原来父节点的左/右结点。
*/
private Node<K, V> rotateLeft(Node<K, V> h) {
//获取h结点的右子节点s
Node<K, V> s = h.right;
//将s的左子节点作为h的右子节点(因为s的左子节点必比h大)
h.right = s.left;
//将h作为s的左子节点
s.left = h;
//将h的颜色复制给s颜色,因为之前与上面连接的是h,该后就变成s了
s.color = h.color;
//h的颜色为红色
h.color = RED;
//返回新的结点
return s;
}
/**
* @param h h结点和其左子结点(s)的连接和 s和s的左子结点的左子树连接都为红,我们需要进行右旋操作
* @return Node 返回更改后的结点(更改后,返回为:h与其左子结点和其右子结点的连接都为红。但是显然,这样也不行,因为还是构成了一个4-结点,所以我们好需要一步操作:”颜色反转“)
* 右旋目的:当出现当出现一个结点的两个连接都为红链接时,说明这是一个4-结点,但是红黑树/2-3树不允许4-结点的存在
* 函数作用:“将h结点和其左子结点(s)的连接和 s和s的左子结点的左子树连接都为红” 修改为: “该结点与其左子结点和其右子结点的连接都为红”
*/
private Node<K, V> rotateRight(Node<K, V> h) {
//获取h的左子节点s
Node<K, V> s = h.left;
//s的右子节点作为h的左子节点
h.left = s.right;
//h作为s的右子节点
s.right = h;
//h的颜色赋值给s的颜色
s.color = h.color;
//让h的颜色为红色
h.color = RED;
//返回修改后满足条件的结点
return s;
}
/**
* 反转颜色:即将右旋后的结果在进行处理,使其恢复红黑树的规则
* 这个方法就比较简单了
* @param h h结点的左右子树连接都为红色时,进行修改
*/
private void flipColors(Node<K, V> h) {
//让h的左子节点和右子结点的颜色变成黑色
h.left.color = BLACK;
h.right.color = BLACK;
//h结点的颜色变为红色
h.color = RED;
}
//由于我们想要实现平衡树,所以每次插入都让其连接为红色,然后使整颗树平衡
public void put(K key, V value) {
root = put(root, key, value);
//根节点的颜色总是黑色的,因为没有其他结点指向根节点
root.color = BLACK;
}
/**
* insert重载方法,向指定树中插入元素
* @param x 指定树
* @param key 插入的键
* @param value 插入的值
* @return 返回的结点是插入后的根节点(因为插入后可能会引起树根节点的变化)
*/
private Node<K, V> put(Node<K, V> x, K key, V value) {
//如果没有元素
if(x==null) {
N++;
return new Node<>(key, value, null, null, RED);
}
//判断插入元素与当前结点key值的大小关系,(cmp<0说明前面的小)
int cmp = x.key.compareTo(key);
//如果比当前元素小
if(cmp>0) {
x. left = put(x.left, key, value);//递归寻找
} else if(cmp<0) {
x.right = put(x.right, key, value);//同理
} else {
//就是与当前结点的key相等了,我们只需要把value改一下即可
x.value = value;
return x;
}
//我们需要判断插入后,如果不满足红黑树的定义,我们要把它改成满足红黑树的样子
if(isRed(x.right) && !isRed(x.left)) {
x = rotateLeft(x);//左旋
}
if(isRed(x.left) && isRed(x.left.left)) {
x = rotateRight(x);//右旋
}
if(isRed(x.left) && isRed((x.right))) {
flipColors(x);//颜色反转
}
return x;
}
/**
* 获取整个树的key所对应value
* @param key 键
* @return 找到的结果
*/
public V get(K key) {
return get(root, key);
}
/**
* get的重载方法,找到某棵树上的键所对应的值
* @param x 某个结点/树的根节点
* @param key 键
* @return 找到的结果
*/
private V get(Node<K,V> x, K key) {
if(x==null) return null;
int cmp = x.key.compareTo(key);
if(cmp>0) {
return get(x.left, key);
} else if(cmp<0) {
return get(x.right, key);
} else {
return x.value;
}
}
}
注:上述部分图片来自黑马程序员教程。