Skip to content

数据结构和算法之旅(十)- 红黑树

约 3190 字大约 11 分钟

数据结构tree红黑树

2017-03-20

红黑树也是一种自平衡的二叉搜索树,较之AVL,插入和删除旋转次数更少。性能上要稍微高一些。

定义

红黑树特性:

  • 1.所有节点都有俩种颜色:红与黑
  • 2.所有null视为黑色
  • 3.红色节点不能相邻,(判断平衡的主要依据)
  • 4.根节点时黑色
  • 5.从根到任意一个叶子节点,路径中的黑色节点数一样(判断平衡的主要依据)

不满足这些特性的都是不平衡的红黑树。

是否是红黑树?

树1

不是红黑树。 因为违反了第3条:红色节点不能相邻。

树2

不是红黑树。 因为违反了第5条:从根到任意一个叶子节点,路径中黑色节点数一样。 右边重,左边轻,是不平衡的。

树3

是红黑树。 关键的3,4,5条特性都满足。

树4

这个和上一个类似,但是它并不是平衡的。
因为没有考虑null值。
那什么时候需要考虑null呢,就是当叶子节点没有自己的兄弟的时候,这个时候就需要把null加进来考虑。

如果加入null值,如下图, 6到2的右孩子只有俩个黑色,而6到1或者8的孩子都是3个黑色,所以是不平衡的。

那为什么树3是平衡的红黑树呢?可以把null考虑进来,可以看出根到任意叶子节点的黑色数都是3,所以是平衡的。

判断是否平衡有一点经验总结。
如果叶子节点是红色,可以不用不用care。
如果叶子节点就一个黑色,没有兄弟节点,那肯定是不平衡的。
红色节点无所谓,黑色节点肯定要成对出现的。

code

package redblack;  
  
import com.sun.org.apache.regexp.internal.RE;  
  
public class RedBlackTree {  
    enum Color {  
        RED, BLACK;  
    }  
  
    private Node root;  
  
    private static class Node {  
        int key;  
        Object value;  
        Node left;  
        Node right;  
        Node parent;//父节点 因为红黑树的删除和新增经常用到父节点  
        Color color = Color.RED;//默认刚创建出来新节点是为红色  
  
        public Node(int key, Object value) {  
            this.key = key;  
            this.value = value;  
        }  
  
        //是否是左孩子 常用工具方法  
        boolean isLeftChild() {  
            //如果父节点不为空且父节点的left是自身则是左孩子  
            return parent != null && parent.left == this;  
        }  
  
        //叔叔 常用工具方法  
        Node uncle() {  
            if (parent == null || parent.parent == null) {  
                return null;//没有爷爷就没有叔叔  
            }  
            if (parent.isLeftChild()) {  
                return parent.parent.right;  
            } else {  
                return parent.parent.left;  
            }  
        }  
  
        //兄弟 常用工具方法  
        Node sibling() {  
            if (parent == null) {  
                return null;//没父亲就没兄弟  
            }  
            if (this.isLeftChild()) {  
                return parent.right;  
            } else {  
                return parent.left;  
            }  
        }  
    }  
  
    //判断红、黑  
    boolean isRed(Node node) {  
        return node != null && node.color == Color.RED;  
    }  
  
    boolean isBlack(Node node) {  
        return node == null || node.color == Color.BLACK;  
    }  
  
    //右选 1.parent的处理 2.旋转后新根的父子关系  
    private void rightRotate(Node pink) {  
        Node parent = pink.parent;//pink不平衡的节点  
        Node yellow = pink.left;//yellow新根  
        Node green = yellow.right;//green要换爹的  
        if (green != null) {  
            green.parent = pink;  
        }  
        yellow.right = pink;//顶上去  
        yellow.parent = parent;  
        pink.left = green;  
        pink.parent = yellow;  
        if (parent == null) {  
            root = yellow;  
        } else if (parent.left == pink) {  
            parent.left = yellow;  
        } else {  
            parent.right = yellow;  
        }  
    }  
  
    //左旋  
    private void leftRotate(Node pink) {  
        Node parent = pink.parent;//pink不平衡的节点  
        Node yellow = pink.right;//yellow新根  
        Node green = yellow.left;//green要换爹的  
        if (green != null) {  
            green.parent = pink;  
        }  
        yellow.left = pink;//顶上去  
        yellow.parent = parent;  
        pink.right = green;  
        pink.parent = yellow;  
        if (parent == null) {  
            root = yellow;  
        } else if (parent.right == pink) {  
            parent.right = yellow;  
        } else {  
            parent.left = yellow;  
        }  
    }  
  
    /**  
     * 新增或更新  
     * 找空位,找到之后根据k-v创建新的节点对象,然后根父节点建立好父子关系,新增操作就算完成了。  
     * 如果没有找到空位,就根据key的大小不断的向左找向右找。  
     * 如果找相同的key,就是更新。  
     * <p>  
     * 正常增,遇到红红不平衡进行调整  
     * 红红不平衡细分有四种case。  
     */  
    public void put(int key, Object value) {  
        Node p = root;  
        Node parent = null;//记录新增节点的父节点  
        while (p != null) {  
            parent = p;  
            if (key < p.key) {  
                p = p.left;  
            } else if (p.key < key) {  
                p = p.right;  
            } else {  
                p.value = value;//更新  
                return;  
            }  
        }  
        Node inserted = new Node(key, value);  
        if (parent == null) root = inserted;  
        else if (key < parent.key) {  
            parent.left = inserted;  
            inserted.parent = parent;//1.维护parent属性  
        } else {  
            parent.right = inserted;  
            inserted.parent = parent;//1.维护parent属性  
        }  
        fixRedRed(inserted);  
    }  
  
    //修复红红  
    void fixRedRed(Node x) {  
        //case 1.插入节点是根节点,变黑集合  
        if (x == root) {  
            x.color = Color.BLACK;  
            return;  
        }  
        //case 2.插入节点父亲是黑色,无需调整  
        if (isBlack(x.parent)) {  
            return;  
        }  
        /*  
        case 3.当红红相邻,叔叔为红色时  
        需要将父亲、叔叔变黑、祖父变红,然后对祖父做递归处理  
         */        Node parent = x.parent;  
        Node uncle = x.uncle();  
        Node grandparent = parent.parent;  
        if (isRed(uncle)) {  
            parent.color = Color.BLACK;  
            uncle.color = Color.BLACK;  
            grandparent.color = Color.RED;  
            fixRedRed(grandparent);  
        }  
  
        //case 4.  
        if (parent.isLeftChild() && x.isLeftChild()) {//LL  
            parent.color = Color.BLACK;  
            grandparent.color = Color.RED;  
            rightRotate(grandparent);  
        } else if (parent.isLeftChild() && !x.isLeftChild()) {//RL  
            leftRotate(parent);//使之变成LL case  
            x.color = Color.BLACK;  
            grandparent.color = Color.RED;  
            rightRotate(grandparent);  
        } else if (!parent.isLeftChild() && !x.isLeftChild()) {//RR  
            parent.color = Color.BLACK;  
            grandparent.color = Color.RED;  
            leftRotate(grandparent);  
        } else {//LR  
            rightRotate(parent);////使之变成RR case  
            x.color = Color.BLACK;  
            grandparent.color = Color.RED;  
            leftRotate(grandparent);  
        }  
    }  
  
    /**  
     * 查找删除节点  
     */  
    private Node find(int key) {  
        Node p = root;  
        while (p != null) {  
            if (key < p.key) {  
                p = p.left;  
            } else if (p.key < key) {  
                p = p.right;  
            } else {  
                return p;  
            }  
        }  
        return null;  
    }  
  
    /**  
     * 查找剩余节点  
     */  
    private Node findReplaced(Node deleted) {  
        if (deleted.left == null && deleted.right == null) {  
            return null;  
        }  
        if (deleted.left == null) {  
            return deleted.right;  
        }  
        if (deleted.right == null) {  
            return deleted.left;  
        }  
        //有俩个孩子,要找后继节点(右子树的最左)  
        Node s = deleted.right;  
        while (s.left != null) {  
            s = s.left;  
        }  
        return s;  
    }  
  
    /**  
     * 删除  
     * 正常删、会用到李代桃僵的技巧,遇到黑黑不平衡进行调整  
     * 黑黑不平衡细分有6种case。  
     */  
    public void remove(int key) {  
        Node deleted = find(key);  
        if (deleted == null) return;  
        doRemove(deleted);  
    }  
  
    private void doRemove(Node deleted) {  
        Node replaced = findReplaced(deleted);  
        Node parent = deleted.parent;  
        if (replaced == null) {//没有孩子  
            //case 1.删的根节点  
            if (deleted == root) {  
                root = null;  
            } else {  
                if (isBlack(deleted)) {//删除到节点时黑,剩余的也是黑,双黑(null也是黑)  
                    //复杂调整  
                    fixDoubleBlack(deleted);//先调整平衡,下面在删除  
                } else {  
                    //红色叶子,无需任何处理  
                }  
                //case 2.不是根节点 并没有孩子  
                if (deleted.isLeftChild()) {  
                    parent.left = null;  
                } else {  
                    parent.right = null;  
                }  
                deleted.parent = null;//help GC  
            }  
            return;  
        }  
        if (deleted.left == null || deleted.right == null) {//有一个孩子  
            if (deleted == root) {  
                //case 1.删的根节点  
                root.key = replaced.key;  
                root.value = replaced.value;  
                root.left = root.right = null;  
            } else {  
                //case 2.不是根节点 并有一个孩子  
                if (deleted.isLeftChild()) {  
                    parent.left = replaced;  
                } else {  
                    parent.right = replaced;  
                }  
                replaced.parent = parent;  
                deleted.left = deleted.right = deleted.parent = null;//help GC  
                if (isBlack(deleted) && isBlack(replaced)) {//删除到节点时黑,剩余的也是黑,双黑  
                    //复杂调整  
                    fixDoubleBlack(replaced);//上面先删除了,再调整  
                } else {  
                    //case 2  
                    replaced.color = Color.BLACK;  
                }  
            }  
            return;  
        }  
        //有俩个孩子,找到后继,替换,删除后继节点  
        int t = deleted.key;  
        deleted.key = replaced.key;  
        replaced.key = t;  
  
        Object v = deleted.value;  
        deleted.value = replaced.value;  
        replaced.value = v;  
        doRemove(replaced);  
    }  
  
    /**  
     * 处理双黑  
     * 删除到节点和剩下的节点都是黑,触发双黑,双黑的意思是,整个路径上少一个黑!需要调整  
     * case 3:被调整节点的兄弟为红,此时俩个侄子定为黑(过度情况,需要转换成4或5,通过旋转)  
     * case 4:被调整节点的兄弟为黑,俩个侄子都为黑  
     * case 5:被调整节点的兄弟为黑,至少一个红侄子  
     */  
    private void fixDoubleBlack(Node x) {  
        if (x == root) {//递归结束条件  
            return;  
        }  
        Node parent = x.parent;//父  
        Node sibling = x.sibling();//兄弟  
        //case 3:兄弟节点是红色  
        if (isRed(sibling)) {  
            if (x.isLeftChild()) {//旋转  
                leftRotate(parent);  
            } else {  
                rightRotate(parent);  
            }  
            parent.color = Color.RED;//换色  
            sibling.color = Color.BLACK;  
            fixDoubleBlack(x);  
            return;  
        }  
        //case 4:兄弟节点是黑色,俩个侄子都是黑  
        if (sibling != null) {  
            if (isBlack(sibling.left) && isBlack(sibling.right)) {  
                sibling.color = Color.RED;  
                if (isRed(parent)) {  
                    parent.color = Color.BLACK;  
                } else {  
                    fixDoubleBlack(parent);  
                }  
            } else {  
                //case 5:兄弟是黑色,但是侄子有红色  
                //LL 兄弟是左孩子,左侄子是红  
                if (sibling.isLeftChild() && isRed(sibling.left)) {  
                    rightRotate(parent);  
                    sibling.left.color = Color.BLACK;  
                    sibling.color = parent.color;  
                }  
                //LR 兄弟是左孩子,右侄子是红  
                else if (sibling.isLeftChild() && isRed(sibling.right)) {  
                    sibling.right.color = parent.color;  
                    leftRotate(sibling);  
                    rightRotate(parent);  
                }  
                //RR 兄弟是右孩子,右侄子是红  
                else if (!sibling.isLeftChild() && isRed(sibling.left)) {  
                    sibling.left.color = parent.color;  
                    rightRotate(sibling);  
                    leftRotate(parent);  
                }  
                //RL 兄弟是右孩子,左侄子是红  
                else {  
                    leftRotate(parent);  
                    sibling.right.color = Color.BLACK;  
                    sibling.color = parent.color;  
                }  
                parent.color = Color.BLACK;  
            }  
        } else {  
            fixDoubleBlack(parent);  
        }  
    }  
}

右旋

旋转前

旋转后

理解旋转,对照图,标记颜色有助于帮助理解,和代码实现。

📌旋转前: yellow是pink的左孩子;Node yellow = pink.left green是yellow的右孩子;Node green = yellow.right 📌旋转后: yellow要顶上去,成为新根; pink要下去,成为yellow的右孩子;yellow.right = pink 而且pink的左孩子变成了green; pink.left = green

到这一步,如果是AVL树就完事了,不过红黑树还要维护一个parent,🙃 还需要继续处理,并且要把新根yellow的父子关系直接在旋转这个方法里给搭建好;

接下来就把pink、yellow、green的parent属性处理好。

1️⃣ 处理green的parent: 旋转前,green的parent是yellow; 旋转后,green的parent是pink; 所以green的parent要重新赋值,当然green并不一定存在,所以需要一个判断: if(green != null) green.parent = pink;

2️⃣ 处理yellow的parent: 旋转前,yellow的parent是pink; 旋转后,yellow成了pink的parent; 该图列不存在5,8的parent,但是可能存在,所以, yellow.parent = pink.parent 等价于 yellow.parent = parent, 因为这个parent就是通过Node parent = pink.parent拿到的。

3️⃣ 处理pink的parent: 旋转后,pink的parent变成了yellow; 所以pink.parent = yellow

还有一件事,就是处理好新根的父子关系,该图例有些特殊,直接就是根了,看一下下面图例:

旋转前

旋转后

要维护yellow顶上去之后的parent父子关系。 旋转前,通过Node parent = pink.parent; 拿到之前的根。 旋转后,需要判断之前pink是它的父的左还是右孩子,就可以判断之后,决定yellow是之前根到左还是右,所以:

if(parent.left == pink){
	parent.left = yellow;
}else{
	parent.right = yellow;
}

最后再考虑之前的特殊情况,

pink是之前的根,它的parent是null,那去给它的left、right赋值肯定是有问题的,所以应该排除这种情况,直接把yellow作为root即可:

if(parent == null) root = yellow;
else if(parent.left == pink){//把上面的非根情况逻辑补充上
	parent.left = yellow;
}else{
	parent.right = yellow;
}

左旋

其实左旋类似右旋,只不过反过来。可以通过着色法搞一下,比对右旋加深理解。

新增或更新

新增的时候,跟基本的二叉搜索树一致,只不过多两件事

  • 1.要维护新增节点的parent属性
  • 2.要维护红红的不平衡,因为新增的是默认红色

红红不平衡,细分有四种case: 因为插入节点均视为红色🔴

  • case 1.插入节点为根节点,将根节点变黑⚫️
  • case 2.插入节点的父节点若为黑色⚫️,树的红黑性质不变,无序调整 插入节点的父节点为红色🔴,触发红红相邻
  • case 3.叔叔为红色🔴
    • 1.父亲变为黑色⚫️,为了保证黑色平衡,连带叔叔也变成黑色⚫️
    • 2.祖父如果是黑色不变,会造成这个子树黑色过多,因此祖父也变为红色🔴
    • 3.祖父如果变成红色,可能会接着触发红红相邻,因此继续对祖父进行递归调整
    • 4.直到根节点,如果root变为红色,改为红色就完成了
  • case 4.叔叔为黑色⚫️
    • 1.父亲为左孩子,插入节点也是左孩子,此时即LL不平衡
    • 2.父亲为左孩子,插入节点是右孩子,此时即LR不平衡
    • 3.父亲为右孩子,插入节点也是右孩子,此时即RR不平衡
    • 4.父亲为右孩子,插入节点是左孩子,此时即RL不平衡

删除

删除可能触发双黑的情况,触发双黑,双黑的意思是,整个路径上少一个黑!需要调整也有很多case如下:

  • case 0:如果删除节点有俩个孩子,化简成只有一个孩子或没有孩子(替换删除)
  • csae 1:删除到是根节点
  • case 2:删除的是黑⚫️,剩下的红🔴,剩下这个红节点变黑⚫️
  • case 3:被调整节点的兄弟为红🔴,此时俩个侄子定为黑⚫️
  • case 4:被调整节点的兄弟是黑⚫️,俩个侄子都为黑⚫️
  • case 5:被调整节点的兄弟是黑⚫️,至少一个红🔴侄子
    • 如果兄弟是左孩子,左侄子是红🔴,LL不平衡
    • 如果兄弟是左孩子,右侄子是红🔴,LR不平衡
    • 如果兄弟是右孩子,右侄子是红🔴,RR不平衡
    • 如果兄弟是右孩子,左侄子是红🔴,RL不平衡