本文主要包括如下内容:node
今天咱们来介绍下很是重要的数据结构:红黑树。算法
不少文章或书籍在介绍红黑树的时候直接上来就是红黑树的5个基本性质、插入、删除操做等。本文不是采用这样的介绍方式,在介绍红黑树以前,咱们要了解红黑树是怎么发展出来的,进而就能知道为何会有红黑树的5条基本性质。bash
这样的介绍方式也是《算法4》的介绍方式。这也不奇怪,《算法4》的做者 Robert Sedgewick 就是红黑树的做者之一。在介绍红黑树以前,咱们先来看下2-3树数据结构
在介绍红黑树以前为何要先介绍 2-3树 呢?由于红黑树是 完美平衡的2-3树 的一种实现。因此,理解2-3树对掌握红黑树是相当重要的。源码分析
2-3树 的一个Node可能有多个子节点(可能大于2个),并且一个Node能够包含2个键(元素)性能
能够把 红黑树(红黑二叉查找树) 看成 2-3树 的一种二叉结构的实现。测试
在前面介绍的二叉树中,一个Node保存一个值,在2-3树中把这样的节点称之为 2- 节点ui
若是一个节点包含了两个值(能够看成两个节点的融合),在2-3树中把这样的节点称之为 3- 节点。 完美平衡的2-3树全部空连接到根节点的距离都应该是相同的spa
下面看下《算法4》对 2-3-节点的定义:3d
以下面一棵 完美平衡的2-3树 :
2-3树 是一棵多叉搜索树,因此数据的插入相似二分搜索树
红黑树是对 完美平衡的2-3树 的一种实现,因此咱们主要介绍完美平衡的2-3树的插入过程
完美平衡的2-3树插入分为如下几种状况(为了方便画图默认把空连接去掉):
由于2-3树中节点只能是2-节点或者3-节点
往3-点中再插入一个键就成了4-节点,须要对其进行分解,以下所示:
往3-点中再插入一个键就成了4-节点,须要对其进行分解,对中间的键向上融合
因为父结点是一个 2- 结点 ,融合后变成了 3- 结点,而后把 4- 结点的左键变成该 3- 节点的中间子结点
在这种状况下,向3- 结点插入新键造成暂时的4- 结点,向上分解,父节点又造成一个4- 结点,而后继续上分解
上面介绍完了2-3树,下面来看下红黑树是怎么来实现一棵完美平衡的2-3树的
红黑树的背后的基本思想就是用标准的二分搜索树和一些额外的信息来表示2-3树的
这额外的信息指的是什么呢?由于2-3树不是二叉树(最多有3叉),因此须要把 3- 结点 替换成 2- 结点
额外的信息就是指替换3-结点的方式
将2-3树的连接定义为两种类型:黑连接、红连接
黑连接 是2-3树中普通的连接,能够把2-3树中的 2- 结点 与它的子结点之间的链看成黑连接
红连接 2-3树中 3- 结点分解成两个 2- 结点,这两个 2- 结点之间的连接就是红连接
那么如何将2-3树和红黑树等价起来,咱们规定:红连接均为左连接
根据上面对完美平衡的2-3树和红连接的介绍能够得出结论:没有一个结点同时和两个红连接相连
根据上面对完美平衡的2-3树和黑连接的介绍能够得出结论:完美平衡的2-3树是保持完美黑色平衡的,任意空连接到根结点的路径上的黑连接数量相同
据此,咱们能够得出3条性质:
在红黑树中,没有一个对象来表示红连接和黑连接,经过在结点上加上一个属性(color)来标识红连接仍是黑连接,color值为red表示结点是红结点,color值为black表示结点是黑结点。
黑结点 2-3树中普通的 2-结点 的颜色 红结点 2-3树中 3- 结点 分解出两个 2-结点 的最小 2-结点
下面是2-3树和红黑树的一一对应关系图:
介绍完了2-3树和红黑树的对应关系后,咱们再来看下红黑树的5个基本性质:
2-3树和红黑树的对应关系后咱们也就知道了红黑树的5个基本性质是怎么来的了
红黑树的第一条性质:每一个节点要么是红色,要么是黑色
由于咱们用结点上的属性来表示红链仍是黑链,因此红黑树的结点要么是红色,要么是黑色是很天然的事情
红黑树的第二条性质:根结点是黑色
红色节点的状况是 3- 结点分解出两个 2- 结点的最小节点是红色,根节点没有父节点因此只能是黑色
红黑树的第三条性质:每一个叶子结点(最后的空节点)是黑色
叶子节点也就是2-3树中的空链,若是空链是红色说明下面仍是有子结点的,可是空链是没有子结点的;另外一方面若是 空链是红色,空链指向的父结点结点若是也是红色就会出现两个连续的红色连接,就和上面介绍的 “没有一个结点同时和两个红连接相连” 相违背
红黑树的第四条性质:若是一个结点是红色的,那么他的孩子结点都是黑色的
上面介绍的‘没有一个结点同时和两个红连接相连’,因此一个结点是红色,那么他的孩子结点都是黑色
红黑树的第五条性质:从任意一个结点到叶子结点,通过的黑色结点是同样的
在介绍完美平衡的2-3树和黑连接咱们得出的结论:‘完美平衡的2-3树是保持完美黑色平衡的,任意空连接到根结点的路径上的黑连接数量相同’, 因此从任意一个结点到叶子结点,通过的黑色结点数是同样的
为何要颜色翻转(flipColor)?在插入的过程当中可能出现以下状况:两个左右子结点都是红色
根据咱们上面的描述,红链只容许是左链(也就是左子结点是红色)
因此须要进行颜色转换:把该结点的左右子结点设置为黑色,本身设置为黑色
private void flipColor(Node<K, V> node) {
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
复制代码
左旋状况大体有两种:
结点是右子结点且是红色
颜色翻转后,结点变成红色且它是父结点的右子节点
private Node<K, V> rotateLeft(Node<K, V> node) {
Node<K, V> x = node.right;
node.right = x.left;
x.left = node;
x.color = node.color;
node.color = RED;
return x;
}
复制代码
须要右旋的状况:连续出现两个左红色连接
private Node<K, V> rotateRight(Node<K, V> node) {
Node<K, V> x = node.left;
node.left = x.right;
x.right = node;
x.color = node.color;
node.color = RED;
return x;
}
复制代码
经过咱们上面对红黑树和2-3树的介绍,红黑树实现2-3树插入操做就很简单了
只要知足不出现 两个连续左红色连接、右红色连接、左右都是红色连接 的状况就能够了
因此仅仅须要处理三种状况便可:
private Node<K, V> _add(Node<K, V> 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
node.value = value;
//1,若是出现右侧红色连接,左旋
if (isRed(node.right) && !isRed(node.left)) {
node = rotateLeft(node);
}
//2,若是出现两个连续的左红色连接,右旋
if (isRed(node.left) && isRed(node.left.left)) {
node = rotateRight(node);
}
//3,若是结点的左右子连接都是红色,颜色翻转
if (isRed(node.left) && isRed(node.right)) {
flipColor(node);
}
}
public void add(K key, V value) {
root = _add(root, key, value);
root.color = BLACK;
}
复制代码
这样下来红黑树依然保持着它的五个基本性质,下面咱们来对比下JDK中的TreeMap的插入操做
先按照上面的红黑树插入逻辑插入三个元素 [14, 5, 20],流程以下:
使用Java TreeMap来插入上面三个元素,流程以下:
经过对比咱们发现二者的插入后的结果不同,并且Java TreeMap是容许左右子结点都是红色结点!
这就和咱们一直在说的用完美平衡的2-3树做为红黑树实现的基础结构相违背了,咱们一直在强调不容许右节点是红色,也不容许两个连续的红色左节点,不容许左右结点同时是红色
这也是《算法4》在讲到红黑树时遵循的。可是JDK TreeMap(红黑树)是容许右结点是红色,也容许左右结点同时是红色,Java TreeMap的红黑树实现从它的代码注释(From CLR)说明它的实现来自《算法导论》
说明《算法4》和《算法导论》中的所介绍的红黑树产生了一些“出入”,给咱们理解红黑树增长了一些困惑和难度
《算法4》在介绍红黑树以前先给咱们详细介绍了2-3树,而后接着讲到完美平衡的2-3树和红黑树的对应关系(红黑树就等于完美平衡的2-3树),让咱们知道红黑树是怎么来的,根据这些介绍你本身是能够解释红黑树的的5个基本性质为何是这样的。
而在《算法导论》中介绍红黑树的时候没有说起2-3树,直接就是红黑树的5个基本性质,以及红黑树的插入、删除操做,感受对初学者是不太合适的,由于你不知道为何是这样的,只是知道有这个五个性质,也许这就是为何它叫导论的缘由吧
并且在《算法4》中做者最后好像也没有明确的给出红黑树的五个基本性质,在《算法导论》中在红黑树章节一开始就贴出了5条性质,感受像是一种递进和升华
这两本书除了对红黑树讲解的方式存在差别外,咱们还发现《算法4》和《算法导论》在红黑树的实现上也是有差别的,就如咱们上面插入三个元素 [14, 5, 20] 产生不一样的结果
在解释这些差别以前,咱们再来看些2-3-4树,上面提到完美平衡的2-3树和红黑树等价,更准确的说是2-3-4树和红黑树等价
2-3-4树 和 2-3树 很是相像。2-3树容许存在 2- 结点 和 3- 结点,相似的2-3-4树容许存在 2- 结点、3- 结点 和 4- 结点
向2-结点插入元素,这个和上面介绍的2-3树是同样的,在这里就不叙述了
向3-结点插入元素,造成一个4-结点,由于2-3-4树容许4-结点的存在,因此不须要向上分解
向4-结点插入元素,须要分解4-结点, 由于2-3-4树最多只容许存在4-结点,如:
若是待插入的4-结点,它的父结点也是一个4-结点呢?以下图的2-3-4树插入结点K:
主要有两个方案:
Bayer全名叫作Rudolf Bayer(鲁道夫·拜尔),他在1972年发明的 对称二叉B树(symmetric binary B-tree) 就是 红黑树(red black tree) 的前身。 红黑树 这个名字是由 Leo J. Guibas 和 Robert Sedgewick 于1978年的一篇论文中提出来的, 对该论文感兴趣的能够查看这个连接:professor.ufabc.edu.br/~jesus.mena…
下面的图就是 自上而下 方案的流程图
在介绍2-3树的时候咱们也讲解了2-3树和红黑树的等价关系,因为2-3树和2-3-4树很是相似,因此2-3-4树和红黑树的等价关系也是相似的。不一样的是2-3-4的 4-结点 分解后的结点颜色变成以下形式:
因此能够得出下面一棵2-3-4树和红黑树的等价关系图:
上面在介绍红黑树实现2-3树的时候讲解了它的插入操做:
private Node<K, V> _add(Node<K, V> 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
node.value = value;
//1,若是出现右侧红色连接,左旋
if (isRed(node.right) && !isRed(node.left)) {
node = rotateLeft(node);
}
//2,若是出现两个连续的左红色连接,右旋
if (isRed(node.left) && isRed(node.left.left)) {
node = rotateRight(node);
}
//3,若是结点的左右子连接都是红色,颜色翻转
if (isRed(node.left) && isRed(node.right)) {
flipColor(node);
}
}
复制代码
咱们能够很轻松的把它改为2-3-4的插入逻辑(只须要把颜色翻转的逻辑提到二分搜索的前面便可):
private Node<K, V> _add(Node<K, V> node, K key, V value) {
//向叶子结点插入新结点
if (node == null) {
size++;
return new Node<>(key, value);
}
//split 4-nodes on the way down
if (isRed(node.left) && isRed(node.right)) {
flipColor(node);
}
//二分搜索的过程
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
node.value = value;
//fix right-leaning reds on the way up
if (isRed(node.right) && !isRed(node.left)) {
node = rotateLeft(node);
}
//fix two reds in a row on the way up
if (isRed(node.left) && isRed(node.left.left)) {
node = rotateRight(node);
}
}
复制代码
//使用2-3-4树插入数据 [E,C,G,B,D,F,J,A]
RB2_3_4Tree<Character, Character> rbTree = new RB2_3_4Tree<>();
rbTree.add('E', 'E');
rbTree.add('C', 'C');
rbTree.add('G', 'G');
rbTree.add('B', 'B');
rbTree.add('D', 'D');
rbTree.add('F', 'F');
rbTree.add('J', 'J');
rbTree.add('A', 'A');
rbTree.levelorder(rbTree.root);
//使用2-3树插入数据 [E,C,G,B,D,F,J,A]
RBTree<Character, Character> rbTree = new RBTree<>();
rbTree.add('E', 'E');
rbTree.add('C', 'C');
rbTree.add('G', 'G');
rbTree.add('B', 'B');
rbTree.add('D', 'D');
rbTree.add('F', 'F');
rbTree.add('J', 'J');
rbTree.add('A', 'A');
rbTree.levelorder(rbTree.root);
复制代码
下面是 2-3-4树 和 2-3树 插入结果的对比图:
因此咱们一开始用红黑树实现完美平衡的2-3树,左右结点是不会都是红色的 如今用红黑树实现2-3-4树,左右结点的能够同时是红色的,这样的红黑树效率更高。由于若是遇到左右结点是红色,就进行颜色翻转,还须要对红色的父结点进行向上回溯,由于父结点染成红色了,可能父结点的父结点也是红色,可能须要进行结点旋转或者颜色翻转操做,因此说2-3-4树式的红黑树效率更高。
因此回到上面咱们提到《算法4》和《算法导论》在实现上的差别的问题,就很好回答了,由于《算法4》是用红黑树实现2-3树的,并非2-3-4树。可是若是是用红黑树实现2-3-4树就和《算法导论》上介绍的红黑树同样吗?不同。
下面继续作一个测试,分别往上面红黑树实现的 2-3-4树 和 JDK TreeMap 中插入**[E, D, R, O, S, X]**
虽然两棵树都是红黑树,可是却不同。而且TreeMap容许右节点是红色,在2-3-4树中最可能是左右子结点同时是红色的状况,不会出现左结点是黑色,右边的兄弟结点是红色的状况,为何会有这样的差别呢?
从上面的2-3-4树的插入逻辑能够看出,若是右节点是红色会执行左旋转操做,因此不会出现单独红右结点的状况 也就是说只会出现单独的左结点是红色的状况,咱们把这种形式的红黑树称之为左倾红黑树(Left Leaning Red Black Tree),包括上面的红黑树实现的完美平衡的2-3树也是左倾红黑树
为何在《算法4》中,做者规定全部的红色连接都是左连接,这只是人为的规定,固然也能够是右连接,规定红连接都是左链,可使用更少的代码来实现黑色平衡,须要考虑的状况会更少,就如上面咱们介绍的插入操做,咱们只须要考虑3中状况便可。
可是通常意义上的红黑树是不须要维持红色左倾的这个性质的,因此为何TreeMap是容许单独右红结点的
若是还须要维护左倾状况,这样的话就更多的操做,可能还须要结点旋转和颜色的翻转,性能更差一些,虽然也是符合红黑树的性质
介绍完了《算法4》上的红黑树,下面就来分析下通常意义上的红黑树的 插入 和 删除 操做,也就是《算法导论》上介绍的红黑树。
插入操做有两种状况是很是简单的,因此在这里单独说一下:
case 1. 若是插入的结点是根结点,直接把该结点设置为黑色,整个插入操做结束
以下图所示:
case 2. 若是插入的结点的父结点是黑色,也无需调整,整个插入操做结束
以下图所示:
下面开始介绍比较复杂的状况
红黑树插入操做,咱们只须要处理父结点是红色的状况,由于一开始红黑树确定是黑色平衡的,就是由于往叶子节点插入元素后可能出现两个连续的红色的结点
须要注意的是,咱们把新插入的结点默认设置为红色,初始的时候,正在处理的节点就是插入的结点,在不断调整的过程当中,正在处理的节点会不断的变化,且叔叔、爷爷、父结点都是相对于当前正在处理的结点来讲的
case 3. 叔叔结点为红色,正在处理的节点能够是左也能够是右结点
调整策略:因为父结点是红色,叔叔结点是红色,爷爷结点是黑色,执行颜色翻转操做
而后把当前正在处理的结点设置为爷爷结点,若是爷爷的父结点是黑色插入操做结束,若是是红色继续处理
复制代码
case 4. 叔叔结点为黑色,正在处理的结点是右结点
调整策略:因为父结点是红色,叔叔结点为黑色,那么爷爷结点确定是黑色
把正在处理的节点设置为父结点,而后左旋,造成Case5状况
复制代码
case 5. 叔叔结点为黑色,正在处理的结点是左孩子
调整策略:因为父结点是红色,叔叔结点为黑色,那么爷爷结点确定是黑色
把父结点染黑,爷爷结点染红,而后爷爷结点右旋
复制代码
Case三、Case四、Case5若是单独来理解的话比较困难,就算单独为每个Case画图,我以为也很难完整的理解,不少博客上都是这种方式,感受不太好理解。我将这三种状况经过一张流程图串联起来,将这三个Case造成一个总体,蓝色箭头表示正在处理的结点,以下所示:
上面介绍完了红黑树的插入操做,接下来看下红黑树的删除操做
红黑树的删除操做比插入操做更加复杂一些
为了描述方便,咱们把正在处理的结点称之为 X,父结点为 P(Parent),兄弟节点称之为 S(Sibling),左侄子称之为 LN(Left Nephew),右侄子称之为 RN(Right Nephew)
若是删除的结点是黑色,那么就致使原本保持黑平衡的红黑树失衡了,从下图能够看出结点P到左子树的叶子结点通过的黑节点数量为4(2+2),到右子树的叶子节点通过的黑色节点数量是5(2+3),以下图所示:
红黑树的删除操做,若是删除的是黑色会致使红黑树就不能保持黑色平衡了,须要进行调整了; 若是删除的是红色,那么就无需调整,直接删除便可,由于没有没有破坏黑色平衡
删除结点后,无需调整的状况
case 1 删除的结点是红色结点,直接删除便可
case 2 删除的节点是黑色,若是当前处理的节点X是根结点
不管根结点是什么颜色,都将根结点设置为黑色
复制代码
case 3 删除的结点是黑色,若是当前处理的结点是红色结点,将该结点设置为黑色
由于删除黑色结点后,就打破了黑色平衡,黑高少了1
因此把一个红色节点设置为黑色,这样黑高又平衡了
复制代码
删除节点后,须要调整的状况
正在处理的结点为X,要删除的结点是左结点,分为4中状况:
case 4 兄弟结点为红色
调整方案:兄弟设置为黑色,父结点设置为红色,父结点进行左旋转
转化为 case五、case六、case7
复制代码
case 5 兄弟结点为黑色,左侄子LN为黑色,右侄子RN为黑色
在这种条件下,还有两种状况:父结点是红色或黑色,不论是那种状况,调整方案都是一致的
调整方案:将兄弟结点设置为红色,把当前处理的结点设置为父结P
复制代码
case 6 兄弟结点为黑色,左侄子为红色,右侄子RN为黑色
调整方案:将左侄子结点设置为黑色,兄弟结点设置为红色,兄弟结点右旋转,这样就转化成了case7
复制代码
case 7 兄弟结点为黑色,左侄子无论红黑,右侄子为红色
处理方式:兄弟结点变成父结点的颜色,而后父结点设置黑色,右侄子设置黑色,父结点进行左旋转
复制代码
和插入操做同样,下面经过一张流程图把删除须要调整的状况串联起来:
上面处理的全部状况都是基于正在处理的结点是左结点 若是要调整正在处理的结点是右节点的状况,就是上面的处理的镜像。插入操做也是同理,因此就省略了
TreeMap底层就是用红黑树实现的,它在插入后调整操做主要在fixAfterInsertion方法里,我为每种状况都添加注释,以下所示:
/** From CLR */
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)));
//-----Case3状况-----
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
//-----Case4状况-----
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
//-----Case5状况-----
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
//省略镜像状况
}
}
root.color = BLACK;
}
复制代码
它的删除后调整操做主要在fixAfterDeletion方法:
/** From CLR */
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
Entry<K,V> sib = rightOf(parentOf(x));
//-----Case4的状况-----
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
//-----Case5的状况-----
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
//-----Case6的状况-----
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
//-----Case7的状况-----
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // symmetric
//省略镜像的状况
}
}
setColor(x, BLACK);
}
复制代码
TreeSet 底层就是用 TreeMap 来实现的,往TreeSet添加进的元素看成TreeMap的key,TreeMap的value是一个常量Object。掌握了红黑树,对于这两个集合的原理就不难理解了。
本文从一开始讲的2-3树和红黑树的对应关系,再到2-3-4树和红黑树的对应关系,再到《算法4》和《算法导论》JDK TreeMap在红黑树上的差别 而后详细介绍了红黑树的插入、删除操做,最后分析了下Java中的TreeMap和TreeSet集合类。
人生当如红黑树,当过于自喜或过于自卑的时候,应当自我调整,寻求平衡。
我很丑,红黑树却很美。 但愿本文对你有 些许帮助。
下面是个人公众号,干货文章不错过,有须要的能够关注下,很是感谢:
参考资料: