在各种体育竞赛中我们常常看见如下所示的树形结构,选手们两两进行比赛,胜者晋级,晋级的选手继续两两比赛,继续晋级,直到决出冠军。
事实上,这种思想对于排序很有帮助,我们将基于这种思想建立的树称为胜者树。
胜者树
建树 and 获取初代冠军
选手们两两进行比赛,数值大者晋级,然后晋级的选手之间继续两两进行比赛。
获取新的冠军(重构)
将上一届冠军取出,然后添加一个新的选手,则举出新的冠军仅需要从新的选手开始向上传导,共进行
l
o
g
2
n
log_2n
log2n次比较就能决出新的冠军。
胜者树有几个特点
- 是一颗完全二叉树
- 非叶子节点记录胜者的标号
- 建立一棵胜者树的时间代价为 O ( n ) O(n) O(n)
- 从建立好的胜者树获取最值的时间代价为 O ( l o g 2 n ) O(log_2n) O(log2n)
- 胜者树的建立和重构涉及到兄弟节点之间的比较
应用——树形选择排序
对于一个包含
n
n
n 个元素的待排序序列,如果使用选择排序对其进行从小到大排序,则需要进行
n
−
1
n-1
n−1轮排序,而且第
i
i
i 轮排序需要进行
n
−
i
n-i
n−i 次比较才能选出当前最小值,最终导致选择排序拥有
O
(
n
2
)
O(n^2)
O(n2)的时间代价。
将胜者树用在排序过程中,能够减少排序的时间代价。将比赛的选手换成待排序的元素,将选手之间的比较换做元素之间的比较,如果规定数值较小者胜出,则经过 O ( n ) O(n) O(n)的时间代价后,可以在树的根节点得到待排序序列的最小值。将最小值取出(最小值位置用无限大填充),然后经过 l o g 2 n log_2n log2n 次比较就可决出次小值。如此循环下去,最终只需要 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) 的时间代价就可完成排序。
败者树
败者树与胜者树很类似,通过比赛使胜者晋级,直到决出冠军。但是败者树的非叶子节点记录的是败者的标号,且需要增加一个节点记录比赛的胜者。
与胜者树不同,我们先来讨论如何从一个已经建立好的败者树中获得新的冠军。
获取新的冠军(重构)
新的选手需要和父节点里的选手比赛,败者被存入父节点,而胜者与上一级父节点里的选手继续比赛,如此迭代,直到决出新的冠军。
败者树在重构的过程中只与父节点进行比较而不涉及兄弟节点之间的比较。
建树 and 获取初代冠军
首先将败者树的非叶子节点使用某一极大值的标号初始化。然后,对每一个选手按照“获得新的冠军”中的步骤向上传导,当最后一个选手传导完毕后,败者树就建成了。
败者树有几个特点
- 是一颗完全二叉树
- 根节点和它的子节点记录败者的标号
- 建立一棵败者树的时间代价为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
- 从建立好的胜者树获取最值的时间代价为 O ( l o g 2 n ) O(log_2n) O(log2n)
- 败者树的建立和重构仅仅涉及到父子节点之间的比较
应用——多路平衡归并
多路平衡归并是一种外排序算法,在待排序元素过多以至于内存放不下的情况下,就会用到外排序算法。所以多路平衡归并不但涉及到内存读写还涉及到外存读写。在这里我们忽略外排序的细节,仅讨论败者树在多路平衡归并中的作用。
我们都熟悉 2 2 2 路归并,在每一轮归并中,仅需一次比较就可以得到一个归并后的结果。但是 k k k 路归并不同,必须经历 k − 1 k-1 k−1 次比较,才能得到一个归并后的结果,这无疑增加了排序的代价。
因此考虑在归并的过程中引入败者树,刨去构建树时 O ( k l o g 2 k ) O(klog_2k) O(klog2k)的时间代价,每次仅需要 l o g 2 k log_2k log2k 次比较就能得到一个归并后的结果。
l o g 2 k log_2k log2k 与 k − 1 k-1 k−1 相差很多吗?假设 k = 5 k=5 k=5,则 l o g 2 k = 2.321928 log_2k=2.321928 log2k=2.321928,而 k − 1 = 4 k-1=4 k−1=4,它们比较次数几乎相差一倍。
参考文献
《数据结构》(严蔚敏)
胜者树与败者树