本文支队红黑树涉及的概念做一些总结,对于红黑树的插入(红黑树的插入操作分为三种情况),删除(红黑树的删除操作分为四种情况)等比较复杂的操作(插入删除等操作都可以通过变色,左旋,右旋等操作来实现)及时间复杂度的推理过程不做总结,如果感兴趣的话可以参考《算法导论》
红黑树是二叉查找树的一种,为了理解红黑树,让我们先来看看二叉查找树。
二叉查找树
二叉查找树对应的英文名称是Binary Search Tree(BST),是一种数据结构,它支持多种动态集合操作,包括search,minimum,maximum,predecessor,successor,insert,以及delete。它既可以用作字典,也可以用作优先队列。二叉查找树是按二叉树结构来组织的。二叉查找树可以用链表结构表示(完全二叉树也可以使用数组来表示),其中每一个结点都是一个对象。结点中除了key域和卫星数据外,还包含域left,right,和p,它们分别指向结点的左儿子、右儿子和父结点。如果某个儿子结点或者父结点不存在,则相应域的值为NIL。根结点是树中唯一的父结点域为NIL的结点。
二叉查找树有如下性质:
设x为二叉查找树中的一个结点。如果y是x的左子树中的一个结点,则key[y]
≤
\leq
≤key[x]。如果y是x的右子树中的一个结点,则key[x]
≤
\leq
≤key[y]。
如下所示是一棵典型的二叉查找树:
上图是一棵包含6个结点、高度为2的二叉查找树。
如下所示是另外一棵二叉查找树:
上图是一棵效率较低的二叉查找树,它包含同样的关键字,但高度为4。
综上所述,我们可以知道二叉查找树并不是平衡二叉树。更重要的一点是对于高度为h的树,minimum,search,maximum,successor,predecessor等动态集合的操作都可以在O(h)时间内完成(该结论可以参考《算法导论》)。而一棵二叉查找树在左右子树高度只差为1时,这棵树的高度是最低的,也就是h值最小,这个时候二叉查找树的常见操作速度也是最快的。因此从二叉查找树中衍生了很多平衡二叉树,这些树的左右子树高度相差不大,其中的红黑树就是一种近似平衡的二叉查找树
红黑树
红黑树是一种二叉查找树,但是每个结点上增加一个存储位表示结点的颜色,可以是red或black。通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍(为什么最多会是两倍?这个是红黑树的定义规定的,原因后面会有解释。)。所谓的平衡二叉树这个概念有点空泛,严格的平衡二叉树是指AVL树,也就是任一节点对应的两棵子树的最大高度差为1,注意此处,并不是左子树高度和右子树高度相同,或者左子树高度比右子树高度大1,而是左右子树高度差为1,可能右子树比左子树高。也因为红黑树中没有一条路径会比其他路径长出两倍,所以红黑树是接近平衡的,因此很多资料上把红黑树当做平衡二叉树的一种。红黑树的能保证在最坏的情况下,基本的动态集合操作时间为O(lg n)。
树中每个结点包含五个域:color,key,left,right和p。如果某结点没有一个子结点或父结点,则该结点相应的指针域包含值NIL。
红黑树的定义:
- 每一个结点或者是红色的,或者是黑色的。
- 根节点是黑色的。
- 每个叶结点(NIL)是黑色的。
- 如果一个结点是红色的,则它的两个儿子是黑色的,也就是说不能连续出现了两个红色结点。
- 对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑色结点。
综上所述:红黑树中把NIL空指针也视为一个黑色的结点,由于结点到其子孙的所有路径上包含相同数目的黑色,而且由于不能连续出现两个红色结点,因此可以知道,红黑树确保没有一条路径会比其他路径长出两倍。
如下所示是一棵典型的红黑树,在一棵红黑树中,每个结点或则是红色的或者是黑色的。红结点的两个儿子结点都是黑色的,并且从每个结点到其子孙结点叶结点的每条路径上,都包含相同数目的黑色结点:
但是如上所述红黑树结构比较耗费存储空间,因此实际上大多数红黑树结构会采用一个哨兵来代表NIL,对一棵红黑树T来说,哨兵nil[T]是一个与树内普通结点有相同域的对象(或者说结点)。它的color域是black,而其他的域p,left,right和key可以设置成任意允许的值。如下图所示:
红黑树的意义
由于二叉查找树的的基本动态集合操作时间复杂度都是O(h),其中h是二叉查找树的高度。而二叉查找树在经历多次删除结点和插入结点后在极端情况下会变成链表结构,这时基本操作的时间复杂度变成O(n),其中n是二叉查找树的元素数量。因此科学家们提出了能保证没有一条路径会比其他路径长出两倍的红黑树,来确保红黑树的高度不会和最优高度相差太大。
为什么实际应用中更平衡的AVL树应用范围没有红黑树这么广?
这其实因为AVL树是严格的平衡二叉树,导致对AVL树进行删除操作和插入操作需要经历更多次的旋转,因此在插入和删除操作等方面都不如红黑树高性能。
下图是一张性能对比图:
B树和红黑树的应用范围的区别?
B树或者及其变形更多的是基于读取一次磁盘驱动器的时间和读取一些内存的时间相差几个数量级,因此科学家们提出了一种读写磁盘驱动器最小次数的算法,B树及其变形大量应用于数据库的索引。而红黑树是一种在内存内部实现的数据结构,是一种能保证最坏情况下基本的动态集合操作的时间复杂度在O(lg n)的算法。Java中的TreeSet,TreeMap以及Java8中的HashMap都是使用红黑树结构实现的。
红黑树旋转操作
由于在平衡二叉查找树中旋转操作比较常见,因此特此记录旋转操作的具体过程。
由于对二叉查找树中进行插入和删除操作时会改变红黑树的特性,为保持这些性质,就要改变树中某些结点的颜色以及指针结构。指针结构的修改是通过旋转来完成的,这是一种能保持二叉查找树性质的查找树局部操作。下图给出了两种旋转:左旋和右旋。
二叉查找树中的旋转操作。左旋操作通过改变常数个指针来将左边两个结点的结构转变成右边的结构。字母α,β和γ代表任意子树。旋转操作保留二叉查找树的属性(左子树各关键字值都不大于根节点的关键字值,右子树各关键字都不小于根结点的关键字值)。
当在某个结点x上做左旋时,我们假设他的右孩子y不是nil[T](nil[T]就是nil哨兵,也就是说红黑树中所有的结点中的空指针nil,都指向这个nil[T]哨兵,比如叶子结点的左右子树,根结点的父结点);x可以为树内任意右孩子不是nil[T]的结点。左旋以x到y之间的链为“支轴”进行。它使y称为该子树新的根,x成为y的左孩子,而y的左孩子则成为x的右孩子。
假设红黑树是T,x是红黑树中任意结点,x.right!=nil[T],并且根的父结点是nil[T],伪代码如下所示:
left_rotate(T, x) {
y = x.right // 设置y
x.right = y.left // 把y的左子树设置为x的右子树
if (y.left != nil[T]) { //
y.left.p = x // 设置原y的左子树的父结点为x
}
y.p = x.p // 设置x的父结点为y
if (x.p==nil[T]) {
root[T] = y
} else if (x = x.p.left) {
x.p.left = y
} else {
x.p.right = y
}
y.left = x // 设置y的左子树为x
x.p = y
}
参考:
《算法导论》
红黑树比 AVL 树具体更高效在哪里?
红黑树深入剖析及Java实现
漫画:什么是红黑树?