拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

055胜者树

055胜者树

胜者树/败者树(Tournament Tree)— 外排序的核心引擎

055胜者树:从体育锦标赛到大数据引擎

5W1H 发明者故事

Who(何人)- 发明者是谁?

发明者:竞标赛排序(Tournament Sort)的思想来源于多人,但将其系统化为数据结构并应用于外排序的是 Donald E. Knuth,在 TAOCP 第三卷第 5.4.1 节中给出了完整的理论分析。
历史渊源:

  • 体育竞标赛的思想(单淘汰赛决出冠军)在人类文明中有数千年历史
  • 将竞标赛思想用于排序由 Knuth 在 1973 年出版的 TAOCP 第三卷中系统阐述
  • 败者树(Loser Tree)作为胜者树的变体,由 Knuth 在同一节中分析,实现上更高效
  • IBM 的工程师在 1950 年代开发磁带排序时已在实践中使用类似思想

When(何时)- 什么时候发明的?

时间:作为数据结构被系统记录于 1973 年 TAOCP 第三卷出版
时代背景:

  • 1950-70 年代,计算机内存极为有限(KB 量级),处理大文件必须依赖磁带/磁盘
  • 外排序(External Sorting)是这一时期最重要的实际计算问题之一
  • IBM 701、IBM 7090 等主机的磁带排序性能直接影响商业价值
  • K 路归并(K-way merge)是外排序的核心,而高效选择最小元素是 K 路归并的瓶颈

Where(何地)- 在哪里发明的?

地点:理论系统化于斯坦福大学(Knuth 的工作地),实践源于 IBM 研究中心
环境:

  • IBM 主导着 1950-60 年代的商业计算,对排序效率有极大的实际需求
  • 斯坦福大学的 TAOCP 项目将这些工程实践提升为严格的数学理论
  • Knuth 在写作 TAOCP 时大量参考了 IBM 的技术报告和实际系统设计

What(何事)- 发明了什么?

数据结构:胜者树(Winner Tree)/ 败者树(Loser Tree)

胜者树结构:

  • 完全二叉树,叶节点为参赛选手(待归并序列的当前元素)
  • 内部节点记录其两个子节点中的"胜者"(最小值的下标)
  • 根节点记录全局冠军(所有叶节点中的最小值)
0 ← 根:记录冠军下标(叶节点0最小) / \ 0 2 ← 内部节点:记录各子树中的胜者下标 / \ / \ 0 1 2 3 ← 叶节点:选手下标 [2][5][3][8] ← 选手值

关键操作 replay(重赛):

  • 当冠军被取出后,该叶节点更新为新值
  • 只需沿该叶到根的路径重新比较,O(log K) 时间完成
  • 其他 K-1 条路径不需要重新比较(关键优化!)

Why(何因)- 为什么发明?

要解决的问题:

  1. K 路归并时,每次选择 K 个序列的最小元素,朴素比较需要 K-1 次比较
  2. 在外排序中,K 可能很大(几十到几百路),每次 K-1 次比较代价太高
  3. 需要一种数据结构,在更新一个元素后能以 O(log K) 时间重新找到最小元素

理论依据:

  • 胜者树将 K 路选择从 O(K) 降至 O(log K)(每次 replay 只比较 log K 次)
  • N 个元素 K 路归并总比较次数:N·log K(而非朴素的 N·K)
  • 败者树进一步减少了比较中的数据移动(内部节点记录败者而非胜者)

当时的挑战:

  • 证明完全二叉树结构能够正确维护冠军
  • 设计 replay 操作使其只沿一条路径更新
  • 处理边界情况(选手数不是 2 的幂、某个序列耗尽)

How(何果)- 如何实现?有什么影响?

K 路归并外排序流程:

1. 初始化:从 K 个有序子序列各取第一个元素作为叶节点 2. 建树:自底向上,每个内部节点取子节点中较小者的下标 3. 循环: a. 输出根所指叶节点的值(冠军) b. 从该冠军所在序列读入下一个元素(若序列耗尽则设为 +∞) c. 执行 replay:从该叶向上重新比较,更新路径上各内部节点 d. 直到所有序列耗尽

性能对比:

方法每次选择代价N 元素 K 路归并总代价
线性扫描O(K)O(N·K)
胜者树O(log K)O(N·log K)
败者树O(log K),常数更小O(N·log K)

历史影响:

  • 外排序至今仍是数据库和大数据系统的核心操作
  • MySQL、PostgreSQL 的外部排序均使用类似的多路归并思想
  • Hadoop MapReduce 的 shuffle/merge 阶段使用败者树
  • Apache Spark 的排序算子也基于类似原理
  • TAOCP 中的败者树分析是算法工程化的经典案例

今天的使用:

  • 数据库外排序(ORDER BY 大表时)
  • 大数据框架(Hadoop、Spark)的 K 路归并
  • 流处理系统的多源有序流合并
  • 磁盘 B 树的顺序扫描优化

自然语言需求定义

需求名称:实现胜者树,支持初始化、查询冠军、更新叶节点后重赛,并模拟 K 路归并的外排序场景

功能需求(用精确的中文描述)

  1. 初始化(build_winner_tree):根据叶节点初始值构建胜者树

    • 输入:叶节点值数组、叶节点数量 K
    • 操作:自底向上,每个内部节点取子节点中较小值的下标
    • 输出:无(就地填充 winner 数组)
  2. 查询冠军(get_winner):返回当前最小值

    • 输入:胜者树结构体指针
    • 操作:返回根节点所指叶节点的值
    • 输出:最小值;若树为空返回 INT_MAX
  3. 更新并重赛(replay):更新某个叶节点的值后重新竞争

    • 输入:胜者树结构体指针、叶节点下标、新值
    • 操作:更新叶节点值,从该叶节点向上逐层重新比较,更新路径上各内部节点
    • 输出:无(就地更新 winner 数组)
  4. K 路归并模拟:模拟将 K 个有序序列合并为一个有序序列

    • 输入:K 个有序子数组及其长度
    • 操作:建树 → 循环取冠军 → 更新对应序列的下一个元素 → replay
    • 输出:填充合并后的有序数组

约束条件

  • 胜者树为完全二叉树,叶节点个数 K 必须为 2 的幂(或需处理非 2 的幂情况)
  • 内部节点数组下标:根为下标 0 或 1(根据实现选择,需注释说明)
  • 叶节点下标 k 的父节点下标为 (k + K - 1) / 2(0-based)或类似公式
  • 当序列耗尽时,将对应叶节点设为 INT_MAX(哨兵值)
  • 实现最小胜者树(最小值为冠军)

验收标准表格

编号测试场景(自然语言描述)预期结果验证方式
18叶节点值 [2,5,3,8,1,7,4,6],查询冠军1(最小值)断言等于 1
2取出冠军后,将叶5(值=1)更新为 10,重赛后冠军2断言等于 2
3继续取出冠军并更新(模拟序列耗尽用 INT_MAX)冠军依次递增断言有序
48叶节点值 [8,7,6,5,4,3,2,1],查询冠军1断言等于 1
54路归并:[1,5,9]、[2,6,10]、[3,7,11]、[4,8,12]有序序列 1~12断言数组各元素
64路归并:长度不等的序列 [1,3]、[2,4,6,8]、[5]、[7,9]有序序列 1~9断言数组各元素
7单叶节点胜者树(K=1),冠军为该叶值叶节点值断言
8所有叶节点值相同(均为 5),冠军为 55断言等于 5

C语言实现文件

对应文件:winner_tree.c

编译运行:

gcc-std=c99-Wall-owinner_tree_test winner_tree.c ./winner_tree_test

核心函数:

  • build_winner_tree(wt, leaves, k)— 初始化胜者树
  • get_winner(wt)— 返回当前冠军值
  • replay(wt, leaf_idx, new_val)— 更新叶节点并重赛
  • kway_merge(seqs, lens, k, output, out_size)— 模拟 K 路归并
  • winner_tree_free(wt)— 释放内存
返回列表