数据结构之红黑树

红黑树

什么是红黑树

红黑树是一种常见的自平衡二分搜索树,常用于关联数组、字典,在各种语言的底层实现中被广泛应用。

满足红黑树的五个性质

  1. 每个节点是红色或者是黑色
  2. 根节点是黑色
  3. 每一个叶子节点(最后的空节点)是黑色
  4. 如果一个节点是红色的,那么它的孩子节点都是黑色的
  5. 从任意一个节点到叶子节点,经过的黑色节点是一样的

2-3树的定义

红黑树和2-3树有相似之处,我们先了解一下什么是2-3树

满足二分搜索树的基本性质,但是2-3树并不是二叉树

2-3树有两个节点,一个可以存放1个元素,一个可以存放2个元素

满足2-3树的三个性质

  1. 满足二叉搜索树的性质
  2. 节点可以存放一个或两个元素
  3. 每个节点有两个或三个子节点

每个节点都有2个子节点或者3个子节点,所以就叫做2-3树

左边节点都比42小,而右边节点都比42大,所以是满足二分搜索树的性质。注意,13和33这个节点中间是18,是比17大同时比33小。

2-3树的绝对平衡性

对于2-3树来说任意一个节点,左右子树的高度一定是相等的,而类似像AVL这种平衡二叉树左右子树的高度相差不超过1。

而2-3树是如何维持绝对平衡性,就要从它的添加操作说起。

对于一颗空树我们添加一个节点42,他既是根节点也是叶子节点,这个时候如果我们想再添加一个节点37应该添加到那里呢?

按平衡二叉树结构的逻辑来讲37比42小,所以添加到42的左孩子的位置。

但是在2-3树的中并不是这样的,

在2-3中我们依然是从根节点出发,请注意在2-3树中新的节点永远不会添加进空的位置

发现42节点的左子树为空,2-3树就会找到最后一个叶子节点上也就是42的位置,这时候37就会和42进行融合,从一个2节点变为了3节点。这个时候还是一颗绝对平衡的二叉树。

如果这个时候我们再添加一个12节点的话,因为12节点比37小,所以去找37的左孩子,但是我们已经知道,37的左孩子为空,而2-3并不会将新的节点添加到空的位置中去,这个时候我们就会变成4节点

但是在2-3中最多支持3个节点所以这个时候就会进行分裂,同时保持绝对平衡。

如果这个时候我们再添加一个18节点,我们可以很清楚的知道18会找到12节点但是12节点的子节点都为空,所以18就会和12节点进行融合。

如果这个时候我们再添加一个6节点,大家很清楚的知道是会和12节点、18节点进行融合,这个时候变成了4节点,所以要进行分裂成为下图这样。

但是!这样我们就不是绝对平衡的二叉树了!

这个时候就需要我们12节点去找他的父节点,发现12的父节点37是一个2节点,这个时候就简单了,我们只需要让12和37节点进行融合为3节点,然后让6变成12和37的左孩子,让18节点变成中间孩子节点即可!

这个时候我们依次加入5和11节点,就会变成下图这样。

因为不满足2-3树的性质,所以我们还是将他们进行分裂。

但是这个时候又不是绝对平衡了,所以我们需要让6去找父亲节点,但是父亲节点已经是3节点了,不过没关系,我们照样把他融合进去成下图这样。

这个时候我们需要进行分裂,我们需要把5和11分别放在6的左孩子和右孩子的位置,37也同理,形成下图这样。依然保持着绝对平衡性。

其实我们可以分为两种情况去理解,一种就是当前节点为3节点而父节点为2节点。

另一种就是当前节点为3节点,父节点也为3节点的情况。

红黑树和2-3树的等价性

在2-3树种,我们可以表示2个节点和3个节点。

在红黑树中,我们可以这样表示,然后用红色的线来表示bc是融合的,同时b是比c小的。由于红色节点是由3节点拆分而来,因此所有的红色节点都只会出现在左子树上。

在下图中只有6和17和66是红色节点,我们很容易得知,因为他们都是3节点。

如果抽象的话可以红色节点上移一下,我们可以看到其实是和2-3树是一样的。

保持根节点为黑色和左旋

在下图上,我们知道最后分裂以后,6继续会向上进行融合,我们知道向上融合的节点一定是红色的,所以我们知道6是红色的,但发现已经没有父亲节点的时候,我们将6变为黑色。

最开始的时候我们想添加一个42的话,我们直接让42当根节点就好了,然后我们需要让42变为黑色节点。

这个时候我们添加一个37,可以直接放入到42左孩子的位置。

但是,如果我们要插入的节点为根节点的右孩子的节点的时候,我们需要进行左旋。

伪代码如下:

node.right = x.left;
x.left = node;
x.color = node.color;
node.color = red;

Java代码如下:

    //   node                     x
    //  /   \     左旋转         /  \
    // T1   x   --------->   node   T3
    //     / \              /   \
    //    T2 T3            T1   T2
    private Node leftRotate(Node node){
        Node x = node.right;
        node.right = x.left;
        x.left = node;
        x.color = node.color;
        node.color = RED;
        return x;
    }

颜色翻转和右旋

最开始的时候我们想添加一个42的话,我们直接让42当根节点就好了,然后我们需要让42变为黑色节点。

这个时候我们添加一个37,可以直接放入到42左孩子的位置。

如果这时候我们再向红黑树中加入66节点,会变成如下图这样。

相应的在2-3树中是这个样子。

在红黑树中红色节点的意思就是和父节点是融合在一起的。

而37和66都是红色节点,说明它们都和父亲节点是融合在一起的。

在2-3树中,我们处理4节点的时候会进行分裂,分裂为3个2节点。

但是在红黑树中其实黑色节点就是2个节点,我们不需要进行旋转操作,我们只需要让37和66进行变色为黑色,就和2-3树中节点的性质是一样的了。

在2-3树中,我们需要继续让42节点向上去找父亲节点进行融合,相应的在红黑树中我们只需要让42变为红色即可。

这个就是颜色翻转(flipColors)

     //颜色翻转
    private void flipColors(Node node){
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

另外一种情况就是,我们树添加成这个样子。我们添加了12节点,其实可以理解为它们是融合在一起的。在2-3树种就是4节点,然后将它进行分裂。

但是在红黑树中我们怎么变成这个样子呢?其实很简单,我们只需要进行右旋转即可。

为了方便理解,我们引入T1和T2虚拟节点来演示。

我们将x的右节点t1放到node的左孩子上,然后将node放在x的右孩子的位置即可。

别忘了,在这个时候node(42)节点其实是融合在x(37)节点上的,所以它要改为红色,37要改为黑色。

对应的伪代码:

node.left = t1;
x.right = node;
x.color = node.color;
node.color = red;

最终我们就可以得到和2-3树结构一样的红黑树了。

    //       node                            x
    //       / \                           /   \
    //      x   T2     向右旋转 (y)        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;
    }

在红黑树中添加新元素

其实还有一种情况我们在前面没有讲到,就是在下图中添加一个元素40应该怎么做。

根据二分搜索树的性质,我们会将40添加进37的右孩子的位置。

但是这个时候已经破坏了红黑树的性质,所以我们需要进行左旋。

这个时候我们还需要进行右旋。

然后我们需要将40变黑色,42变为红色。

最后我们进行颜色翻转即可维持红黑树的性质。

总结图片如下:

如果我们添加的第三个元素是最小的话,我们可以直接从右旋开始。

反之我们添加的如果是最大的元素,我们可以直接进行颜色翻转。

    // 判断节点node的颜色
    private boolean isRed(Node node){
        if(node == null) {
            return BLACK;
        }
        return node.color;
    }


    //   node                     x
    //  /   \     左旋转         /  \
    // T1   x   --------->   node   T3
    //     / \              /   \
    //    T2 T3            T1   T2
    private Node leftRotate(Node node){
        Node x = node.right;
        node.right = x.left;
        x.left = node;
        x.color = node.color;
        node.color = RED;
        return x;
    }

    //颜色翻转
    private void flipColors(Node node){
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

    //       node                            x
    //       / \                           /   \
    //      x   T2     向右旋转 (y)        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;
    }

    // 向红黑树中添加新的元素(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 = leftRotate(node);
        }
        // 左孩子为红色,并且左孩子的左孩子也是红色,进行右旋
        if(isRed(node.left) && isRed(node.left.left)){
            node = rightRotate(node);
        }
        //颜色翻转
        if(isRed(node.left) && isRed(node.right)){
            flipColors(node);
        }

        return node;
    }

时间复杂度分析

红黑树相比于AVL树,其实是牺牲了平衡性的,红黑树并不完全满足平衡二叉树的定义,红黑树的最大高度达到了2logn的高度,红色节点影响了红黑树的的平衡性。红黑树虽然牺牲了一定的查询性能,但是在增删改操作的性能得到了弥补,红黑树的综合性能还是要优于AVL树的。

你可能感兴趣的:(java,数据结构)