【C/C++】胜者树与败者树:多路归并排序的利器
文章目录
- 胜者树与败者树:多路归并排序的利器
- 1 胜者树简介
- 1.1 定义
- 1.2 胜者树结构与原理
- 1.2.1 构造流程
- 1.2.2 归并过程
- 2 败者树简介
- 2.1 背景场景
- 2.2 基本定义
- 2.3 败者树结构和原理
- 2.3.1 树的构造(初始建树)
- 2.3.2 查询和更新
- 3 胜者树 vs 败者树
- 4 胜者树示例代码
- 5 败者树代码示例
- 6 总结
胜者树与败者树:多路归并排序的利器
“胜者树”(Winner Tree)和“败者树”(Loser Tree)都是完全二叉树结构,前者广泛用于多路归并排序和优先级选择问题中,尤其是在归并排序器、堆式选择算法中有重要应用,后者常用于多路归并排序中(例如外部排序),是**胜者树(Winner Tree)**的一种变体。
1 胜者树简介
1.1 定义
- 胜者树是一棵完全二叉树,每个叶子结点表示一个选手(或者一个序列当前值),
- 每个非叶子节点存储当前两名子选手的胜者(较小者)索引。
- 根节点最终存储的是所有选手中最小值的索引(即冠军/胜者)。
胜者树常用于:
- ✅ 多路归并排序
- ✅ 外部排序(归并多个文件)
- ✅ 堆选择算法(优先队列)
- ✅ 归并K个有序序列/流式合并
1.2 胜者树结构与原理
1.2.1 构造流程
假设我们要从 k
个已排好序的序列中进行归并,每个序列的当前元素是一个“选手”:
- 叶子结点存放
k
个选手的索引(或值)。 - 相邻两个选手进行比较,较小者(胜者)进入上层节点。
- 一直递归比较,直到根节点,表示最终胜者。
- 内部节点都记录每次比赛的胜者索引。
1.2.2 归并过程
每轮输出根节点对应的值(最小值),然后用该“选手”所在的序列下一项替代,并自底向上更新路径即可,时间复杂度为 O(log k)
。
2 败者树简介
2.1 背景场景
假设你有多个已经排好序的数组(或文件),想把它们合并成一个大的有序序列。这就叫多路归并。当有很多路时(比如上百个文件),直接线性比较效率很低,败者树可以高效解决这个问题。
2.2 基本定义
- 败者树是一棵完全二叉树,用于记录多路归并中每轮比较的失败者(较大者)。
- 树的内部结点记录的是败者,而不是胜者。
- 整棵树的根节点并不表示最小值,而是指向在当前轮比赛中被打败的选手。
败者树常用于:
- 外部排序中的多路归并排序
- 优先级选择器(类似小根堆)
- K 路归并排序器(如归并 16 个文件,找到最小项)
2.3 败者树结构和原理
假设有 k
个已排好序的输入序列,败者树的构建步骤如下:
2.3.1 树的构造(初始建树)
- 将
k
个选手(对应于每个输入序列的第一个元素)作为叶子节点。 - 比较相邻两个选手,将较大的(败者)向上传递,较小的(胜者)继续往上走。
- 最后胜者会落在“胜者路径”上,沿路的结点记录被其打败的对手(即败者)。
- 树的根并不是最终胜者,而是最后一次比较的败者。
2.3.2 查询和更新
- 每次输出最小元素(胜者)。
- 然后将该序列推进一个新元素,再从该叶子节点重新进行比较并更新路径,时间复杂度是
O(log k)
。
3 胜者树 vs 败者树
特性 | 胜者树 | 败者树 |
---|---|---|
内部节点记录 | 胜者(最小) | 败者(较大) |
根节点 | 胜者(最小值) | 败者 |
插入/更新效率 | O(log k) | O(log k) |
查询最小值 | 直接查根 | 查 tree[0] 对应叶子 |
代码逻辑复杂度 | 相对较简单 | 相对复杂 |
实际使用 | 优先队列、归并排序等 | 多路归并排序优化 |
4 胜者树示例代码
#include <iostream>
#include <vector>
#include <climits>
#include <cassert>class WinnerTree {
private:int k;std::vector<int> tree; // 完全二叉树结构,1-based,tree[1] 是根,tree[0] 存储最终 winner 索引std::vector<int> leaves; // 叶子数组,存储实际数据public:WinnerTree(const std::vector<int>& init) : k(init.size()), leaves(init) {// 树大小需要 2*k(包含叶子和内部节点)tree.resize(2 * k, -1);build();}void build() {// 初始化叶子节点(从 tree[k] 到 tree[2k - 1])for (int i = 0; i < k; ++i) {tree[k + i] = i;}// 从底向上构建 winner treefor (int i = k - 1; i > 0; --i) {int left = tree[i * 2];int right = tree[i * 2 + 1];if (right == -1 || (left != -1 && leaves[left] <= leaves[right])) {tree[i] = left;} else {tree[i] = right;}}tree[0] = tree[1]; // winner 存到 tree[0]}void adjust(int leafIdx) {int pos = k + leafIdx;while (pos > 1) {int sibling = (pos % 2 == 0) ? pos + 1 : pos - 1;int parent = pos / 2;int left = tree[parent * 2];int right = (parent * 2 + 1 < (int)tree.size()) ? tree[parent * 2 + 1] : -1;if (right == -1 || (left != -1 && leaves[left] <= leaves[right])) {tree[parent] = left;} else {tree[parent] = right;}pos = parent;}tree[0] = tree[1];}int winner() const {return tree[0];}int value(int i) const {return leaves[i];}void popAndReplace(int idx, int newValue) {leaves[idx] = newValue;adjust(idx);}
};int main() {std::vector<int> init = {3, 6, 1, 9};WinnerTree wt(init);for (int i = 0; i < 4; ++i) {int win = wt.winner();std::cout << wt.value(win) << " ";wt.popAndReplace(win, INT_MAX); // 替换为无穷大,模拟归并过程}return 0;
}
输出结果:
1 3 6 9
5 败者树代码示例
#include <iostream>
#include <vector>
#include <climits>class LoserTree {
private:int k;std::vector<int> tree; // 内部节点,size k,tree[0]为胜者叶子编号std::vector<int> leaves; // 叶子值,size kpublic:explicit LoserTree(const std::vector<int>& input) : k((int)input.size()), tree(k, -1), leaves(input) {build();}void build() {// 初始化所有内部节点为-1for (int i = 0; i < k; ++i) tree[i] = -1;for (int i = k - 1; i >= 0; --i) {adjust(i);}}void adjust(int s) {int t = s;int parent = (s + k) / 2;while (parent > 0) {if (parent >= k) {std::cerr << "ERROR: parent index out of range: " << parent << std::endl;std::cerr << "k = " << k << std::endl;std::exit(1);}// debug输出当前比较的节点和值// std::cout << "Adjust: parent = " << parent << ", t = " << t// << ", leaves[t] = " << leaves[t]// << ", tree[parent] = " << tree[parent]// << ", leaves[tree[parent]] = " << (tree[parent] == -1 ? INT_MAX : leaves[tree[parent]])// << std::endl;if (tree[parent] == -1 || leaves[t] < leaves[tree[parent]]) {std::swap(t, tree[parent]);}parent /= 2;}tree[0] = t;}int winner() const { return tree[0]; }int value(int idx) const {if (idx < 0 || idx >= k) return INT_MAX;return leaves[idx];}void popAndReplace(int idx, int newVal) {if (idx < 0 || idx >= k) {std::cerr << "popAndReplace: idx out of range: " << idx << std::endl;return;}leaves[idx] = newVal;adjust(idx);}
};int main() {std::vector<int> input = {20, 15, 30, 10};LoserTree lt(input);std::cout << "Winner index: " << lt.winner() << std::endl;std::cout << "Winner value: " << lt.value(lt.winner()) << std::endl;lt.popAndReplace(lt.winner(), 25);std::cout << "After replacement:" << std::endl;std::cout << "Winner index: " << lt.winner() << std::endl;std::cout << "Winner value: " << lt.value(lt.winner()) << std::endl;return 0;
}
6 总结
胜者树:
优点 | 描述 |
---|---|
✅ 高效 | 查询最小值时间复杂度 O(1),更新 O(log k) |
✅ 结构清晰 | 比堆更适合用于 K 路归并选择 |
✅ 实用性强 | 外排序、文件合并、流式排序等场景常见 |
败者树:
- 败者树是一种适合多路归并排序的高效数据结构。
- 每次找最小值和更新的操作是
O(log k)
。 - 通常用于数据量过大、需要外部存储的场景(如磁盘文件排序)。
- 实现比堆略复杂,但效率在多路归并时更优。