AVL树
AVL树叫做平衡二叉树,它的前提是二叉排序树(BST或叫做二叉查找树)。由于在生成BST树的过程中可能会出现线型树结构,比如插入的顺序是:1, 2, 3, 4, 5, 6, 7..., n。
定义:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
在BST树中,比较理想的状况是每个子树的左子树和右子树的高度相等,此时搜索的时间复杂度是log(N)。
可是,一旦这棵树演化成了线型树的时候,这个理想的情况就不存在了,此时搜索的时间复杂度是O(N),在数据量很大的情况下,我们并不愿意看到这样的结果。
现在我们要做的事就是让BST在创建的过程中不要向线型树发展。方法就是让其在添加新节点的时候,不断调整树的平衡状态。
节点失衡
对于节点平衡有这样的定义:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。而这里提到的高度差,就是我们下面会引入的平衡因子:BF(balance factor)。
因为AVL树说到底还是一个二叉树,只有两个子节点。而且节点失衡的发生,是因为有一个新节点的插入,这个新插入的节点导致了某些节点左右子节点高度的不一致。所以我们可以枚举出以下4种情况的失衡状态。
- 在一个节点的左子树的左子树上插入一个新节点。即LL。在这种情况下,我们可以通过将节点右旋使其平衡。如图-2所示
原A的左孩子B变为父结点,A变为其右孩子,而原B的右子树D变为A的左子树,注意旋转之后D是A的左子树。
- 在一个节点的右子树的右子树上插入一个新节点。即RR。在这种情况下,我们可以通过将节点左旋使其平衡。如图-3所示
这时只需要把树向左旋转一次即可,如图所示,原A右孩子B变为父结点,A变为其左孩子,而原B的左子树Blh将变为A的右子树。
- 在一个节点的左子树的右子树上插入一个新节点。即LR。在这种情况下,我们不能直接通过将节点左旋或右来使其平衡了。这里需要两步来完成,先让树中高度较低的进行一次左旋(RR型),这个时候就变成了LL了。再进行一次单右旋操作即可。如图-4所示;
- 左旋一次
右旋一次
总结:LR这种情况,需要旋转两次,仅一次的旋转是不能够使二叉树再次平衡。如图所示,在B节点按照RR型向左旋转一次之后,二叉树在A节点仍然不能保持平衡,这时还需要再向右旋转一次。
- 在一个节点的右子树的左子树上插入一个新节点。即RL。在这种情况下,我们不能直接通过将节点左旋或右来使其平衡了。这里需要两步来完成,先让树中高度较低的进行一次右旋,这个时候就变成了RR了。再进行一次单左旋操作即可。如图-5所示;
- 右旋一次
左旋一次
平衡二叉树某一节点的右孩子的左子树上插入一个新的节点,使得该节点不再平衡。同样,这时需要旋转两次,旋转方向刚好同LR型相反。
从上面对节点失衡的说明,以及图解。我想你已经对旋转的操作有了一个大概地认识了吧。从图中我们也可以看出,LL型和RR型、LR型和RL型是两个行为很相似地操作。其实他们互为对称。
红黑树
红黑树是一种很有意思的平衡检索树。它的统计性能要好于平衡二叉树(有些书籍根据作者姓名,Adelson-Velskii和Landis,将其称为AVL-树),因此,红黑树在很多地方都有应用。在C++ STL中,很多部分(目前包括set, multiset, map, multimap)应用了红黑树的变体(SGI STL中的红黑树有一些变化,这些修改提供了更好的性能,以及对set操作的支持)。
红黑树的定义如下:
- 满足下列条件的二叉搜索树是红黑树
- 每个结点要么是“红色”,要么是“黑色”
- 所有的叶结点都是空结点,并且是“黑色”的。
- 如果一个结点是“红色”的,那么它的两个子结点都是“黑色”的。
- (注:也就是說,如果結點是黑色的,那么它的子節點可以是紅色或者是黑色的)。
- 结点到其子孙结点的每条简单路径都包含相同数目的“黑色”结点
- 根结点永远是“黑色”的
之所以称为红黑树的原因就在于它的每个结点都被“着色”为红色或黑色。这些结点颜色被用来检测树的平衡性。但需要注意的是,红黑树并不是平衡二叉树,恰恰相反,红黑树放松了平衡二叉树的某些要求,由于一定限度的“不平衡”,红黑树的性能得到了提升。
从根结点到叶结点的黑色结点数被称为树的“黑色高度”(black-height)。前面关于红黑树的性质保证了从根结点到叶结点的路径长度不会超过任何其他路径的两倍。
考虑一棵黑色高度为3的红黑树:从根结点到叶结点的最短路径长度显然是2(黑-黑-黑),最长路径为4(黑-红-黑-红-黑)。
不可能在最长路经中加入更多的黑色结点,红色结点的子结点必须是黑色的,因此在同一简单路径中不允许有两个连续的红色结点。综上,我们能够建立的最长路经将是一个红黑交替的路径。
由此我们可以得出结论:对于给定的黑色高度为n的红黑树,从根到叶结点的简单路径的最短长度为n-
1,最大长度为2(n-1)。
插入和删除操作中,结点可能被旋转以保持树的平衡。红黑树的平均和最差搜索时间都是O(log2 n)。Cormen [2001]给出了对于这一结论的证明。在实际应用中,红黑树的统计性能要好于平衡二叉树,但极端性能略差。
红黑树中结点的插入过程
插入结点的过程是:
- 在树中搜索插入点
- 新结点将替代某个已经存在的空结点,并且将拥有两个作为子结点的空结点
- 新结点标记为红色,其父结点的颜色根据红黑树的定义确定;如果需要,对树作调整
注意 空结点和NULL指针是不同的。在简单的实现中,可以使用作为“监视哨”,标记为黑色的公共结点作为前面提到的空结点。
给一个红色结点加入两个空的子结点符合性质4,同时,也必须确保红色结点的两个子结点都是黑色的(根据性质3)。尽管如此,当新结点的父结点时红色时,插入红色的子结点将是违反定义的。这时存在两种情况。
红色父结点的兄弟结点也是红色的
简单地对上级结点重新着色将解决冲突。当结点B被重新着色之后,应该重新检验更大范围内树结点的颜色,以确保整棵树符合定义的要求。结束时根结点应当是黑色的,如果它原先是红色的,则红黑树树的黑色高度将递增1。
红色父结点的兄弟结点是黑色的
这种情形比较复杂。
重新对结点着色将把结点A变成黑色,于是,树的平衡将被破坏,因为左子树的黑色高度将增加,而右子树的黑色高度没有相应地改变。如果我们把结点B着上红色,那么左右子树的高度都将减少,树依然不平衡。此时,继续对结点C进行着色将导致更糟糕的情况,左子树黑色高度增加,右子树黑色高度减少。为了解决问题,需要旋转并对树结点进行重新着色。这时算法将正常结束,因为子树的根结点(A)被着色为黑色,同时,不会引入新的红-红冲突。
结束插入过程
插入结点时,可能会需要重新着色,或者旋转,来保持红黑树的性质。如果旋转完成,那么算法就结束了。对于重新着色来说,我们会在子树的根留下一个红色结点,于是需要继续向上对树进行修整,以保持红黑树的性质。最坏情况下,我们将不得不对到树根的所有路径进行处理。插入的时间复杂度为O(log2 n)。删除结点的时间复杂度与此类似。
结点的删除
红黑树的结点删除情况要比插入复杂一些。我们可以把实际的删除操作分成3种情况(先不讨论颜色),其中被删除的结点用紫色标记,蓝色表示任意颜色的结点,可能是红色,也可能是黑色:
情况a: 被删除的结点没有子结点(两个子结点都是空结点)
原先属于X的空结点被A“继承”。如果被删除结点是黑色结点,则可能造成树的黑色高度变化。
情况b: 有一个子结点
B结点取代原X结点的位置。如果被删除的结点是黑色结点,则可能造成树的黑色高度发生变化;如果B是红色结点,还可能需要重新着色。
情况c: 有两个子结点
这种情形比较复杂。需要将X和它的左子树中的键值最大的结点进行交换。这通常会导致重新着色,对树的黑色高度的改变,以及随之而来的旋转。
需要说明的是,仅仅删除结点是不够的,因为此后很可能还需要对树进行重新着色。如果删除的是红色结点,那么没有关系,因为这不会影响树的黑色高度;而如果删除的是黑色结点,事情就没那么简单了。需要把受到影响(移动或交换)的结点标记为黑色,如果它原来已经是黑色的,那么需要标记为“双黑”(双黑,或double-black是许多英文资料中提及的一个概念。简单地说,标记为“双黑”意味着需要对周围的红色结点进行“抹黑 ”处理)。
包含“双黑”结点显然不符合红黑树的要求,因此必须消除这种情况。出现“双黑”的情况可以分为4种:
1、双黑结点的兄弟结点是红色的
2、双黑结点的兄弟是黑色的,并且它的兄弟有两个黑色的子结点
3、双黑结点的兄弟是黑色的,并且,它的兄弟的左、右子结点分别是红色和黑色
4、双黑结点的兄弟是黑色的,并且,它的兄弟的右子结点是红色的
很显然,上述四种情况包括了可能的所有状况。
处理双黑结点的基本思想是进行“色彩补偿”。换言之,将邻近的红色结点变为黑色,同时,此双黑结点也“还原”为黑色。
总结
红黑树引入了“颜色”的概念。引入“颜色”的目的在于使得红黑树的平衡条件得以简化。正如著名的密码学专家Bruce Schneier所说的那样,“Being Partly balanced can be good enough”,红黑树并不追求“完全平衡”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。
红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。
当然,红黑树并不适应所有应用树的领域。如果数据基本上是静态的,那么让他们待在他们能够插入,并且不影响平衡的地方会具有更好的性能。如果数据完全是静态的,例如,做一个哈希表,性能可能会更好一些。
在实际的系统中,例如,需要使用动态规则的防火墙系统,使用红黑树而不是散列表被实践证明具有更好的伸缩性。