目录
红黑树的概念
红黑树是一种自平衡二叉搜索树(Binary Search Tree, BST),其每个节点带有颜色属性,可以是红色或黑色。红黑树通过约束节点颜色和树的结构来确保树的高度在最坏情况下是O(log n),从而保证了基本的动态集合操作(如插入、删除和查找)在最坏情况下的时间复杂度都是O(log n)。
红黑树通过对任何一条从根到叶子的路径上各个结点着色方式的限制,确保没有一条路径会比其他路径长出两倍,因此红黑树是近似平衡的。
性质
红黑树是每个节点都带有颜色属性的二叉搜索树,颜色为红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
- 节点是红色或黑色。
- 根是黑色。
- 所有叶子都是黑色(叶子是NIL节点)。
- 每个红色节点必须有两个黑色的子节点。(或者说从每个叶子到根的所有路径上不能有两个连续的红色节点。)(或者说不存在两个相邻的红色节点,相邻指两个节点是父子关系。)(或者说红色节点的父节点和子节点均是黑色的。)
- 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
下面是一个具体的红黑树的图例:
这些约束确保了红黑树的关键特性:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉搜索树。
要知道为什么这些性质确保了这个结果,注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。
在很多树数据结构的表示中,一个节点有可能只有一个子节点,而叶子节点包含数据。用这种范例表示红黑树是可能的,但是这会改变一些性质并使算法复杂。为此,本文中我们使用“nil叶子”,如上图所示,它不包含数据而只充当树在此结束的指示。这些节点在绘图中经常被省略,导致了这些树好像同上述原则相矛盾,而实际上不是这样。与此有关的结论是所有节点都有两个子节点,尽管其中的一个或两个可能是空叶子。
在红黑树中,NIL节点的作用
- 结束标志:在树的遍历过程中,NIL节点表示子树的结束位置。
- 保持树的性质:通过使用NIL节点,可以确保所有叶子节点是黑色的,从而保持红黑树的性质。
- 简化操作:在插入、删除和旋转操作中,NIL节点的存在简化了对边界情况的处理,使得代码更加简洁和一致。
红黑树如何确保从根到叶子的最长可能路径不会超过最短可能路径的两倍?
根据红黑树的性质4可以得出,红黑树当中不会出现连续的红色节点,而根据性质4又可以得出,从某一结点到其后代叶子节点的所有路径上包含的黑色节点的数目是相同的。
我们假设在红黑树中,从根到叶子的所有路径上包含的黑色结点的个数都是N个,那么最短路径就是全部由黑色节点构成的路径,即长度为N。
而最长可能路径就是由一黑一红结点构成的路径,该路径当中黑色结点与红色结点的数目相同,即长度为2N。
因此,红黑树从根到叶子的最长可能路径不会超过最短可能路径的两倍。
结点的定义
我们这里直接实现KV模型的红黑树,为了方便后序的旋转操作,将红黑树的结点定义为三叉链结构,除此之外还新加入了一个成员变量,用于表示结点的颜色。
enum Color { RED, BLACK };
template<class K, class V>
struct RBTreeNode
{
// 三叉链
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
// 存储的键值对
std::pair<K, V> _kv;
// 结点的颜色
Color _col;
// 构造函数
RBTreeNode(const std::pair<K, V>& kv)
: _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _col(RED)
{}
};
-
枚举类型
Color
:定义了枚举类型来表示节点的颜色,红色为RED
,黑色为BLACK
。 -
模板结构体
RBTreeNode
:定义了一个模板类,用于表示红黑树的节点。-
成员变量:
_left
:指向左孩子节点。_right
:指向右孩子节点。_parent
:指向父节点。_kv
:存储键值对,类型为std::pair<K, V>
。_col
:节点的颜色,类型为Color
。
-
构造函数:接受一个键值对,并初始化节点的指针和颜色。初始颜色设置为红色
RED
。
-
为什么构造结点时,默认将节点的颜色设置为红色?
当我们向红黑树插入节点时,若我们插入的是黑色节点,那么插入路径上黑色节点的数目就比其他路径上黑色节点的数目多了一个,即破坏了红黑树的性质5,此时我们就需要对红黑树进行调整。
若我们插入红黑树的节点是红色的,此时如果其父节点也是红色的,那么表明出现了连续的红色节点,即破坏了红黑树的性质4,此时我们需要对红黑树进行调整;但如果其父节点是黑色的,那我们就无需对红黑树进行调整,插入后仍满足红黑树的要求。
总结一下:
插入黑色节点,一定破坏红黑树的性质5,必须对红黑树进行调整。
插入红色节点,可能破坏红黑树的性质4,可能对红黑树进行调整。
权衡利弊后,我们在构造节点进行插入时,默认将节点的颜色设置为红色。
插入
红黑树插入结点的逻辑分为三步:
- 按二叉搜索树的插入方法,找到待插入位置。
- 将待插入节点插入到树中。
- 若插入节点的父结点是红色的,则需要对红黑树进行调整。
其中前两步与二叉搜索树插入结点时的逻辑相同,红黑树的关键在于第三步对红黑树的调整。
在下面的示意图中,将要插入的节点标为N,N的父节点标为P,N的祖父节点标为G,N的叔父节点标为U。在图中展示的任何颜色要么是由它所处情形这些所作的假定,要么是假定所暗含的。
情形1:新节点N位于树的根上,没有父节点。在这种情形下,我们把它重绘为黑色以满足性质2。因为它在每个路径上对黑节点数目增加一,性质5符合。
情形2:新节点的父节点P是黑色,所以性质4没有失效(新节点是红色的)。在这种情形下,树仍是有效的。性质5也未受到威胁,尽管新节点N有两个黑色叶子子节点;但由于新节点N是红色,通过它的每个子节点的路径就都有同通过它所取代的黑色的叶子的路径同样数目的黑色节点,所以依然满足这个性质。
注意:在下列情形下我们假定新节点的父节点为红色,所以它有祖父节点;因为如果父节点是根节点,那父节点就应当是黑色。所以新节点总有一个叔父节点&#