前言html
众所周知,红黑树是很是经典,也很很是重要的数据结构,自从1972年被发明以来,由于其稳定高效的特性,40多年的时间里,红黑树一直应用在许多系统组件和基础类库中,默默无闻的为咱们提供服务,身边有不少同窗常常问红黑树是怎么实现的,因此在这里想写一篇文章简单和你们聊聊下红黑树java
小编看过不少讲红黑树的文章,都不是很容易懂,主要也是由于完整的红黑树很复杂,想经过一篇文章来讲清楚实在很难,因此在这篇文章中我想尽可能用通俗口语化的语言,再结合 Robert Sedgewick 在《算法》中的改进的版本(2-3树版本,容易理解也方便实现),能够保证让你们对红黑树的原理有大概的理解面试
其实对于大部分同窗来讲,大概了解红黑树的工做原理就基本够用了,由于一般不会有面试官让你去手写红黑树,你也几乎不须要去本身实现一个红黑树,看完这里,若是感受意犹未尽,还有兴趣的同窗能够去看看《算法导论》的红黑树实现,那是完整的4阶B树(2-3-4树)版本的实现算法
关于红黑树的主题,咱们的文章有如下的灵魂三问:swift
为何会有红黑树?数组
红黑树的应用场景和定义?数据结构
红黑树的高效和稳定是怎么实现?app
为何会有红黑树工具
要了解红黑树,先它的前辈:二叉树,平衡二叉树(咱们的读者应该都具有这些前置知识,因此咱们只作大概的讲解)性能
前置知识:
二叉树:传统的数组和链表等线性结构表效率低下,线性表在处理大规模数据的时间复杂度都是线性级别 O(n),因此这种低效的数据结构,几乎不可能用来处理千万级别或者以上的数据量,因而基于二分思想的二叉树就诞生了,在最好状况下,二叉树查找的时间复杂度能够达到恐怖的对数级别 O(logN),什么概念呢?就是在十亿级别的数据量里面,二叉树只须要15~30次的访问就能够找到目标,固然咱们的前提是最好状况,那么最坏状况呢?能够参考下图
二叉树的最好/最坏状况:
上图能够看到,二叉树的性能的好坏,依赖数据的插入顺序,最坏状况下二叉树会退化为链表,全部操做的时间复杂度回到的线性级别 O(n),那么怎么解决这个问题呢?
想要让树的查找效率最大化,那么就要保持树的平衡,因此平衡二叉树出现了,平衡二叉树的思想是在操做的时候对树进行平衡调整,来防止二叉树退化为链表,从而保证二叉树的最优查找性能,完美的平衡二叉树对高度的定义是相差不会大于1,这就至关于每次都插入/删除操做,都会对树进行平衡操做,这是代价很是高的操做,你能够理解为,相似数组为了保证有序性,数组中间插入数据,全部元素都要向后移动的代价,虽然名字叫 平衡二叉树,其实它的性能很是不平衡,由于它是最大化 插入/删除 操做的时间来换取 查找 操做的时间最小化
看到这里,就有好奇的同窗问,那么有没有既能够保证树的完美平衡,又能够保证全部操做性能的数据结构呢?能够很负责任的告诉你,有的,就是红黑树,咱们先看看红黑树能为咱们带来什么?
红黑树能够保证 全部操做时间复杂度都是对数级别 O(logN)
和二叉树不一样,不管插入顺序如何,红黑树都是接近完美平衡的
无数实验的应用证实,红黑树的操做成本比二叉树下降40%左右
常见树形结构的操做复杂度对比,能够看到红黑树是最均衡的:
红黑树的应用场景和定义
简单罗列下咱们经常使用的哪些工具是经过红黑树实现的
Java 的 HashMap (8 之后)的链表树化是经过 红黑树实现
Java 的 TreeMap 是经过红黑树实现
Nginx 用红黑树管理 timer 等
Linux 进程调度用红黑树管理进程控制块
等等……
红黑树的定义,标准的红黑树示意图:
红黑树自己是二叉树,其背后的思想是使用二叉树的结构再加载额外的颜色信息,来表示2-3树,因此红黑树是包含了二叉树的高效查找和2-3树的高效插入平衡优势的算法
在咱们讨论的版本中对红黑树的定义以下:
红连接必须为左连接
不能出现两条相连的红连接
该树是完美黑色平衡的
只看这些定义你可能会以为描述很是的学院派,很差理解,咱们先看看标准的红黑树,后面再用画图的方式来逐渐讲解
红黑树插入维护规则的核心代码
private Node put(Node h, Key key, Value val) { // 二分插入 if(h == null) return new Node(key, val, RED, 1); int cmp = key.compareTo(h.key); if(cmp < 0) h.left = put(h.left, key, val); else if(cmp > 0) h.right = put(h.right, key, val); else h.val = val; // 修复 右倾链接 if(isRed(h.right) && !isRed(h.left)) h = rotateLeft(h); // 违反规则 不容许出现右红链接 if(isRed(h.left) && isRed(h.left.left)) h = rotateRight(h); // 违反规则 不容许出现连续的左红链接 if(isRed(h.left) && isRed(h.right)) flipColors(h); // 当左右子节点为红色, 则变色 h.size = size(h.left) + size(h.right) + 1; return h; }
红黑树的高效和稳定是怎么实现?
在插入数据的过程当中红黑树会出现不少违反上面定义的状况,若是出现违反红黑树定义的状况,那么就依靠红黑树的三个核心操做来保证树的平衡,这三个操做也对应了红黑树定义的三条规则,分别以下:
左旋转(当出现右红子节点时,进行左旋转)
右旋转(当出现两条相连的左子红连接时,进行右旋转)
变色(当左右节点都是红连接时,进行变色)
左旋转
将红色的右节点,调整到树的左边,假如我要在树的底部插入元素S,可是元素被分配到的元素E的右边,具体以下:
左旋转是针对明显的红右连接,红色的右连接违反了红黑树定义的第一条规则,因此咱们须要将它进行左旋转操做,被操做了左旋转后,元素E的位置会被元素S取代,E元素成为了S的左子节点,符合了二叉树的定义,左旋转的具体代码:
private Node rotateLeft(Node h) { Node x = h.right; h.right = x.left; x.left = h; x.color = x.left.color; x.left.color = RED; x.size = h.size; h.size = size(h.left) + size(h.right) + 1; return x; }
右旋转
当左边出现连续的左红连接时,把左连接放到右边
右旋转的代码(右旋转的代码和左旋转几乎相同把 x.left 换成 x.right 便可)
private Node rotateRight(Node h) { Node x = h.left; h.left = x.right; x.right = h; x.color = x.right.color; x.right.color = RED; x.size = h.size; h.size = size(h.left) + size(h.right) + 1; return x; }
变色
当左右子节点都是红色的时候,把颜色进行转换,具体如图:
颜色转换的代码也很是简单:
private void flipColors(Node h) { h.color = !h.color; h.left.color = !h.left.color; h.right.color = !h.right.color; }
理解了以上三种操做的原理,基本也就理解了红黑树的原理,有了这三种操做的基本知识,最后咱们开始结合案例来分析红黑树插入平衡的全过程
为了便于理解,咱们看一个简单的例子,下面罗列的三种状况:
插入最大键
插入最小键
插入中间键
咱们能够发现,不管插入的数据如何不一样,经过旋转,变色操做后最终获得的结果都是相同的,树永远保持平衡,具体能够看下方的示意图:
有了上面的理解,咱们能够分析一组有序数据插入的过程,再结合文字逐步分析红黑树是怎么把它构造为一颗接近完美平衡的树
解析:
A首先成为根节点
C首先插入在A的右边,A违反了不能出现红右子节点的规则,进行左旋转,A成了C的左红子节点
E首先插入在C的右边,C违反左右子节点均为红色的规则,进行变色,C,A,E变黑(根节点永远为黑)
H首先插入在E的右边,E违反了不能出现红右子节点的规则,进行左旋转,E成了H的左红子节点
L首先插入在H的右边,H违反左右子节点均为红色的规则,进行变色,E,L变黑,H变红,致使C违反了不能出现红右子节点的规则,进行左旋转,C成为H的左红子节点(这里违反2个规则)
M首先插入在L的右边,L违反了不能出现红右子节点的规则,进行左旋转,L成为M的左红子节点
P首先插入在M的右边,M违反左右子节点均为红色的规则,进行变色,L,P变黑,M变红,致使H违反左右子节点均为红色的规则,进行变色,H,C,M变黑(这里违反2个规则)
R首先插入到P的右边,P违反了不能出现红右子节点的规则,进行左旋转,P成为R的左红子节点
S首先插入到R的右边,R违反左右子节点均为红色的规则,进行变色,S,P变黑,R变红,致使M违反了不能出现红右子节点的规则,进行左旋转,M成为R的左红子节点(这里违反2个规则)
X首先插入到S的右边,S违反了不能出现红右子节点的规则,进行左旋转,S成为X的左红子节点
经过以上证实,就能够得出结论,和二叉树不一样,不管数据的插入顺序如何,红黑树均可以保证完美平衡
理解红黑树的背后思想,就能明白只要谨慎的使用简单的,左旋,右旋,变色这三个操做,就能够保证红黑树的两种重要的特性 有序性和完美平衡性,由于旋转和变色都是局部操做,因此无需为整棵树的平衡性担忧,另外红黑树的查找彻底和二叉树相同,不须要额外的平衡,这里并不打算讲红黑树的删除操做,由于红黑树的删除实现复杂,比插入平衡还要复杂的多,要在文章里讲清楚很困难,推荐你们去看看我开篇推荐的经典书籍
总结
到这里对于为何要使用红黑树的结论已经很是简单了,红黑树最吸引人的是它的全部操做在 最好 最坏 状况下均可以保证对数级别的时间复杂度 O(logN),是什么概念呢,能够简单说明对比下:
例如要在十亿级别的数据量找到一条数据,十亿的对数是30,线性表要找到数据须要访问十亿次,而使用红黑树的书只须要访问30次元素就能找到,10亿次/30次,差很少是3千万倍的性能提高,在现代上千亿数据的信息海洋里,只要经过几十次的比较就能随意的插入和查找数据,这是多么了不得的成就呀
并且对于二叉树,无数的实验和应用都能证实,红黑树的操做成本比二叉树要低 40% 左右(包含旋转和变色),红黑树自从被发现这40年来,一直高效稳定的经过各类应用的考验,包含须要系统基础组件和类库都是用红黑树,因此很是值得咱们去学习和掌握它,最后留给你们一个问题,红黑树和散列表有什么区别,散列表查找的时间复杂度是常数级别 O(1),那为何不少场景咱们不用散列表而用红黑树呢?欢迎留言拍砖
参考资料
https://algs4.cs.princeton.edu/33balanced/
https://algs4.cs.princeton.edu/33balanced/RedBlackBST.java.html
https://zh.wikipedia.org/wiki/%E7%BA%A2%E9%BB%91%E6%A0%91
https://book.douban.com/subject/10432347/