数据结构讲解 - 红黑树

266 阅读11分钟

这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战

R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

红黑树的特点:

1.每个节点非红即黑;

2.根节点总是黑色的;

3.每个叶子节点都是黑色的空节点(NIL节点);

4.如果节点是红色的,则它的子节点必须是黑色的(反之不一定);

5.从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。

红黑树的应用

红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。
例如,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。

红黑树的时间复杂度

红黑树的时间复杂度为: O(lgn)

下面通过“数学归纳法”对红黑树的时间复杂度进行证明。

定理:一棵含有n个节点的红黑树的高度至多为2log(n+1) .

证明:
"一棵含有n个节点的红黑树的高度至多为2log(n+1)" 的逆否命题是 "高度为h的红黑树,它的包含的内节点个数至少为 2h/2-1个"。
我们只需要证明逆否命题,即可证明原命题为真;即只需证明 "高度为h的红黑树,它的包含的内节点个数至少为 2h/2-1个"。

从某个节点x出发(不包括该节点)到达一个叶节点的任意一条路径上,黑色节点的个数称为该节点的黑高度(x's black height),记为bh(x) 。关于bh(x)有两点需要说明: 
第1点:根据红黑树的"特性(5)  ,即从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点"可知,从节点x出发到达的所有的叶节点具有相同数目的黑节点。这也就意味着,bh(x)的值是唯一的
第2点:根据红黑色的"特性(4),即如果一个节点是红色的,则它的子节点必须是黑色的"可知,从节点x出发达到叶节点"所经历的黑节点数目">= "所经历的红节点的数目"。假设x是根节点,则可以得出结论"bh(x) >= h/2"。进而,我们只需证明 "高度为h的红黑树,它的包含的黑节点个数至少为 2bh(x)-1个"即可。

到这里,我们将需要证明的定理已经由
"一棵含有n个节点的红黑树的高度至多为2log(n+1)"
转变成只需要证明
"高度为h的红黑树,它的包含的内节点个数至少为 2bh(x)-1个"。


下面通过"数学归纳法"开始论证高度为h的红黑树,它的包含的内节点个数至少为 2bh(x)-1个"。

(01) 当树的高度h=0时,
内节点个数是0,bh(x) 为0,2bh(x)-1 也为 0。显然,原命题成立。

(02) 当h>0,且树的高度为 h-1 时,它包含的节点个数至少为 2bh(x)-1-1。这个是根据(01)推断出来的!

下面,由树的高度为 h-1 的已知条件推出“树的高度为 h 时,它所包含的节点树为 2bh(x)-1”。

当树的高度为 h 时,
对于节点x(x为根节点),其黑高度为bh(x)。
对于节点x的左右子树,它们黑高度为 bh(x) 或者 bh(x)-1。
根据(02)的已知条件,我们已知 "x的左右子树,即高度为 h-1 的节点,它包含的节点至少为 2bh(x)-1-1 个";

所以,节点x所包含的节点至少为 ( 2bh(x)-1-1 ) + ( 2bh(x)-1-1 ) + 1 = 2^bh(x)-1。即节点x所包含的节点至少为 2bh(x)-1。
因此,原命题成立。

由(01)、(02)得出,"高度为h的红黑树,它的包含的内节点个数至少为 2^bh(x)-1个"。
因此,“一棵含有n个节点的红黑树的高度至多为2log(n+1)”。

红黑树的Java基本操作

左旋和右旋

红黑树的基本操作是添加删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的节点之后,红黑树就发生了变化,可能不满足红黑树的5条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转,可以使这颗树重新成为红黑树。简单点说,旋转的目的是让树保持红黑树的特性。
旋转包括两种:左旋 和 右旋

添加

将一个节点插入到红黑树中,需要执行哪些步骤呢?首先,将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过旋转和重新着色等方法来修正该树,使之重新成为一颗红黑树。详细描述如下:

第一步: 将红黑树当作一颗二叉查找树,将节点插入。
红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。
好吧?那接下来,我们就来想方设法的旋转以及重新着色,使这颗树重新成为红黑树!

第二步:将插入的节点着色为"红色"。
为什么着色成红色,而不是黑色呢?为什么呢?在回答之前,我们需要重新温习一下红黑树的特性:
(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
将插入的节点着色为红色,不会违背"特性(5)"!少违背一条特性,就意味着我们需要处理的情况越少。接下来,就要努力的让这棵树满足其它性质即可;满足了的话,它就又是一颗红黑树了。o(∩∩)o...哈哈

第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。
第二步中,将插入节点着色为"红色"之后,不会违背"特性(5)"。那它到底会违背哪些特性呢?
对于"特性(1)",显然不会违背了。因为我们已经将它涂成红色了。
对于"特性(2)",显然也不会违背。在第一步中,我们是将红黑树当作二叉查找树,然后执行的插入操作。而根据二叉查找数的特点,插入操作不会改变根节点。所以,根节点仍然是黑色。
对于"特性(3)",显然不会违背了。这里的叶子节点是指的空叶子节点,插入非空节点并不会对它们造成影响。
对于"特性(4)",是有可能违背的!
那接下来,想办法使之"满足特性(4)",就可以将树重新构造成红黑树了。

删除

将红黑树内的某一个节点删除。需要执行的操作依次是:首先,将红黑树当作一颗二叉查找树,将该节点从二叉查找树中删除;然后,通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。详细描述如下:

第一步:将红黑树当作一颗二叉查找树,将节点删除。
这和"删除常规二叉查找树中删除节点的方法是一样的"。分3种情况:
① 被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就OK了。
② 被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。
③ 被删除节点有两个儿子。那么,先找出它的后继节点;然后把“它的后继节点的内容”复制给“该节点的内容”;之后,删除“它的后继节点”。在这里,后继节点相当于替身,在将后继节点的内容复制给"被删除节点"之后,再将后继节点删除。这样就巧妙的将问题转换为"删除后继节点"的情况了,下面就考虑后继节点。 在"被删除节点"有两个非空子节点的情况下,它的后继节点不可能是双子非空。既然"的后继节点"不可能双子都非空,就意味着"该节点的后继节点"要么没有儿子,要么只有一个儿子。若没有儿子,则按"情况① "进行处理;若只有一个儿子,则按"情况② "进行处理。

第二步:通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。
因为"第一步"中删除节点之后,可能会违背红黑树的特性。所以需要通过"旋转和重新着色"来修正该树,使之重新成为一棵红黑树。

红黑树的Java实现


package com.tntxia.basic;

public class RBTree <K extends Comparable<K>, V> {
    // 定义颜色
    private static final Boolean RED = true;
    private static final Boolean BLACK = false;

    // 定义红黑树的根root
    private RBNode root;

    public RBNode getRoot() {
        return root;
    }

    public void insert(K key, V value) {
        RBNode node = new RBNode();
        node.setKey(key);
        node.setValue(value);
        node.setColor(RED);
        insert(node);
    }

    private void insert(RBNode node) {
        // 1.找到插入的位置
        RBNode parent = null;
        RBNode x = this.root;

        while (x != null) {
            parent = x;

            //a > b 则返回 1,否则返回 -1 ,相等返回0
            int cmp = node.key.compareTo(parent.key);
            if (cmp > 0) {
                x = x.right;
            } else if (cmp == 0) {
                // 替换操作
                parent.setValue(node.value);
                return;
            } else {
                x = x.left;
            }
        }

        node.parent = parent;

        if (parent != null) {
            if (node.key.compareTo(parent.key) < 0)
                parent.left = node;
            else
                parent.right = node;
        } else {
            this.root = node;
        }

        // 插入过后需要进行红黑树平衡调整
        balanceRBTree(node);
    }
    /**
     * 插入后修复红黑树平衡的方法
     *     |---情景1:红黑树为空树
     *     |---情景2:插入节点的key已经存在
     *     |---情景3:插入节点的父节点为黑色
     *
     *     情景4 需要咱们去处理
     *     |---情景4:插入节点的父节点为红色
     *          |---情景4.1:叔叔节点存在,并且为红色(父-叔 双红)
     *          |---情景4.2:叔叔节点不存在,或者为黑色,父节点为爷爷节点的左子树
     *               |---情景4.2.1:插入节点为其父节点的左子节点(LL情况)
     *               |---情景4.2.2:插入节点为其父节点的右子节点(LR情况)
     *          |---情景4.3:叔叔节点不存在,或者为黑色,父节点为爷爷节点的右子树
     *               |---情景4.3.1:插入节点为其父节点的右子节点(RR情况)
     *               |---情景4.3.2:插入节点为其父节点的左子节点(RL情况)
     */
    private void balanceRBTree(RBNode node) {
        RBNode parent = parentOf(node);
        RBNode gparent = parentOf(parent);

        // 存在父节点,并且父节点为红色
        if (parent != null && parent.color == RED) {
            //父节点是红色的,那么一定存在爷爷节点

            // 父节点是爷爷节点的左子树
            if (parent == gparent.left) {
                RBNode uncle = gparent.right;

                // 叔叔节点存在,并且为红色,(父叔, 双红)
                if (uncle != null && uncle.color == RED) {
                    setBlack(parent);
                    setBlack(uncle);
                    setRed(gparent);
                    balanceRBTree(gparent);
                    return;
                }

                // 叔叔节点不存在,或者为黑色
                if (uncle == null || isBlack(uncle)) {

                    //插入节点为其父节点的左子节点(LL情况)=>
                    //变色(父节点变黑,爷爷节点变红),右旋爷爷节点
                    if (node == parent.left) {
                        setBlack(parent);
                        setRed(gparent);
                        rightRotate(gparent);
                    } else {
                        leftRotate(parent);
                        balanceRBTree(parent);
                        return;
                    }
                }
            } else {
                // 父节点是爷爷节点的右子树
                RBNode uncle = gparent.left;

                // 叔叔节点存在,并且为红色(父-叔 双红)
                if (uncle != null && uncle.color == RED) {
                    setBlack(parent);
                    setBlack(uncle);
                    setRed(gparent);
                    balanceRBTree(gparent);
                    return;
                }

                // 叔叔节点不存在或者为黑色
                if (uncle == null || uncle.color == BLACK) {
                    //插入节点为其父节点的右子节点(RR情况)=>
                    //变色(父节点变黑,爷爷节点变红),右旋爷爷节点
                    if (node == parent.right) {
                        setBlack(parent);
                        setRed(gparent);
                        leftRotate(gparent);
                    }

                    //插入节点为其父节点的左子节点(RL情况)
                    //右旋(父节点)得到RR情况,当前节点设置为父节点,进入下一次循环
                    if (node == parent.left) {
                        rightRotate(parent);
                        balanceRBTree(parent);
                        return;
                    }
                }

            }
        }

        setBlack(this.root);
    }

    /**
     * 左旋方法
     * 左旋示意图:左旋x节点
     *    p                   p
     *    |                   |
     *    x                   y
     *   / \         ---->   / \
     *  lx  y               x   ry
     *     / \             / \
     *    ly  ry          lx  ly
     *
     * 左旋做了几件事?
     * 1.将y的左子节点赋值给x的右边,并且把x设置为y的左子节点的父节点
     * 2.将x的父节点(非空时)指向y,更新y的父节点为x的父节点
     * 3.将y的左子节点指向x,更新x的父节点为y
     */
    private void leftRotate(RBNode x) {
        RBNode y = x.right;
        x.right = y.left;
        if (y.left != null) {
            y.left = x;
        }

        y.parent = x.parent;

        if (x.parent != null) {
            if (x == x.parent.left) {
                x.parent.left = y;
            } else {
                x.parent.right = y;
            }
        } else {
            this.root = y;
            this.root.parent = null;
        }

        y.left = x;
        x.parent = y;
    }

    /**
     * 右旋方法
     * 右旋示意图:右旋y节点
     *
     *    p                       p
     *    |                       |
     *    y                       x
     *   / \          ---->      / \
     *  x   ry                  lx  y
     * / \                         / \
     *lx  ly                      ly  ry
     *
     * 右旋都做了几件事?
     * 1.将x的右子节点 赋值 给了 y 的左子节点,并且更新x的右子节点的父节点为 y
     * 2.将y的父节点(不为空时)指向x,更新x的父节点为y的父节点
     * 3.将x的右子节点指向y,更新y的父节点为x
     */
    private void rightRotate(RBNode y) {
        RBNode x = y.left;
        y.left = x.right;
        if (x.right != null) {
            x.right.parent = y;
        }

        x.parent = y.parent;

        if (y.parent != null) {
            if (y == y.parent.left) {
                y.parent.left = x;
            } else {
                y.parent.right = x;
            }
        } else {
            this.root = x;
            this.root.parent = null;
        }

        x.right = y;
        y.parent = x;

    }
    /**
     * 中序打印,可以将二叉查找树有顺序的打印出来
     */
    public void inOrderPrint() {
        if(this.root != null) {
            inOrderPrint(this.root);
        }
    }

    private void inOrderPrint(RBNode node) {
        if (node != null) {
            inOrderPrint(node.left);
            System.out.println("key -> " + node.key + ", value -> " + node.value);
            inOrderPrint(node.right);
        }
    }

    private RBNode parentOf(RBNode node) {
        return node != null ? node.parent : null;
    }

    private Boolean isRed(RBNode node) {
        return node != null ? node.color == RED : false;
    }

    private void setRed(RBNode node) {
        if (node != null) {
            node.color = RED;
        }
    }

    private Boolean isBlack(RBNode node) {
        return node != null ? node.color == BLACK : false;
    }

    private void setBlack(RBNode node) {
        if (node != null) {
            node.color = BLACK;
        }
    }

    /**
     * 红黑树节点 Node
     */
    static class RBNode <K extends Comparable<K>, V> {
        private RBNode parent;
        private RBNode left;
        private RBNode right;
        private boolean color;
        private K key;
        private V value;

        public RBNode getParent() {
            return parent;
        }

        public void setParent(RBNode parent) {
            this.parent = parent;
        }

        public RBNode getLeft() {
            return left;
        }

        public void setLeft(RBNode left) {
            this.left = left;
        }

        public RBNode getRight() {
            return right;
        }

        public void setRight(RBNode right) {
            this.right = right;
        }

        public boolean isColor() {
            return color;
        }

        public void setColor(boolean color) {
            this.color = color;
        }

        public K getKey() {
            return key;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }

        public RBNode() {
        }

        public RBNode(RBNode parent, RBNode left, RBNode right, boolean color, K key, V value) {
            this.parent = parent;
            this.left = left;
            this.right = right;
            this.color = color;
            this.key = key;
            this.value = value;
        }
    }
}