排序算法之:比较排序算法中基于“锦标赛淘汰制”(Tournament Elimination)的“胜者树”(Winner Tree)与“败者树”(Loser Tree)的对比、构造与应用
题目描述
在多路归并(K-Way Merge)与基于比较的排序算法中,经常需要从多个候选元素中快速选取最小(或最大)值。传统的逐个比较方法,在重复选取时效率较低。“胜者树”(Winner Tree)和“败者树”(Loser Tree)是两种基于锦标赛淘汰制的高效数据结构,用于维护一个动态集合的最小值(或最大值),并在元素被取出后,用新元素替换并快速更新树结构。
本题将深入讲解:
- 胜者树与败者树的构造原理与差异。
- 如何用胜者树/败者树实现多路归并排序。
- 两种树在初始化、更新操作上的时间复杂度与空间复杂度对比。
- 胜者树与败者树各自的适用场景。
解题过程讲解
第一步:理解基本结构
假设有 k 个元素,分别来自 k 个已排序的序列(在多路归并中)。我们需要反复取出当前 k 个元素中的最小值,然后从取出元素对应的序列中补充下一个元素,再重新选出最小值。
- 胜者树:一棵完全二叉树,叶子节点存放当前 k 个待比较元素,内部节点存放子节点比较的“胜者”(即较小的元素)。根节点就是当前全局最小值。
- 败者树:与胜者树类似,但内部节点存放的是子节点比较的“败者”(即较大的元素),而胜者向上传递继续比较。根节点之上有一个虚拟节点存放最终胜者。
关键差异:在更新树时,胜者树需要知道胜者路径上的兄弟节点进行比较;败者树在内部节点已记录了败者,因此更新时只需要与父节点记录的败者比较,可以减少比较次数,提高缓存友好性。
第二步:胜者树的构造与更新
-
初始化:
- 将 k 个元素放在叶子节点。
- 自底向上,每个内部节点比较两个子节点的值,将较小者(胜者)复制到该内部节点。
- 直到根节点得到全局最小值。
-
取出最小值后更新:
- 从被取出的元素所在的叶子节点,用其所属序列的下一个元素替换(若无,用无穷大标记)。
- 从该叶子节点开始,向上遍历到根:在当前节点,与兄弟节点比较,胜者复制到父节点。
- 重复直到根更新。
-
时间复杂度:
- 建树:O(k)。
- 每次更新(取最小+重构):O(log k)。
第三步:败者树的构造与更新
-
初始化:
- 叶子节点存放 k 个元素。
- 内部节点初始化为一个特殊值(例如 -∞),表示“尚无败者”。
- 额外设置一个全局变量
winner[0]存放最终胜者。 - 自底向上,对于每个内部节点,比较两个子树上来的胜者,将败者记录在该内部节点,胜者继续向上比较。
- 最终,根节点之上的
winner[0]是全局最小元素。
-
更新操作:
- 替换叶子节点元素后,从该叶子节点向上,与父节点记录的败者比较:
- 如果新值比父节点记录的败者小,则新值成为胜者继续向上,原败者留在父节点。
- 否则,新值成为败者留在父节点,原胜者继续向上。
- 直到根节点,更新
winner[0]。
- 替换叶子节点元素后,从该叶子节点向上,与父节点记录的败者比较:
-
优势:
- 更新时,每次只需要和父节点存储的败者比较一次,而不需访问兄弟节点(兄弟节点可能不在缓存中),因此常数因子更优。
- 在多路归并中,败者树通常比胜者树更快。
第四步:在多路归并排序中的应用
- 将 k 个有序序列的第一个元素放入胜者树/败者树的叶子节点。
- 取出根节点(最小值)放入输出序列。
- 从该最小值所属序列取下一个元素替换叶子节点,更新树。
- 重复直到所有序列为空。
时间复杂度:合并 n 个元素的总代价为 O(n log k),其中 k 是归并路数。
第五步:对比总结
| 维度 | 胜者树 | 败者树 |
|---|---|---|
| 内部节点存储 | 胜者(较小者) | 败者(较大者) |
| 更新比较对象 | 必须访问兄弟节点 | 只需与父节点存储的败者比较 |
| 缓存友好性 | 较差(需访问兄弟节点) | 较好(数据局部性高) |
| 代码复杂度 | 较简单直观 | 稍复杂,但性能更优 |
| 常见应用 | 教学示例、简单多路归并 | 实际外部排序库(如STL) |
第六步:实例演示(败者树)
假设有 4 个有序序列:
S1: [3, 9]
S2: [2, 5]
S3: [7, 8]
S4: [1, 6]
-
初始化叶子:叶子节点 L1=3, L2=2, L3=7, L4=1。建败者树:
- 先比较 3 和 2,胜者 2 向上,败者 3 记录在父节点。
- 比较 7 和 1,胜者 1 向上,败者 7 记录在父节点。
- 比较胜者 2 和 1,胜者 1 成为全局胜者(输出),败者 2 记录在根节点。
- 最终树结构:内部节点记败者,全局胜者 winner=1。
-
输出 1 后,从 S4 取下一个元素 6 替换 L4=1。更新路径:
- 比较 6 和父节点记录的败者 7:6 更小,所以 6 成为新胜者向上,7 保留在父节点。
- 向上比较 6 和根节点记录的败者 2:6 更大,所以 6 成为新败者留在根节点,2 成为新胜者成为全局胜者。
- 输出 2,然后从 S2 取 5 替换 L2=2,重复更新。
如此反复,直到所有序列合并完成。