提示
这篇博客主要是我对红黑树一些认识,如果你想较全面的了解红黑树推荐下面这两个,算法4和算法导论也有较全面的讲解。
本文主要参考
初识红黑树
红黑树和2-3树是等价的,基于2-3树的红黑树是一种特殊的红黑树——左倾红黑树,我觉得基于2-3树来理解红黑树,比较好理解,毕竟红黑树的发明人Robert Sedgewick 在其经典著作《算法4》也是用2-3树来引入红黑树的。2-3树是完美平衡的树结构。2–3树的查找元素操作与二叉搜索树的查找类似。因为节点中的数据元素都是有序的,所以查找函数可以据此进入正确的子树进行查找,最终找到正确的节点。进行插入操作时,可以先通过查找操作确定合适的位置,然后将数据插入对应节点。如果插入后的节点变成4节点(包含三个数据元素),则需将该节点拆分为两个2节点,中间的数据元素进入父节点。这样一来,该父节点也可能也会因此变成4节点,则该父节点也会拆分为两个2节点,中间的数据元素进入该父节点的父节点,以此类推,直到修改后的父节点不需要分裂,或者被拆分的是根节点,此时中间数据元素就会单独形成2节点,成为新的根节点。
左倾红黑树的红节点对应的就是2-3树的3节点的左节点,红黑树是保持“黑平衡”的二叉树,严格意义上讲,不是平衡二叉树,最大高度为 2logn,高度的复杂度为O(logn)
图片截自刘宇波老师数据结构的视频很nice的一门课
红黑树的性质
- 每个节点或者是红色的,或者是黑色的。
- 根节点是一定是黑色的,2-3树中,当根节点是二节点的时候明显对应为黑色,当跟节点是三节点的时候,红黑树中对应的红节点就跑到坐下角了。
- 每一个叶子节点(指最后的空节点,并不指左右节点都为空的那个节点)是黑色的相当于定义了空节点本身就是一个黑色的节点
- 每个红色结点的两个子结点一定都是黑色。(父子节点之间不能出现两个连续的红节点)
- 任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
开始手撕红黑树(代码是跟着刘宇波老师的课程实现的)
结点定义
class Node<T extends Comparable<T>> {
private T value;//node value
private Node<T> left;//left child pointer
private Node<T> right;//right child pointer
private Node<T> parent;//parent pointer
private boolean red;//color is red or not red
public Node(){}
public Node(T value){this.value=value;}
public Node(T value,boolean isRed){this.value=value;this.red = isRed;}
public T getValue() {
return value;
}
void setValue(T value) {
this.value = value;
}
Node<T> getLeft() {
return left;
}
void setLeft(Node<T> left) {
this.left = left;
}
Node<T> getRight() {
return right;
}
void setRight(Node<T> right) {
this.right = right;
}
Node<T> getParent() {
return parent;
}
void setParent(Node<T> parent) {
this.parent = parent;
}
boolean isRed() {
return red;
}
boolean isBlack(){
return !red;
}
/**
* is leaf node
**/
boolean isLeaf(){
return left==null && right==null;
}
void setRed(boolean red) {
this.red = red;
}
void makeRed(){
red=true;
}
void makeBlack(){
red=false;
}
@Override
public String toString(){
return value.toString();
}
}
左旋转
/**左旋转
* node x
* / \ 左旋转 / \
* t1 x ---------> node t3
* / \ / \
* t2 t3 t1 t2
* */
private Node leftRotate(Node node) {
Node x = node.right;
Node t2 = x.right;
// 左旋转
x.left = node;
node.right = t2;
x.color = node.color; // x等于原来树的根节点
// 2-3树中,添加节点都是红节点,旋转交换之后,也必须
// 保证这个特性。所以要把node变为红色!(以2-3树举个例子:
// 一颗树先只有根节点为黑2,现在添加节点红4,对应到红黑树,
// 根节点就要变成黑4,左子树就要变成红2!)
node.color = RED;
return x;
}
右旋转
/**右旋转
* node x
* / \ 右旋转 / \
* x t2 -------> y node
* / \ / \
* y t1 t1 t2
* */
private Node rightRotate(Node node) {
Node x = node.left;
// 右旋转
node.left = x.right;
x.right = node;
// 维护颜色
x.color = node.color;
node.color = RED;
return x;
}
颜色翻转
// 颜色翻转,向3节点添加一个节点(节点对应的位置在右子树,
// 子节点变黑,父节点变红和上层进行融合)
private void flipColors(Node node) {
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
添加节点
由于我们使用的是简化版的左倾红黑树,对于某些场景还需要特殊处理
- 场景3,当插入节点为父节点的左节点时,直接插入;当插入节点为父节点的右节点时,需要进行左旋转。
- 场景4.1 不存在。
// 向红黑树中添加新的元素(key, value)
public void add(K key, V value){
root = add(root, key, value);
root.color = BLACK; // 保持最终的根节点为黑色
}
// 向以node为根的红黑树中插入元素(key, value),递归算法
// 返回插入新节点后红黑树的根
private Node add(Node node, K key, V value){
if(node == null){
size ++;
return new Node(key, value);
}
if(key.compareTo(node.key) < 0)
node.left = add(node.left, key, value);
else if(key.compareTo(node.key) > 0)
node.right = add(node.right, key, value);
else // key.compareTo(node.key) == 0
node.value = value;
// 维护红黑树!!!!
// 左旋转(对应两种情况!)
if(isRed(node.right) && !isRed(node.left))
node = this.leftRotate(node.left);
// 右旋转
if(isRed(node.left) && isRed(node.left.left))
node = this.rightRotate(node.left);
// 颜色翻转
if(isRed(node.left) && isRed(node.right))
this.flipColors(node);
return node;
}
左倾红黑树相对来说比较容易理解,但是为了维护“左倾”这个性质,做了额外的事情,消耗了性能,没有“任何不平衡都可以在三次旋转内解决”这么好的性能优势。
删除节点
删除节点的过程,参考美团技术团队(这个实现的是优化的红黑树)
红黑树其他的功能实现跟其他平衡二叉树差不多。
参考