面试官 :小桂子是吧,看你简历上写着精通 java 编程,想必对 java 已经掌握的很好了吧?
小桂子 :系呀系呀,一直都用 java 写 bug 呢~
面试官 :那你说说 jdk1.7 以前 HashMap 的底层实现原理呗,另外为何在高并发场景下可能形成较高的 CPU 占用?
小桂子 :这个。。。好像是红黑树?
面试官 :哦?你说的是 jdk1.8 以后的设计,既然你提到了,那就聊聊红黑树这个数据结构吧,这里是白纸和笔,手写一棵吧!
小桂子 :哎呀,哎呀哎呀,老师,忽然肚子好疼,我要去一下厕所,一下子就回来~~~html
面试到处是套路呀。。。不知道你是否有和小桂子同样尴尬的面试经历呢,若是有的话欢迎到评论区留言,说出你的故事~java
接下来咱们进入正题,开始探究面试官为难小桂子的红黑树。说到红黑树,大部分人应该对他既熟悉又陌生,熟悉是由于咱们天天 coding 都会直接或者间接的用到它,可是设计和实现上的复杂性又让不少人对其原理望而却步。红黑树的定义比较简单,无非是在插入和删除的过程当中自平衡规则多了一些,不过再多也只是个位数而已,只要静下心来跟随本文,相信你会有所收获,let's moving...git
接下去的篇幅小编假设你已经对二叉树、平衡二叉树的结构、做用,以及弊端有必定的了解。github
红黑树(如上图,引用自维基百科)是一种 自平衡 的二叉树,所谓的自平衡是指在插入和删除的过程当中,红黑树会采起必定的策略对树的组织形式进行调整,以尽量的减小树的高度,从而节省查找的时间。红黑树的特性以下:面试
- 结点是红色或黑色
- 根结点始终是黑色
- 叶子结点(NIL 结点)都是黑色
- 红色结点的两个直接孩子结点都是黑色(即从叶子到根的全部路径上不存在两个连续的红色结点)
- 从任一结点到每一个叶子的全部简单路径都包含相同数目的黑色结点
以上性质保证了红黑树在知足平衡二叉树特征的前提下,还能够作到 从根到叶子的最长路径最多不会超过最短路径的两倍 ,这主要是考虑两个极端的状况,由性质 4 和 5 咱们能够知道在一棵红黑树上从根到叶子的最短路径所有由黑色结点构成,而最长结点则由红黑结点交错构成(始终按照一红一黑的顺序组织),又由于最短路径和最长路径的黑色结点数目是一致的,因此最长路径上的结点数是最短路径的两倍。算法
对于一棵红黑树的操做最基本的无外乎增删改查,其中查和改都不会改变树的结构,因此与普通平衡二叉树操做无异。剩下的就是增删操做,插入和删除都会破坏树的结构,不过借助必定的平衡策略可以让树从新知足定义。平衡策略能够简单归纳为三种: 左旋转 、 右旋转 ,以及 变色 。在插入或删除结点以后,只要咱们沿着结点到根的路径上执行这三种操做,就能够最终让树从新知足定义。编程
对于当前结点而言,若是右子结点为红色,左子结点为黑色,则执行左旋转,以下图:数据结构
对于当前结点而言,若是左子、左孙子结点均为红色,则执行右旋转,以下图:并发
对于当前结点而言,若是左、右子结点均为红色,则执行变色,以下图:高并发
红黑树做为平衡二叉树的一种,一样须要借助于查找操做定位插入点,不过红黑树约定 新插入的结点一概为红色 ,这主要也是为了简化树的自平衡过程。对于一棵空树而言,插入结点为红色会增长一次变色操做,可是对于其他的状况,若是插入的结点是一个黑色结点,那么必然会破坏性质 5,而插入一个红色结点有可能会破坏性质 4,可是此时咱们能够经过简单的策略对树进行调整以从新知足定义。
咱们约定 X 为插入的结点,P 为 X 的父结点,G 为 X 的祖父结点,U 为 X 的叔叔结点。
下面听从上述策略分场景对插入过程进行探讨:
1.新插入结点 X 是根结点
此时新插入结点为红色,违背性质 2,只需将其变为黑色便可。
2.新插入结点 X 的父结点 P 是黑色
此时须要依据新插入结点 X 值相对于父结点 P 的大小分为两种状况,若是小于则将 X 简单插入到 P 的左子位置便可(下图左),若是 X 的值大于 P,则须要将 X 插入到 P 的右子结点位置,而后执行一次左旋转便可(下图右)。
3.父结点 P 为红色,同时存在叔叔结点 U 也为红色
由于 P 为红色,按照性质 4 则 G 一定为黑色,若是 X 的值小于 P,则须要在 P 的左子位置插入(以下图),插入后不知足性质 4,此时只须要执行一次变色操做,将 P、G、U 的颜色反转一下便可,由于 G 变为红色,因此路径长度减 1,可是由于 P 和 U 都变为了黑色,因此路径长度又加 1,最终长度不变,但此时 G 变为了红色,因此须要继续向上递归。
若是 X 的值大于 P,则须要在 P 的右子位置插入(以下图),插入后不知足性质 4,此时须要先执行左旋转变为上面这种状况,继续变色便可。
4.父结点 P 为红色,同时叔叔结点 U 为黑色或不存在
由于 P 为红色,按照性质 4 则 G 一定为黑色,若是 X 的值小于 P,则须要在 P 的左子位置插入(以下图),插入后不知足性质 4,此时须要先执行一次右旋转,旋转以后仍然违背性质 4,同时左子树的高度减 1,这个时候须要再执行一次变色操做便可知足定义。
若是 X 的值大于 P,则须要在 P 的右子位置插入(以下图),插入后不知足性质 4,此时咱们须要执行一次左旋转,而后就转换成了上面这种状况,继续右旋转、变色便可。
红黑树做为平衡二叉树的一种,一样须要借助于查找操做定位删除点,在执行删除以前咱们须要判断待删除结点有几个孩子结点,若是是 2 个的话咱们须要从结点的左子树中寻找值最大的结点,或者从右子树中寻找值最小的结点,并用结点值替换掉待删除结点(只要目标结点值从树上消失便可,不要纠结具体删除的是哪一个结点)。这两个结点有一个共性,即最多只有一个孩子结点(由于已是本身所处范围内的最大和最小了嘛,一山不容二虎(鼠)),此时就将需求转变成删除只有一个孩子结点的结点,相对要简单了许多。
咱们约定 X 为待删除的结点,P 为 X 的父结点,S 为 X 的孩子结点,B 为 X 的兄弟结点,BL 为 B 的左孩子结点,BR 为 B 的右孩子结点。
对于第三种状况咱们首先将 X 替换成 S,并重命名其为 N,N 沿用 X 对于长辈和晚辈的称呼,须要清楚这里实际删除的是 X 结点,而且删除以后经过 N 的路径长度减 1。
1.N 是新的根
这种状况比较简单,不须要再作任何调整。
2.N 的父结点、兄弟结点 B,以及 B 的孩子结点均为黑色
以下图,此时只须要将 B 变为红色便可,这样全部经过 B 的路径减 1,与全部经过 N 的路径正好一致,可是此时经过 P 的路径都减小了 1 个长度,因此须要向上递归对结点 P 继续断定。
3.N 的兄弟结点 B 为红色,其他结点均为黑色
以下图,此时须要执行一次左旋转,而后将 P 和 B 的颜色互换。调整先后各个结点的路径没有变化,可是由于以前通过 N 的路径长度少了一个单位,因此此时仍然不知足定义,须要按照后面的场景继续调整。
4.N 的父结点 P 为红色,兄弟结点 B,以及 B 的孩子结点均为黑色
以下图,此时咱们只须要简单互换 P 和 B 的颜色,这种状况下对于不经过 N 的结点路径没有影响,可是却让经过 N 的结点路径加 1,正好弥补以前删除操做所带来的损失。
5.N 的兄弟结点 B 为黑色,B 的左孩子为红色,B 的右孩子为黑色
以下图,此时咱们须要先执行一次右旋转操做,而后互换 B 与 BL 的颜色,操做以后经过全部结点的路径长度并无发生变化,却让 N 有了一个新的黑色兄弟结点,而且该兄弟结点的右孩子为红色,从而能够按照接下去介绍的一种场景继续调整。
注:白色结点表示该结点既能够是黑色也能够是红色,后续图示亦是如此。
6.N 的兄弟结点 B 为黑色,B 的右孩子为红色
以下图,此时咱们须要先执行一次左旋转,并互换 P 和 B 的颜色,同时将 B 的右孩子结点变为黑色。变动以后,除 N 外其他结点的路径长度未发生变化,可是通过 N 的路径上却增长了一个黑色结点,这恰好弥补以前删除操做所带来的损失。
红黑树的主要难点在于插入和删除过程当中的自平衡调整,其中插入过程的调整相对简单,删除的过程须要处理的状况要多一些,但不论是插入仍是删除,都建议读者将全部的图放置在一块儿进行观察,可以发现其中承前启后的奥妙,本文鉴于篇幅就再也不贴出长图。
另外也建议读者按照上述过程本身在白纸上手动去构造一棵红黑树,并逐一将结点删除,以此来加深理解,也能够旧金山大学提供的交互网站辅助学习(点此前往),相关实现位于 algorithm-design 项目的 org.zhenchao.classic.search
包下面,地址:
https://github.com/plotor/algorithm-design
《算法》红宝书的做者之一 “罗伯特·塞奇威克” 是红黑树的提出者,红黑树是在 2-3 树的基础上改进而成,相对于红黑树而言 2-3 树的自平衡策略要容易理解不少,在此也推荐你们在学习时参阅相关章节。