要了解红黑树,就需要了解红黑树是为了解决什么问题,先了解一些基础知识作为铺垫
一、二叉搜索树
一颗二叉查找树(BST)是一颗二叉树,其中每个节点都含有一个可比较的键(以及相关联的值)
其在二叉树的基础上实现了:
每个结点的键都大于其左子树中的任意结点的键而小于右子树的任意结点的键
如果我们将一颗二叉查找树的所有键投影到一条直线上,保证一个结点的左子树中的键出现在它的右边,右子树中的键出现在它的右边,那么我们一定可以得到一条有序的键列
1.二叉查找树的查找
在二叉查找树中查找一个键的递归算法:
如果被查找的键和根结点的键相等,查找命中。否则我们就在适当的子树中继续查找。如果被查找的键较小就选择左子树,较大就选择右子树。
在二叉查找树中,随着我们不断向下查找,当前结点所表示的子树的大小也在减小(理想情况下是减半)
2.二叉查找树的插入
当查找一个不存在于树中的结点并结束于一条空链接时,我们需要做的就是将链接指向一个含有被查找的键的新结点。如果被查找的键小于根结点的键,我们会继续在左子树中插入该键,否则在右子树中插入该键。
3.存在的问题
使用二叉查找树的算法的运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。
在最好的情况下,一颗含有N个结点的树是完全平衡的,每条空链接和根结点的距离都为~lgN。在最坏的情况下,搜索路径上可能有N个结点。但在一般情况下树的形状和最好情况更接近。
我们假设键的插入顺序是随机的。对这个模型的分析而言,二叉查找树和快速排序几乎就是“双胞胎”。树的根结点就是快速排序中的第一个切分元素(左侧的键都比它小,右侧的键都比它大),而这对于所有的子树同样适用,这和快速排序中对于子数组的递归排序完全对应。
在由N个随机键构造的二叉查找树中,查找命中平均所需的比较次数为~2lgN。 N越大这个公式越准确
二、2-3搜索树
在一颗含有N个结点的树中,我们希望树高为~lgN,这样我们就能保证所有查找都能在~lgN此比较内结束,就和二分查找一样。不幸的是,在动态插入中保证树的完美平衡的代价太高了。所以2-3搜索树出现了
1.定义
为了保证查找树的平衡性,我们需要一些灵活性,因此在这里我们允许树中的一个结点保存多个键。
2-结点:含有一个键和两条链接,左链接指向的2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点。
3-结点:含有两个键和三条链接,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。
2.查找
要判断一个键是否在树中,我们先将它和根结点中的键比较。如果它和其中的任何一个相等,查找命中。否则我们就根据比较的结果找到指向相应区间的链接,并在其指向的子树中递归地继续查找。如果这是个空链接,查找未命中。
3.插入
要在2-3树中插入一个新结点,我们可以和二叉查找树一样先进行一次未命中的查找,然后把新结点挂在树的底部。但这样的话树无法保持完美平衡性。我们使用2-3树的主要原因就在于它能够在插入之后继续保持平衡。
如果未命中的查找结束于一个2-结点,我们只要把这个2-结点替换为一个3-结点,将要插入的键保存在其中即可。如果未命中的查找结束于一个3-结点,需要按如下规则进行转换。
a.只有一个3-结点的树,向其插入一个新键
先考虑最简单的例子:只有一个3-结点的树,向其插入一个新键。
这棵树唯一的结点中已经没有可插入的空间了。我们又不能把新键插在其空结点上(破坏了完美平衡)。为了将新键插入,我们先临时将新键存入该结点中,使之成为一个4-结点。创建一个4-结点很方便,因为很容易将它转换为一颗由3个2-结点组成的2-3树(如图所示),这棵树既是一颗含有3个结点的二叉查找树,同时也是一颗完美平衡的2-3树,其中所有空链接到根结点的距离都相等。
b.向一个父结点为2-结点的3-结点中插入新键
假设未命中的查找结束于一个3-结点,而它的父结点是一个2-结点。在这种情况下我们需要在维持树的完美平衡的前提下为新键腾出空间。
我们先像刚才一样构造一个临时的4-结点并将其分解,但此时我们不会为中键创建一个新结点,而是将其移动至原来的父结点中。(如图所示)
这次转换也并不影响(完美平衡的)2-3树的主要性质。树仍然是有序的,因为中键被移动到父结点中去了,树仍然是完美平衡的,插入后所有的空链接到根结点的距离仍然相同。
c.向一个父结点为3-结点的3-结点中插入新键
假设未命中的查找结束于一个3-结点,而它的父结点是一个3-结点。
我们再次和刚才一样构造一个临时的4-结点并分解它,然后将它的中键插入它的父结点中。但父结点也是一个3-结点,因此我们再用这个中键构造一个新的临时4-结点,然后在这个结点上进行相同的变换,即分解这个父结点并将它的中键插入到它的父结点中去。
我们就这样一直向上不断分解临时的4-结点并将中键插入更高的父结点,直至遇到一个2-结点并将它替换为一个不需要继续分解的3-结点,或者是到达3-结点的根。
4.总结:
先找插入结点,若结点有空(即2-结点),则直接插入。如结点没空(即3-结点),则插入使其临时容纳这个元素,然后分裂此结点,把中间元素移到其父结点中。对父结点亦如此处理。(中键一直往上移,直到找到空位,在此过程中没有空位就先搞个临时的,再分裂。)
★2-3树插入算法的根本在于这些变换都是局部的:除了相关的结点和链接之外不必修改或者检查树的其他部分。每次变换中,变更的链接数量不会超过一个很小的常数。所有局部变换都不会影响整棵树的有序性和平衡性。
来一个完整的流程,和标准的二叉查找树由上向下生长不同,2-3树的生长是由下向上的
5.优点
2-3树在最坏情况下仍有较好的性能。每个操作中处理每个结点的时间都不会超过一个很小的常数,且这两个操作都只会访问一条路径上的结点,所以任何查找或者插入的成本都肯定不会超过对数级别。
完美平衡的2-3树要平展的多。例如,含有10亿个结点的一颗2-3树的高度仅在19到30之间。我们最多只需要访问30个结点就能在10亿个键中进行任意查找和插入操作。
6.缺点
我们需要维护两种不同类型的结点,查找和插入操作的实现需要大量的代码,而且它们所产生的额外开销可能会使算法比标准的二叉查找树更慢。
平衡一棵树的初衷是为了消除最坏情况,但我们希望这种保障所需的代码能够越少越好。
三、红黑树
红黑树的标准定义如下:
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。
在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
- 性质1. 节点是红色或黑色。
- 性质2. 根节点是黑色。
- 性质3 每个叶节点(NIL节点,空节点)是黑色的。
- 性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
通过这个定义来了解红黑树比较抽象。所以这里先引入《史上最简单清晰的红黑树讲解》一文对红黑树进行解释,非常容易理解。(该文中用红链接代替了红节点,其本质是一样的,其红链接指向的节点就为传统我们了解的红黑树的红节点)
其讲解如下:
理解红黑树一句话就够了:红黑树就是用红链接表示3-结点的2-3树。
那么红黑树的插入、构造就可转化为2-3树的问题,即:在脑中用2-3树来操作,得到结果,再把结果中的3-结点转化为红链接即可。而2-3树的插入,前面已有详细图文,实际也很简单:有空则插,没空硬插,再分裂。 这样,我们就不用记那么复杂且让人头疼的红黑树插入旋转的各种情况了。只要清楚2-3树的插入方式即可。 下面图文详细演示。
红黑树的本质: 红黑树是对2-3查找树的改进,它能用一种统一的方式完成所有变换。
1.替换3-结点
★红黑树背后的思想是用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。
我们将树中的链接分为两种类型:红链接将两个2-结点连接起来构成一个3-结点,黑链接则是2-3树中的普通链接。确切地说:
我们将3-结点表示为由一条左斜的红色链接相连的两个2-结点。
这种表示法的一个优点是,我们无需修改就可以直接使用标准二叉查找树的get()方法。对于任意的2-3树,只要对结点进行转换,我们都可以立即派生出一颗对应的二叉查找树。我们将用这种方式表示2-3树的二叉查找树称为红黑树。
总结如下:
- ⑴红链接均为左链接。
- ⑵没有任何一个结点同时和两条红链接相连。
- ⑶该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同。
如果我们将一颗红黑树中的红链接画平,那么所有的空链接到根结点的距离都将是相同的。如果我们将由红链接相连的结点合并,得到的就是一颗2-3树。
相反,如果将一颗2-3树中的3-结点画作由红色左链接相连的两个2-结点,那么不会存在能够和两条红链接相连的结点,且树必然是完美平衡的。
无论我们用何种方式去定义它们,红黑树都既是二叉查找树,也是2-3树。
(2-3树的深度很小,平衡性好,效率高,但是其有两种不同的结点,实际代码实现比较复杂。而红黑树用红链接表示2-3树中另类的3-结点,统一了树中的结点类型,使代码实现简单化,又不破坏其高效性。)
颜色表示:
因为每个结点都只会有一条指向自己的链接(从它的父结点指向它),我们将链接的颜色保存在表示结点的Node数据类型的布尔变量color中(若指向它的链接是红色的,那么该变量为true,黑色则为false)。
当我们提到一个结点颜色时,我们指的是指向该结点的链接的颜色。
旋转
在我们实现的某些操作中可能会出现红色右链接或者两条连续的红链接,但在操作完成前这些情况都会被小心地旋转并修复。
(我们在这里不讨论旋转的几种情况,把红黑树看做2-3树,自然可以得到正确的旋转后结果)
2.插入
在插入时我们可以使用旋转操作帮助我们保证2-3树和红黑树之间的一一对应关系,因为旋转操作可以保持红黑树的两个重要性质:有序性和完美平衡性。
a.向2-结点中插入新键
(向红黑树中插入操作时,想想2-3树的插入操作。红黑树与2-3树在本质上是相同的,只是它们对3结点的表示不同。
向一个只含有一个2-结点的2-3树中插入新键后,2-结点变为3-结点。我们再把这个3-结点转化为红结点即可)
b.向一颗双键树(即一个3-结点)中插入新键
(向红黑树中插入操作时,想想2-3树的插入操作。你把红黑树当做2-3树来处理插入,一切都变得简单了)
(向2-3树中的一个3-结点插入新键,这个3结点临时成为4-结点,然后分裂成3个2结点)
3.★一颗红黑树的构造全过程
分割线————————————————————————————————————————
前文通过了一种独特的视角对红黑树进行了解释。如果我们要在代码层去了解红黑树,需要阅读下文中对红黑树插入旋转删除的解读
首先再贴一遍红黑树的5个原则
– 性质1. 节点是红色或黑色。
– 性质2. 根节点是黑色。
– 性质3 每个叶节点(NIL节点,空节点)是黑色的。
– 性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
– 性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
一、插入节点
对于新节点的插入有如下三个关键地方:
1、插入新节点总是红色节点 。
2、如果插入节点的父节点是黑色, 能维持满足5个原则,插入成功。
3、如果插入节点的父节点是红色, 则破坏了性质4**从每个叶子到根的所有路径上不能有两个连续的红色节点**的原则. 故插入后需要通过对红黑树进行重新着色或旋转的方法, 来维持5个原则 。
先了解一下什么是旋转:
左旋:
以某个节点为支点,将它右下方的子节点转到它的右上方作为它的父节点
右旋转:
以某个节点为支点,将它左下方的子节点旋转到它的左上方作为它的父节点
其实进行插入节点操作会遇到的所有情况可以归纳如下,我们只需要针对不同的情况做不同的处理就可以了
1.插入节点为跟节点
若新插入的节点N没有父节点,则直接当做根据节点插入即可,同时将颜色设置为黑色。(如图一(1))
2.插入节点的父节点为黑色
这种情况新节点N同样是直接插入,同时颜色为红色,由于根据规则四它会存在两个黑色的叶子节点,值为null。同时由于新增节点N为红色,所以通过它的子节点的路径依然会保存着相同的黑色节点数,同样满足规则5。(如图一(2))
3.若插入节点的父节点P和P的兄弟节点U都为红色
对于这种情况若直接插入肯定会出现不平衡现象。破坏了性质4**从每个叶子到根的所有路径上不能有两个连续的红色节点**的原则
怎么处理?处理的方式为颜色转换(注:插入节点永远为红色不能变)
P、U节点变黑、G节点变红。这时由于经过节点P、U的路径都必须经过G所以在这些路径上面的黑节点数目还是相同的。
但是要注意的是经过上面的处理,可能G节点的父节点也是红色,这个时候我们需要将G节点当做新增节点递归处理。
4.若父节点P为红色,叔父节点U为黑色或者缺少,且新增节点N为P节点的左孩子
解法:当前节点为轴,进行右旋(右上的父节点旋转到右下)。效果如下
当然旋转完的结果还是不符合第4条原则的,这就进入了情况5,如下
5.若父节点P为红色,叔父节点U为黑色或者缺少,且新增节点N为P节点的右孩子
解法:父节点N变为黑色,祖父节G点变为红色,以父节点N为支点进行右旋(右上的父节点旋转到右下)。效果如下
可以看到,在插入P节点前,G节点以下有2个黑节点(包括G),进过处理后 P节点代替了G的位置,P节点以下也是只有两个黑节点(包括P)
同时还保证了红黑树的5个原则
本文大部分内容从网络获取,再加上自己的理解做的总结
内容来源:
《史上最简单清晰的红黑树讲解》
教你透彻了解红黑树