跟着TreeMap学红黑树

跟着TreeMap学红黑树

最近面试被问了两三次红黑树,感觉红黑树还是有必要好好学一下的,因为TreeMap是用红黑树实现的,所以打算跟着TreeMap的源码学习一下红黑树,基于jdk1.8源码。

红黑树的介绍

红黑树是一种类平衡的树,红黑树和平衡树的区别是:平衡树是严格平衡的,对于任意一个节点n,n的左子树和右子树的高度差不得超过1,红黑树是非严格平衡的,对于任意一个节点,从当前节点开始,向左的最长路径记为ll,向右的最长路径记为lr,假设ll<lr,那么lr-ll<=ll,反之同理。

红黑树有以下几条性质(规则)
1.任意节点要么为红色,要么为黑色
2.根节点颜色为黑色
3.对于每个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点
4.红色节点不能连续,及红色节点的父节点和子节点不能为红色
5.叶子节点(称为null节点或nil节点)默认为黑色

TreeMap的源码

因为红黑树在进行插入或者删除时,可能会破坏上述的规则,所以需要调整,与调整有关的方法主要有4个,分别为rotateLeft(Entry<K,V> p),rotateRight(Entry<K,V> p),fixAfterInsertion(Entry<K,V> x),fixAfterDeletion(Entry<K,V> x)。所以这四个方法是看源码过程中最重要的四个方法,也比较有难度。

红黑树的节点

private static final boolean RED   = false;
private static final boolean BLACK = true;
static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;//键值
    V value;//value值
    Entry<K,V> left;//指向左子节点的指针
    Entry<K,V> right;//指向右子节点的指针
    Entry<K,V> parent;//指向父节点的指针
    boolean color = BLACK;//节点的颜色,因为有两种取值,所以用boolean表示
                          //BLACK为true,RED为false,默认为黑

    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }
   
    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
     
    public V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;

        return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
    }

    public int hashCode() {
        int keyHash = (key==null ? 0 : key.hashCode());
        int valueHash = (value==null ? 0 : value.hashCode());
        return keyHash ^ valueHash;
    }

    public String toString() {
        return key + "=" + value;
    }
}

从上可以看出,因为map中存放的是键值对,所以Entry中有key和value,此外,由于要实现红黑树,所以需要用一个布尔值color来表示颜色,另外需要三个指针,分别指向当前节点的父节点,左子节点和右子节点。

左旋和右旋

因为在调整红黑树时需要左旋和右旋,所以先来讲解一下这两个操作
左旋的过程是将x的右子树绕x逆时针旋转,使得x的右子树成为x的父亲,同时修改相关节点的指针。旋转之后,二叉查找树的属性仍然满足。
左旋过程:
在这里插入图片描述
左旋举例:
在这里插入图片描述
左旋代码:
左旋在TreeMap中是通过rotateLeft(Entry<K,V> p)函数来实现的,下面贴上代码:

    private void rotateLeft(Entry<K,V> p) {
        if (p != null) {
            Entry<K,V> r = p.right;
            p.right = r.left;
            if (r.left != null)
                r.left.parent = p;
            r.parent = p.parent;
            if (p.parent == null)
                root = r;
            else if (p.parent.left == p)
                p.parent.left = r;
            else
                p.parent.right = r;
            r.left = p;
            p.parent = r;
        }
    }

上述代码实现的功能是:
在这里插入图片描述
下面详细分析一下上述代码,代码可分为3个步骤来理解:
1)4-6行功能是处理r的左子节点,
2)7-13行的功能是处理r的父节点,
3)14-15行的功能是处理p和r的关系。
接着详细分析一下指针的改变过程,这里画出所有的指针,以及指针改变的过程,为了解释方便,画出p的父节点pp,并假设p为pp的左子节点,因为左旋过程只是改变指针,所以这里没有考虑颜色
初始状态:
在这里插入图片描述
步骤1结束之后的状态(去掉了两条指针,新增了两条指针):
在这里插入图片描述
步骤2结束之后的状态(也是去掉了两条指针,新增了两条指针):
在这里插入图片描述
现在有必要整理一下上图,调整某些节点的位置,已经去掉的指针不再显示:
在这里插入图片描述
可以看到,只有两条指针的指向不正确,而这正是步骤3所做的工作。
步骤3结束之后的状态,即为最终结果:
在这里插入图片描述
右旋与左旋类似,如果左旋明白,右旋代码应该可以写出来,这里不再详细介绍。
右旋过程与举例:
在这里插入图片描述
在这里插入图片描述
右旋代码:

    private void rotateRight(Entry<K,V> p) {
        if (p != null) {
            Entry<K,V> l = p.left;
            p.left = l.right;
            if (l.right != null) l.right.parent = p;
            l.parent = p.parent;
            if (p.parent == null)
                root = l;
            else if (p.parent.right == p)
                p.parent.right = l;
            else p.parent.left = l;
            l.right = p;
            p.parent = l;
        }
    }

插入操作

代码:

    public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);//重点
        size++;
        modCount++;
        return null;
    }

插入的步骤就是找到适当的位置,然后插入这个节点,如果小,往左走,如果大,往右走,如果相等,覆盖旧的value并返回旧的value值。
最重要的是在插入之后要执行fixAfterInsertion(e);这行代码,它的作用就是如果需要的话,调整红黑树,使其满足所有的规则。下面看代码。
代码:

    private void fixAfterInsertion(Entry<K,V> x) {
        x.color = RED;//将当前节点涂红(初始值为黑),为了满足规则3

        while (x != null && x != root && x.parent.color == RED) {//如果x和其父节点都为红色,进入while循环
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {//如果x的父节点为x的祖父节点的左子节点,分三种情况讨论
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));//取x的叔叔节点,即x的祖父节点的右子节点,记为y
                if (colorOf(y) == RED) {//情况1:y为红色
                    setColor(parentOf(x), BLACK);//将x的父节点涂黑
                    setColor(y, BLACK);//将y涂黑
                    setColor(parentOf(parentOf(x)), RED);//将x的祖父节点涂红
                    x = parentOf(parentOf(x));//将x指向x的祖父节点,开始下一轮while循环
                } else {
                    if (x == rightOf(parentOf(x))) {//情况2:y为黑色,且x为其父节点的右子节点
                        x = parentOf(x);//将x指向x的父节点
                        rotateLeft(x);//对x进行左旋
                    }
                    //情况3:y为黑色,且x为其父节点的左子节点,可以看出,执行情况2之后就会接着执行情况3
                    setColor(parentOf(x), BLACK);//将x的父节点涂黑
                    setColor(parentOf(parentOf(x)), RED);//将x的祖父节点涂红
                    rotateRight(parentOf(parentOf(x)));//对x的祖父节点进行右旋
                }
            } else {//与上面的三种情况对称,不再细说
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        root.color = BLACK;//最后,确保根节点为黑色(规则2)
    }

最后,举几个例子,走一下上面的代码
假设初始状态为下图,满足情况1(x和其父节点都为红色,x的叔叔节点为红色):
在这里插入图片描述
那么,执行情况1的几个步骤,即将5涂黑,8涂黑,7涂红,x指向7,结果如下:
在这里插入图片描述
现在,发现当前状态并不满足跳出循环的条件(x和其父节点都为红),进入循环,发现满足情况2(x和其父节点都为红色,x的叔叔节点为黑色,x为其父节点 的右子节点),执行情况2的几个步骤,即将x指向x的父节点(即x指向2),然后对2进行左旋,结果如下:
在这里插入图片描述
发现现在的状态满足情况3(x和其父节点都为红色,x的叔叔节点为黑色,x为其父节点 的左子节点),而代码的逻辑也是紧接着就执行情况3,完美,那么现在执行情况3的几个步骤,即将7涂黑,11涂红,对11进行右旋,结果如下:
在这里插入图片描述
现在发现,当前状态不满足循环条件,退出循环,将根节点涂黑(确保根节点为黑色,这个例子中本就是黑色,但有可能出现退出循环之后,根节点为红色的情况),调整结束。也可以检查一下当前状态,发现红黑树的规则都是满足的。
至此,插入过程的调整已经讲完,总结一下可能的调整路径
满足情况1时,调整路径可能为1->2->3(及刚才的例子),1->3,1这三种
满足情况2时,调整路径只能为2->3
满足情况3时,调整路径只能为3
下面,对1->3这条路径再找一个例子(其他路径的例子都很好找,不再举例):
在这里插入图片描述
最后,再总结一下,三种情况及对应的执行步骤:
情况1:x和父节点都为红色,叔叔节点为红色
执行步骤:将父节点和叔叔节点涂黑,祖父节点涂红,将x指向祖父节点
情况2:x和父节点都为红色,叔叔节点为黑色,x为其父节点的右子节点
执行步骤:将x指向父节点,对x进行左旋
情况3:x和父节点都为红色,叔叔节点为黑色,x为其父节点的左子节点
执行步骤:将父节点涂黑,祖父节点涂红,对祖父节点进行右旋

注意:以上总结都是针对x的父节点为祖父节点的左子节点而言的,对另一种对称的情况,即x的父节点为祖父节点的右子节点,是一样的道理,本文没有讨论。

至此,红黑树的插入及插入之后的调整已经介绍完毕,删除及删除之后的调整过几天再说吧,今天不想看了,哈哈。

参考

【数据结构和算法05】 红-黑树(看完包懂~)
史上最清晰的红黑树讲解(上)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值