本篇文章深入讲述红黑树的原理与实现。红黑树是一个非常重要的数据结构,其本质上是一个平衡查找树,可是其“平衡”的条件和AVL树不同。在深入了解红黑树之前要先熟悉平衡查找树的相关知识,这里就不再介绍。
红黑树简介
红黑树性质
首先,什么是红黑树?红黑树(RB-Tree)是一种平衡二叉搜索树,除此之外还要满足以下规则:
- 每个节点不是红色就是黑色
- 根节点为黑色
- 如果节点为红色,其子节点必为黑色
- 任意节点至NULL(树末尾)的任何路径所含的黑色节点数相同
- 每一个NULL视为黑色节点
根据以上五条规则,可以推出一些推论:
- 新插入的节点必为红色节点(规则4所示,如果新增节点为黑色则会使这条路径上的黑色节点数比其他路径多1,则必须要调整红黑树)
- 一颗n个节点的红黑树,其树高度至多为2log(n+1)
红黑树应用场景与比较
由于红黑树插入、删除、查找的时间复杂度都为O(logn),所以十分利于在存储大量数据中进行查找。接下来说一下红黑树的具体应用场景。
- 首先在STL中,关联容器中map与set都是以红黑树作为底层实现。
- linux内核中有大量用到红黑树的地方,最典型的就是epoll将监听事件的fd存储在红黑树中,可以快熟查找和添加,又比如高精度计时器使用红黑树树组织定时请求,EXT3文件系统也使用红黑树树来管理目录,虚拟存储管理系统也有用红黑树进行管理等。
但是普通的平衡查找树的查询时间复杂度也是O(logn),那红黑树的优势在哪呢?实际上,平衡查找树对于平衡条件的控制非常严格,如AVL树要求任何一个节点的左右子树高度差不超过1,但是其实维持树平衡状态的开销很大,红黑树的优势就在这,红黑树对于“平衡条件”的控制不太严格,只要满足其性质就是属于平衡状态,所以其对于维持树平衡的开销较小,也是其最大的优势。
与此同时,跳表(skiplist)的插入和查询时间复杂度也是O(logn),并且跳表的实现较红黑树而言简单太多,因此Redis,LevelDB的MemTable中都采用跳表作为基本数据结构;除此之外,由于平衡查找树和跳表都是有序的(而哈希表是无序的),在范围查找的过程中跳表也比红黑树容易很多。具体参考Redis的数据结构
红黑树节点插入
接下来正式进入分析。
红黑树插入操作的时间复杂度为O(logn),具体步骤如下:
- 先把其当做二叉搜索树,将节点插入适当的位置(若插入的节点值比当前节点小则往左子树移动,否则往右子树移动);
- 按照推论1,将新插入的节点设置为红色;
- 将树经过转化变成一颗红黑树。分成三种情况:①若插入节点为根节点,则把该节点变成黑色即可(性质2)②若插入节点的父节点为黑色,则不需要做任何事情还是一颗红黑树(因为没有破坏任何一个性质)③若插入节点的父节点为红色,则又分成三种情况,如表格所示:
条件 | 步骤 |
---|---|
新加入的节点的叔节点为红色(根据性质,祖父节点必为黑色) | 1. 父亲节点设为黑节点 2.叔叔节点设为黑节点 3.祖父节点设为红节点 4. 以祖父节点为新节点继续重复 |
新加入的节点的叔节点为黑色,且新加入的节点是内侧插入(双旋转,也可以理解为两次单旋转) | 1. 将父亲节点进行左旋 2. 改变祖父节点和自己节点的颜色 3. 祖父节点进行右旋 |
新加入的节点的叔节点为黑色,且新加入的节点是外侧插入(单旋转) | 1. 将祖父节点设为红色 2. 父节点设为黑色 3. 右旋祖父节点 |
到此,红黑树的插入规则就阐述完了。
红黑树节点删除
红黑树删除操作时间复杂度也为O(logn),具体的实施步骤比插入操作复杂一点。但是也可以分成多步骤进行。
- 按照二叉查找树的方式定位并删除节点。分成三种情况:①被删除的节点没有子节点,则直接删除即可 ②如果被删除的节点只有一个子节点,则删除该节点并用其唯一子节点替代其位置 ③如果被删除的节点有两个子节点,则选取后继子节点(即左子树的最右节点),将后继节点的值替换到被删除的节点,然后删除后继节点(如果后继节点有左子节点,则连接上即可)。
- 调整平衡。调整平衡的过程中分成三种情况: ①删除的节点是红色,则不需调整 ②如果删除的节点是黑色且为根节点,则不需调整 ③如果删除的节点是黑色且不为根节点,则又可以分成四种情况,如下所示:
条件 | 策略 |
---|---|
如果被删除节点的兄弟节点是红色(此时父节点与兄弟节点的子节点都是黑色) | 1. 将兄弟节点设为黑色 2. 将父节点设为红色 3. 对父节点进行左旋 4. 左旋后,重新设置兄弟节点。 |
如果被删除节点的兄弟节点是黑色,且兄弟节点的子节点都是黑色 | 1. 将兄弟节点设为红色 2. 设置父节点为“新的被删除节点”。 |
如果被删除节点的兄弟节点是黑色,且兄弟节点的左孩子是红色,右孩子是黑色 | 1. 将兄弟节点的左孩子设为黑色 2. 将兄弟节点设为红色 3. 对兄弟节点进行右旋 4. 右旋后,重新设置兄弟节点。 |
如果被删除节点的兄弟节点是黑色,且兄弟节点的右孩子是红色 | 1. 将父节点颜色赋值给兄弟节点 2. 将父节点设为黑色 3. 将兄弟节点的右子节设为黑色 4. 对父节点进行左旋 5. 设置被删除节点为根节点。 |