我们讲二叉树啊,平衡树的旋转啊,费了那么多劲,目的就是为了引出今天的主题:红黑树。红黑树是JDK中最重要的一个树型数据结构。TreeSet, TreeMap,以及最新的Hashtable都使用了红黑树。
红黑树在各种框架,开源软件,系统中十分常见。比如在linux源代码中,就有使用红黑树做为容器管理进程的代码,再比如C++的STL中,Set, Map都是使用红黑树实现的。但是红黑树不光在教材里很少出现,即使在各类博客,教程也很少出现。我觉得这可能是因为红黑树的规则太复杂所导致的。
实际上,大家接下来会看到,我的讲解虽然看上去很啰嗦,很可怕,但在JDK里,红黑树的代码十分简洁。我们先看原理。
定义和性质
红黑树是一种自平衡二叉查找树。它的统计性能要好于平衡二叉树(AVL树),因此,红黑树在很多地方都有应用。在STL中,很多部分(目前包括set,multiset,map, multimap)应用了红黑树。它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在O(logn)时间内做查找,插入和删除等操作。
与二叉查找树相比,红黑树的树结点多了一个颜色属性,而每个结点不是黑色即是红色。这就是红黑树名称的由来。树的每个节点是一个五元组:color(颜色),key(数据), left(左孩子),right(右孩子)和parent(父结点)。注意哦,红黑树是一种平衡树,但是结点的结构却出奇的简单,比起传统二叉树,只多了颜色而已。
红黑树具有以下性质:
- 根是黑色
- 所有叶子都是黑色(叶子是NIL结点)
- 如果一个结点是红的,则它的两个儿子都是黑的
- 从任一结点到其叶子的所有简单路径都包含相同数目的黑色结点
总结起来,最重要的就是红色结点不能有红色孩子,从根到任意叶子,经过的黑色结点数目相等。
插入数据
红黑树从根本上说还是排序二叉树,因此,红黑树的查找操作与二叉排序树的查找操作是一致的。
向红黑树中插入新的结点。具体做法是,将新结点的 color 赋为红色,然后以二叉排序树的插入方法插入到红黑树中去。之所以将新插入的结点的颜色赋为红色,是因为:如果设为黑色,就会导致根到叶子的路径上有一条路上,多一个额外的黑结点,这个是很难调整的。但是设为红色结点后,可能会导致出现两个连续红色结点的冲突,那么可以通过颜色调换和树旋转来调整,这样简单多了。
接下来,讨论一下插入以后,红黑树的情况。设要插入的结点为N,其父结点为P,其 祖父结点为G,其父亲的兄弟结点为U(即P和U 是同一个结点的两个子结点)。如果P是黑色的,则整棵树不必调整就已经满足了红黑树的所有性质。如果P是红色的(可知,其父结点G一定是黑色的),则插入N后,违背了红色结点只能有黑色孩子的性质,需要进行调整。
调整时分以下三种情况:
第一、N的叔叔U是红色的。将P和U重绘为黑色并重绘结点G为红色。现在新结点N有 了一个黑色的父结点P,因为通过父结点P或叔父结点U的任何路径都必定通过祖父结点G, 在这些路径上的黑结点数目没有改变。但是,红色的祖父结点G的父结点也有可能是红色 的,这就违反了性质3。为了解决这个问题,我们在祖父结点G递归向上调整颜色。如图2.14
第二、N的叔叔U是黑色的,且N是左孩子。对祖父结点G 的一次右旋转; 在旋转产生的树中,以前的父结点P现在是新结点N和以前的祖父节点 G 的父结点,然后交换以前的父结点P和祖父结点G的颜色,结果仍满足红黑树性质。如图 2.15。在(b)中,虚线代表原来的指针,实线代表旋转过后的指针。所谓旋转就是改变图中所示的两个指针的值即可。当然,在实际应用中,还有父指针p也需要修改,这里为了图示的简洁而省略掉了。
第三、N的叔叔U是黑色的,且N是右孩子。我们对P进行一次左旋转调换新结点和其父 结点的角色,就把问题转化成了第二种情况。如图 2.16所示。
红黑树插入数据的代码与二叉查找树是相同的,只是在插入以后,会对不满足红黑树性质的结点进行调整。具体的调整的代码如下所示。
代码中的 right rotate 的操作就是平衡二叉树中的右旋操作的函数。可见,红黑树的插入的调整的核心操作仍然是结点的旋转。所以深刻地理解旋转操作对于理解自平衡的二叉树具有重要意义。
好了。今天就讲这么多。今天的作业:
向树中依次插入(1,2,3,4,5,6,7,8,9,10),并且在纸上画出来红黑树变化的过程。总共画十幅图。拍成照片,发到我QQ上。
明天我们来讲红黑树的删除。后天讲代码。
上一节课:
下一节课:红黑树(二):删除
目录:课程目录
https://zhuanlan.zhihu.com/p/25358857