学过了二叉查找树还有平衡二叉树以后,再看点更复杂的树形结构——红黑树,跟平衡二叉树一样,红黑树也是为了解决二叉搜索树不能自平衡的问题。红黑树是2-3树的变形,以2-3树的角度去理解红黑树会容易的多。
先回忆一下AVL树:
AVL树就是要保证插入结点后,任一结点对应的两棵子树的最大高度差为1,这样就可以保证整棵树的深度最小。
AVL虽然高度平衡,但是我们发现它每次插入或者删除结点的时候,树的平衡都可能被打破,而且如果要一直保证也就是动态的来使得这个 AVL 树达到平衡是需要很多操作的,太多步的操作反而会影响这个树形结构的的性能。除非是在树结构变化特别少的情况,不然我们让AVL 树平衡的时候,搜索性能的提升可能还不够抵消平衡树所带来的性能消耗。所以就相继出现了2-3树和红黑树。
1、2-3树
1、什么是2-3树
2-3树的意思就是说,一个父节点可以有两个子结点,也可以有三个子结点,并且其也满足类似二叉搜索树的定义(父结点的值大于左子树,但小于右子树),所有叶子节点都在同一层。
2-3树的某个节点会有两种可能,一是正常的2节点,二是3节点:
- 2节点:父亲节点存储一个值,最多有左右两个子树。假设父节点为p,子节点为l(左节点)、r(有节点),且满足:
如下图:
- 3节点:父亲节点存储两个值,最多有左中右三个子树。假设父节点分别为p1,p2,子节点分别为l(左节点)、m(中间节点)、r(右节点),且满足:
如下图:
一颗完整的2-3树就如下图所示:
2、2-3树的创建
假设有一组数据集合数组为{ 30, 13, 7, 43, 23, 12, 9, 33, 42, 21, 18, 6, 3, 50 }。
创建2-3树的过程:
1、将30作为根节点
2、插入13,13比30小,融合成一个值为(13 30)的3节点
3、插入7,7比13小,融合成一个值为(7 13 30)的4节点,然后分解
4、插入43,43大于13,43大于30,与30一起融合成3节点
5、插入23,23大于13,23小于30,与(30 43)融合成4节点,然后分解
6、插入12,12小于13,12大于7,与7融合成3节点
7、插入9,9小于13,9大于7,9小于12,与(7 12)融合成4节点,然后分解。分解后9升级到上一层,与(13 30)融合成4节点,再次分解
8、插入33,33大于13,33大于30,33小于43,与43融合成3节点
9、插入42,42大于13,42大于30,42大于33,42小于43,与(33 43)融合成4节点,然后分解
10、省略后面过程,最终生成结果为:
至此,我们创建了一颗2-3树,可以看出2-3树的平衡性还是很好的。
2、红黑树
尽管有平衡二叉树和2-3树了,但是现在用的多的还是红黑树,主要是因为红黑树有这4点的优势:
1、AVL的左右子树高度差不能超过1,每次进行插入/删除操作时,几乎都需要通过旋转操作保持平衡。
2、在频繁进行插入/删除的场景中,频繁的旋转操作使得AVL的性能大打折扣。
3、红黑树通过牺牲严格的平衡,换取插入/删除时少量的旋转操作,整体性能优于AVL。(红黑树插入时的不平衡,不超过两次旋转就可以解决;删除时的不平衡,不超过三次旋转就能解决。)
4、红黑树的红黑规则,保证最坏的情况下,也能在O(log2N)时间内完成查找操作。
1、将2-3树转换成红黑树
创建了一棵2-3树以后,我们就可以进行一些结构上的变化
将所有的3节点进行变换,并且满足三个条件:
- 红链接均为左链接。
- 没有任何一个节点同时和两条红链接相连。
- 任意空链接到根节点路径上的黑色连接数目相同。
这三个条件均满足以后我们创建的就是一棵不那么标准的红黑树,如下图:
2、红黑树的性质
红黑树本身是一棵二叉查找树,在其基础上附加了两个要求:
- 树中的每个结点增加了一个用于存储颜色的标志域;
- 树中没有一条路径比其他任何路径长出两倍,整棵树要接近于“平衡”的状态。
路径:指的是从任何一个结点开始,一直到其子孙的叶子结点的长度;
接近于平衡:红黑树并不是平衡二叉树,只是由于对各路径的长度之差有限制,所以近似于平衡的状态。
下面图示的就是一棵典型的红黑树:
观察上图,可以发现红黑树的一些规律:
1、这些节点不是红色的就是黑色的,但是根节点是黑色的。
2、再看叶子节点,叶子节点跟普通的树不太一样,红黑树的叶子节点都是null节点(空节点)并且都为黑色的。
3、假设从根节点出发,然后从最左边的路径走到叶子节点,都没有连续的红色节点。所以可以得出一个结论,同一路径,不存在连续的红色节点。
以上观察出来的这三条规律就是红黑规则的一部分。
因此我们就可以得出红黑树的红黑规则了:
- 性质1:每个结点要么是黑色,要么是红色。
- 性质2:根节点是黑色。
- 性质3:每个叶子结点(NIL)是黑色。
- 性质4:每个红色结点的两个子结点一定都是黑色。
- 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
- 从性质5又可以推出:
性质5.1:如果一个结点存在黑子结点,那么该结点肯定有两个子结点
3、红黑树的术语约定

正在遍历的结点称做当前结点,如图2中的D,它的父亲叫做父结点,它的父亲的另外一个子结点叫做叔叔结点,父亲的父亲叫做祖父结点。
4、红黑树基本操作
$\color{red}{红黑树总是通过旋转和变色达到自平衡}$
1、左旋
1 | void left_rotate(RBT_Node* x) //x 表示需要进行左旋的子树的根结点 |
2、右旋
1 | //右旋操作(与左子树操作基本相同) |

3、变色
结点的颜色由红变黑或由黑变红。
5、红黑树的插入
在创建红黑树或者向已有红黑树中添加新的数据时,其实我们只需要按部就班地执行以下3 步操作:
1、由于红黑树本身是一棵二叉查找树,所以在插入新的结点时,完全按照二叉查找树插入结点的方法,找到新结点插入的位置;
2、将新插入的结点结点初始化,颜色设置为红色后插入到指定位置;(将新结点初 始化为红色插入后,不会破坏红黑树第 5 条的性质)
3、由于插入新的结点,可能会破坏红黑树第 4 条的性质(若其父结点颜色为红色,就破坏了红黑树的性质),此时需要调整二叉查找树,想办法通过旋转以及修改树中结点的颜色,使其重新成为红黑树!
第一步和第二步都比较容易,与二叉查找树差不多,最麻烦的就是第三步对树的调整。在红黑树中插入结点时,根据插入位置的不同可分为以下 3 种情况,前两种都比较简单:
第一种,插入位置为整棵树的树根。处理办法:只需要将插入结点的颜色改为黑色即可。
第二种:如果插入位置的双亲结点的颜色为黑色。处理方法:此种情况不需要做任何工作,新插入的颜色为红色的结点不会破坏红黑树的性质。
第三种情况就麻烦了,插入位置的双亲结点的颜色为红色。同样的,我们也有相对应的处理方法:因为插入结点颜色为红色,并且双亲结点也是红色的,所以破坏了红黑树第 4 条性质,那么这个时候就需要结合其祖父结点和祖父结点的另一个孩子结点(即叔叔结点)的状态,在这里我们又要分成 3 种情况来讨论:
1、当前结点的父节点是红色,且“叔叔结点”也是红色,所以破坏了红黑树的第 4 条性质。此时的解决方案就是:我们将父结点颜色改为黑色;将叔叔结点颜色改为黑色;再将祖父结点颜色改为红色;下一步将祖父结点认做当前结点,继续判断,处理结果如下图所示:
分析:此种情况下,由于父结点和当前结点颜色都是红色,所以为了不产生冲突,将父结点的颜色改为黑色。但是虽避免了破坏第 4 条,但是却导致该条路径上的黑高度增加了 1 ,破坏了第 5 条性质。但是在将祖父结点颜色改为红色、叔叔结点颜色改为黑色后,该部分子树没有破坏第 5 条性质。但是由于将祖父结点的颜色改变,还需判断是否破坏了上层树的结构,所以需要将祖父结点看做当前结点,继续判断。
2、当前结点的父结点颜色为红色,叔叔结点颜色为黑色,且当前结点是父结点的右孩子。解决方案:将父结点作为当前结点做左旋操作。
在进行以父结点为当前结点的左旋操作后,此种情况就转变成了第 3 种情况,处理过程跟第 3 种情况同步进行。
3、当前结点的父结点颜色为红色,叔叔结点颜色为黑色,且当前结点是父结点的左孩子。解决方案:将父结点颜色改为黑色,祖父结点颜色改为红色,从祖父结点处进行右旋处理。如下图所示:
分析:在此种情况下,由于当前结点 F 和父结点 S 颜色都为红色,违背了红黑树的性质 4,此时可以将 S 颜色改为黑色,又违反了性质 5,因为所有通过 S 的路径其黑高度都增加了 1 ,所以需要将其祖父结点颜色设为红色后紧接一个右旋,这样这部分子树有成为了红黑树。(上图中的右图虽看似不是红黑树,但是只是整棵树的一部分,以 S 为根结点的子树一定是一棵红黑树)
1 | RBT_Node* Insert_BST(RBT_Node*& p, RBT_Node*& r, const int& v) |
6、红黑树的删除
删除节点要比插入节点简单一点,在红黑树中删除结点,只需要完成 2 步操作:
1、将红黑树按照二叉查找树删除结点的方法删除指定结点;
2、重新调整删除结点后的树,使之重新成为红黑树;(还是通过旋转和重新着色的方式进行调整)
在树删除结点的时候,又要分为 3 种情况:
第一种:若该删除结点本身是叶子结点,则可以直接删除;
第二种:若只有一个孩子结点(左孩子或者右孩子),则直接让其孩子结点顶替该删除结点;
第三种:若有两个孩子结点,则找到该结点的右子树中值最小的叶子结点来顶替该结点,然后删除这个值最小的叶子结点。
以上这三种情况最终都需要删除某个结点,所以我们此时需要判断删除该结点是否会破坏红黑树的性质。判断的依据是:
- 如果删除结点的颜色为红色,则不会破坏;
- 如果删除结点的颜色为黑色,则肯定会破坏红黑树的第 5 条性质,此时就需要对树进行调整,调整方案又分成4种情况讨论:
1、删除结点的兄弟结点颜色是红色,调整措施为:将兄弟结点颜色改为黑色,父亲结点改为红色,以父亲结点来进行左旋操作,同时更新删除结点的兄弟结点(左旋后兄弟结点发生了变化),如下图所示:
2、删除结点的兄弟结点及其孩子全部都是黑色的,调整措施为:将删除结点的兄弟结点设为红色,同时设置删除结点的父结点标记为新的结点,继续判断;
3、删除结点的兄弟结点是黑色,其左孩子是红色,右孩子是黑色。调整措施为:将兄弟结点设为红色,兄弟结点的左孩子结点设为黑色,以兄弟结点为准进行右旋操作,最终更新删除结点的兄弟结点;
4、删除结点的兄弟结点是黑色,其右孩子是红色(左孩子不管是什么颜色),调整措施为:将删除结点的父结点的颜色赋值给其兄弟结点,然后再设置父结点颜色为黑色,兄弟结点的右孩子结点为黑色,根据其父结点做左旋操作,最后设置替换删除结点的结点为根结点;
1 | void RBT_Transplant(RBT_Node*& u, RBT_Node*& v) |
6、红黑树的应用
1、散列表的冲突处理
map的实现,底层一般会采用红黑树,在节点多的时候效率高。
在节点少的时候,可用链表方式。
2、动态插入、删除和查询较多的场景

