红黑树一直是数据结构中的难点,大部分关于红黑树的学习资料(包括《算法导论》)对于这部分的讲解都是上来就下定义,告诉我们红黑树这个性质那个性质,插入删除要注意1234点,但是基本没有讲为什么这样定义红色和黑色,让人理解起来十分费力。直到我看了下图这本树中关于红黑树部分的讲解,一时间豁然开朗,上网查了下这本书的作者Sedgewick,他是伟大的高德纳的学生!红黑树的发明者!
他在这本书中告诉了我们红黑树的根本模型:以二叉树的形式实现2-3树,通过红黑树与2-3树之间的一一对应,让我们对红黑树有了更直观的理解。
这本树里所讲的是左偏红黑树模型,理解了这个模型,再理解算法导论的完整红黑树模型就容易的多了。
2-3树
定义。2-3查找树允许树中的一个结点保存多个键,一棵2-3查找树或为一棵空树,或由以下结点组成:
2-结点,含有一个键(及其对应的值)和两条链接,左链接指向的2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点。
3-结点,含有两个键(及其对应的值)和三条链接,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。
一棵完美平衡的2-3查找树中的所有空链接到根结点的距离都应该是相同的。这里我们用2-3树指代一棵完美平衡的2-3查找树,如下图所示。
查找。将二叉查找树的查找算法一般化我们就能够直接得到2-3树的查找算法。要判断一个键是否在树中,我们先将它和根节点中的键比较。如果它和其中任意一个相等,查找命中;否则我们就根据比较的结果找到指向相应区间的链接,并在其指向的子树中递归地继续查找。如果这个是空连接,查找未命中。
插入。要在2-3树中插入一个新结点,我们可以和二叉查找树一样先进行一次未命中的查找,如果未命中的查找结束于一个2-结点,事情就好办了:我们只要把这个2-结点替换为一个3-结点,将要插入的键保存在其中即可。如果未命中的查找结束于一个3-结点,事情就要麻烦一些。
向2-结点中插入新键。
向一棵只含有一个3-结点的树中插入新键。
向一个父节点为2-结点的3-结点中插入新键。
向一个父节点为3-结点的3-结点中插入新键。
插入结点到根节点的路径上全都是3-结点。
和标准的二叉查找树由上向下生长不同,2-3树的生长是由下向上的:随着结点的插入,临时4-结点的中键不断上浮,一旦根节点变成临时4-结点,我们可以分解根结点完成树的生长,使得树高加1。
下图给出了我们的标准索引测试用例中产生的一系列2-3树,以及一系列由同一组键按照升序一次插入到树中时所产生的所有2-3树。如果是在二叉查找树中,按照升序插入10个键会得到高度为9的一棵最差查找树,而使用2-3树,树的高度是2。
在一棵大小为N的2-3树中,查找和插入操作访问的结点必然不超过lgN个。
尽管我们可以用不同的数据类型表示2-结点和3-结点并写出变换所需的代码,这样我们需要维护两种不同类型的结点,将被查找的键和结点中的每个键进行比较,将链接和其他信息从一种结点复制到另一种结点,将结点从一种数据类型转换到另一种数据类型等等。实现这些不仅需要大量的代码,而且它们所产生的额外开销可能会使算法比标准的二叉查找树更慢。幸运的是我们可以使用红黑树来解决这个问题,它以二叉树的形式实现了2-3树。
红黑树
定义。红黑树的一种定义是含有红黑链接并满足下列条件的二叉查找树:
红链接均为左链接(左偏红黑树);
没有任何一个结点同时和两条红链接相连;
该树是完美黑色平衡的,即任意空链接到根节点的路径上的黑链接数量相同。
满足这样定义的红黑树和相应的2-3树是一一对应的。
如果我们将一棵红黑树中的红链接画平,那么所有的空链接到根节点的距离都将是相同的。如果我们将有红链接相连的结点合并,得到的就是一棵2-3树。
相反,如果将一棵2-3树中的3-结点画作由红色左链接相连的两个2-结点,那么不会存在能够和两条红链接相连的结点,且树必然是完美黑色平衡的。
红黑树既是二叉查找树,也是2-3树。因此如果我们能够在保持一一对应关系的基础上实现2-3树的插入算法,那么我们就能够将两个算法的优点结合起来:二叉查找树中简洁高效的查找算法和2-3树中高效的平衡插入算法。
颜色表示。因为每个结点都只会有一条指向自己的链接(从它的父结点指向它),我们将链接的颜色保存在表示结点的Node数据类型的布尔变量color中。如果指向它的链接是红色的,那么该变量为true,黑色则为false。我们约定空链接为黑色。
旋转。首先,假设我们有一条红色的右链接需要被转化为左链接,这个操作叫左旋转。它只是将用两个键中的较小者作为根节点变为将较大者作为根节点。
实现将一个红色左链接转换为红色右链接的右旋转的代码完全相同,只需要将left换成right即可。
插入。在插入新的键时我们可以使用旋转操作帮助我们保证红黑树和2-3树之间的一一对应关系,因为旋转操作可以保持红黑树的两个重要性质:有序性和完美平衡性。我们只需考虑如何使用旋转操作来保持红黑树的另外两个重要性质:不存在两条连续的红链接和不存在红色的右链接。
用和二叉查找树相同的方式向一棵红黑树中插入一个新键会在树的底部新增一个结点,但总是用红链接将新结点和它的父结点相连。
向2-结点中插入新键。
向树底部的2-结点插入新键。
向一个3-结点中插入新键。这种情况又可分为三种子情况:新键大于树中的两个键,小于树中的两个键,或是在两者之间。
颜色转换。我们专门用一个方法flipColors()来转换一个结点的两个红色子结点的颜色。颜色转换会使根结点变成红色,两个子结点变成黑色。
向树底部的3-结点插入新键。
红黑树构造轨迹。
删除。和插入操作一样,我们也可以定义一系列局部变换来在删除一个结点的同时保持树的完美平衡性。这一过程比插入一个结点更加复杂,因为我们不仅要在(为了删除一个结点而)构造临时4-结点时沿着查找路径向下进行变换,还要在分解遗留的临时4-结点时沿着查找路径向上进行变换(同插入操作)。
删除最小键。从树底部的3-结点中删除键是很简单的,但2-结点则不然。从2-结点中删除一个键会留下一个空结点,一般我们会将它替换为一个空链接,但这样会破坏树的完美平衡性。为了保证我们不会删除一个2-结点,我们沿着左链接向下进行变换(最小键一定在左链接上),确保当前结点不是2-结点(即左链接上的结点可以是3-结点,也可以是临时的4-结点),最后能够得到一个含有最小键的3-结点或者临时4-结点,然后我们就可以直接从中将其删除,将3-结点变为2-结点,或者将临时4-结点变为3-结点。然后我们再回头向上分解所有临时的4结点。
删除任意键。在查找路径上进行和删除最小键相同的变换同样可以保证在查找过程中任意当前结点均不是2-结点。如果被查找的的键在树的底部,我们可以直接删除它。如果不在,我们需要将它和它的后继结点交换。因为当前结点必然不是2-结点,问题已经转化为在一棵根节点不是2-结点的子树中删除最小的键,我们可以在这棵子树中使用上述的算法,删除之后我们需要向上回溯并分解余下的4-结点。