史上最清晰的红黑树讲解(上)

521 阅读12分钟
原文链接: www.cnblogs.com
CarpenterLee
技术只是工具,重要的是人才! 博客园   首页   新随笔       管理 随笔-27  评论-156  文章-0 

史上最清晰的红黑树讲解(上)

本文github地址

本文以Java TreeMap为例,从源代码层面,结合详细的图解,剥茧抽丝地讲解红黑树(Red-Black tree)的插入,删除以及由此产生的调整过程。

总体介绍

Java TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的评判可以通过其本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator)。

TreeMap底层通过红黑树(Red-Black tree)实现,也就意味着containsKey(), get(), put(), remove()都有着log(n)的时间复杂度。其具体算法实现参照了《算法导论》。

TreeMap_base.png

出于性能原因,TreeMap是非同步的(not synchronized),如果需要在多线程环境使用,需要程序员手动同步;或者通过如下方式将TreeMap包装成(wrapped)同步的:

SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

红黑树是一种近似平衡的二叉查找树,它能够确保任何一个节点的左右子树的高度差不会超过二者中较低那个的一陪。具体来说,红黑树是满足如下条件的二叉查找树(binary search tree):

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点必须是黑色
  3. 红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色)。
  4. 对于每个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点。

在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件3或条件4,需要通过调整使得查找树重新满足红黑树的条件。

预备知识

前文说到当查找树的结构发生改变时,红黑树的条件可能被破坏,需要通过调整使得查找树重新满足红黑树的条件。调整可以分为两类:一类是颜色调整,即改变某个节点的颜色;另一类是结构调整,集改变检索树的结构关系。结构调整过程包含两个基本操作:左旋(Rotate Left),右旋(RotateRight)

左旋

左旋的过程是将x的右子树绕x逆时针旋转,使得x的右子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。

TreeMap_rotateLeft.png

TreeMap中左旋代码如下:

//Rotate Left
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;
    }
}

右旋

右旋的过程是将x的左子树绕x顺时针旋转,使得x的左子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。

TreeMap_rotateRight.png

TreeMap中右旋代码如下:

//Rotate Right
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;
    }
}

方法剖析

get()

get(Object key)方法根据指定的key值返回对应的value,该方法调用了getEntry(Object key)得到相应的entry,然后返回entry.value。因此getEntry()是算法的核心。算法思想是根据key的自然顺序(或者比较器顺序)对二叉查找树进行查找,直到找到满足k.compareTo(p.key) == 0entry

TreeMap_getEntry.png

具体代码如下:

//getEntry()方法
final Entry<K,V> getEntry(Object key) {
    ......
    if (key == null)//不允许key值为null
        throw new NullPointerException();
    Comparable<? super K> k = (Comparable<? super K>) key;//使用元素的自然顺序
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)//向左找
            p = p.left;
        else if (cmp > 0)//向右找
            p = p.right;
        else
            return p;
    }
    return null;
}

put()

put(K key, V value)方法是将指定的key, value对添加到map里。该方法首先会对map做一次查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似于getEntry()方法;如果没有找到则会在红黑树中插入新的entry,如果插入之后破坏了红黑树的约束,还需要进行调整(旋转,改变某些节点的颜色)。

public V put(K key, V value) {
    ......
    int cmp;
    Entry<K,V> parent;
    if (key == null)
        throw new NullPointerException();
    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);//创建并插入新的entry
    if (cmp < 0) parent.left = e;
    else parent.right = e;
    fixAfterInsertion(e);//调整
    size++;
    return null;
}

上述代码的插入部分并不难理解:首先在红黑树上找到合适的位置,然后创建新的entry并插入(当然,新插入的节点一定是树的叶子)。难点是调整函数fixAfterInsertion(),前面已经说过,调整往往需要1.改变某些节点的颜色,2.对某些节点进行旋转。

TreeMap_put.png

调整函数fixAfterInsertion()的具体代码如下,其中用到了上文中提到的rotateLeft()rotateRight()函数。通过代码我们能够看到,情况2其实是落在情况3内的。情况4~情况6跟前三种情况是对称的,因此图解中并没有画出后三种情况,读者可以参考代码自行理解。

//红黑树调整函数fixAfterInsertion()
private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED;
    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {//如果y为null,则视为BLACK
                setColor(parentOf(x), BLACK);              // 情况1
                setColor(y, BLACK);                        // 情况1
                setColor(parentOf(parentOf(x)), RED);      // 情况1
                x = parentOf(parentOf(x));                 // 情况1
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);                       // 情况2
                    rotateLeft(x);                         // 情况2
                }
                setColor(parentOf(x), BLACK);              // 情况3
                setColor(parentOf(parentOf(x)), RED);      // 情况3
                rotateRight(parentOf(parentOf(x)));        // 情况3
            }
        } else {
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);              // 情况4
                setColor(y, BLACK);                        // 情况4
                setColor(parentOf(parentOf(x)), RED);      // 情况4
                x = parentOf(parentOf(x));                 // 情况4
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);                       // 情况5
                    rotateRight(x);                        // 情况5
                }
                setColor(parentOf(x), BLACK);              // 情况6
                setColor(parentOf(parentOf(x)), RED);      // 情况6
                rotateLeft(parentOf(parentOf(x)));         // 情况6
            }
        }
    }
    root.color = BLACK;
}

remove()

remove(Object key)的作用是删除key值对应的entry,该方法首先通过上文中提到的getEntry(Object key)方法找到key值对应的entry,然后调用deleteEntry(Entry<K,V> entry)删除对应的entry。由于删除操作会改变红黑树的结构,有可能破坏红黑树的约束,因此有可能要进行调整。

有关remove()的具体讲解将放到下一篇博文当中,敬请期待!

posted on 2016-05-18 07:49 CarpenterLee 阅读(37255) 评论(20) 编辑 收藏
评论: #1楼 2016-05-18 08:32 | 大新博客  
牛逼 支持(0)反对(0)   #2楼 2016-05-18 09:19 | Edison Chou  
32个赞!已推荐 支持(1) 反对(0) http://pic.cnblogs.com/face/381412/20161204141727.png   #3楼[楼主] 2016-05-18 09:30 | CarpenterLee  
@ Edison Chou
多谢啦! 支持(0) 反对(0) http://pic.cnblogs.com/face/939998/20170411210156.png   #4楼 2016-05-18 09:40 | 菜鸟奋斗史  
正需要,先收藏啦,有空细读 支持(0)反对(0) http://pic.cnblogs.com/face/653266/20160312192620.png   #5楼 2016-05-18 10:37 | rsyy  
写得真好!赞 --------journey_fan 支持(0)反对(0)   #6楼 2016-05-18 10:42 | 拉拉叟  
分享个在线生成红黑树的链接
sandbox.runjs.cn/show/2nngvn… 支持(0)反对(0) http://pic.cnblogs.com/face/u63907.jpg?id=23134257   #7楼 [楼主] 2016-05-18 10:56 | CarpenterLee  
@ 拉拉叟
这个挺好玩的,要是能把平衡过程体现的再细致一些就更好了。 支持(0)反对(0) http://pic.cnblogs.com/face/939998/20170411210156.png   #8楼 2016-05-18 11:02 | 拉拉叟  
@ CarpenterLee
对啊 当初做的时候 想做成增加一个节点,中间不是有几步慢慢平衡的过程吗? 想这些过程分解成一个一个图,这样方便大家理解平衡的过程,结果遇到了javascript的一个难题(一个类似缓存问题。。具体问题都忘记了),不知道怎么解决。我对javascript不是很熟,等忙完手上项目,再学习下javascript再去解决吧 支持(0) 反对(0) http://pic.cnblogs.com/face/u63907.jpg?id=23134257   #9楼[楼主] 2016-05-18 12:06 | CarpenterLee  
@ 拉拉叟
期待~~ 支持(0) 反对(0) http://pic.cnblogs.com/face/939998/20170411210156.png   #10楼[楼主] 2016-05-18 12:08 | CarpenterLee  
@ rsyy
学姐多来逛逛啊:) 支持(0)反对(0) http://pic.cnblogs.com/face/939998/20170411210156.png   #11楼 2016-05-18 14:10 | 马三小伙儿  
学习啦! 支持(0)反对(0) http://pic.cnblogs.com/face/798142/20151022140256.png   #12楼 2016-05-18 14:29 | rsyy  
@ CarpenterLee
找回了用户名,写了一篇,发现不怎么好用,还是用csdn了 支持(0)反对(0)   #13楼 2016-06-11 22:43 | 拉拉叟  
@ CarpenterLee
完成了 sandbox.runjs.cn/show/2nngvn… 支持(0)反对(0) http://pic.cnblogs.com/face/u63907.jpg?id=23134257   #14楼[楼主] 2016-06-12 09:07 | CarpenterLee  
@ 拉拉叟
点赞!! 支持(0)反对(0)http://pic.cnblogs.com/face/939998/20170411210156.png   #15楼 2016-07-22 22:56 | internHe  
“它能够确保任何一个节点的左右子树的高度差不会超过二者中较低那个的一陪.”
错别字? 支持(0)反对(0)   #16楼 2016-10-23 19:44 | StupidTortoise  
楼主,能说下图片是用什么软件画的吗? 支持(0) 反对(0)   #17楼[楼主] 2016-10-24 12:30 | CarpenterLee  
@ StupidTortoise
是一个叫做dia的矢量图软件,类似于windows下的visio。下载地址: sourceforge.net/projects/di… 支持(0)反对(0)http://pic.cnblogs.com/face/939998/20170411210156.png   #18楼 2016-10-24 14:50 | StupidTortoise  
@ CarpenterLee
好的,谢谢^_^ 支持(0)反对(0)   #19楼 2017-04-21 17:12 | 程序人生0407  
学习了 分析的很到位!!! 赞一个!! 支持(0)反对(0)http://pic.cnblogs.com/face/731200/20161121101030.png   #20楼39066042018/2/9 16:39:05 2018-02-09 16:39 | 明渊阁  
总体不错。
情况1,情况2,情况3.。。。
这些没有总结说明,让读者用立体思维去思考这些情况吗 支持(0)反对(0)   刷新评论刷新页面返回顶部 注册用户登录后才能发表评论,请 登录注册访问网站首页。 【推荐】超50万VC++源码: 大型组态工控、电力仿真CAD与GIS源码库!
【推荐】如何快速搭建人工智能应用?
【大赛】2018首届“顶天立地”AI开发者大赛
腾讯云0710 最新IT新闻:
· 淘宝十大夏日神器出炉:脸基尼排第七 超一半被男士购买
· 蓝箭航天:朱雀二号火箭研发完毕,计划2020年首飞
· 英国Openreach宣布将携手华为诺基亚部署FTTH网络
· Firefox用Clang编译器编译Windows版本
· 苹果3D摄像头的方案 让安卓器件供应商奥比迎来曙光
» 更多新闻... 最新知识库文章:
· 危害程序员职业生涯的三大观念
· 断点单步跟踪是一种低效的调试方法
· 测试 | 让每一粒尘埃有的放矢
· 从Excel到微服务
· 如何提升你的能力?给年轻程序员的几条建议
» 更多知识库文章... --更多内容欢迎访问-- 博主github主页 昵称:CarpenterLee
园龄:2年2个月
粉丝:243
关注: 2 +加关注
< 2018年7月 >
24 25 26 27 28 29 30
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31 1 2 3 4

最新随笔

随笔分类

最新评论

阅读排行榜

Powered by: 博客园 模板提供:沪江博客 Copyright ©2018 CarpenterLee