首页/数据结构/07-sorting/外部排序的败者树 🔗 在 Obsidian 中打开
数据结构 · 07-sorting

外部排序的败者树

难度 ★★★★重要度 ★★ 考查频率 低题型 综合应用 数据结构/排序外部排序败者树
速查
败者树是一棵完全二叉树,用于在 k 路归并中快速选出最小值。朴素方法需 $k-1$ 次比较,败者树只需 $\lceil\log_2 k\rceil$ 次。调整时只沿路径向上比较,比胜者树更高效。

速查

项目
主题外部排序的败者树
核心概念多路归并中,用败者树在 k 个段当前元素里快速选最小值
时间复杂度$O(\log_2 k)$
稳定性稳定
难度⭐⭐⭐⭐

核心概念

外部排序的背景

当数据量太大无法全部装入内存时,需要使用外部排序。最常用的外部排序方法是多路归并排序

基本过程

  1. 生成初始归并段:将数据分成若干段,每段在内存中排序后写回外存
  2. 多路归并:将 k 个归并段归并成一个有序文件

败者树的作用

在 k 路归并中,需要从 k 个归并段的当前元素中选出最小值

  • 朴素方法:比较 $k-1$ 次 → 时间太长
  • 败者树:只需要 $\lceil\log_2 k\rceil$ 次比较

败者树是一棵完全二叉树,用于在 k 个元素中快速找到最小值。

败者树 vs 胜者树

  • 胜者树:每个节点记录胜者(较小值的下标)
  • 败者树:每个节点记录败者(较大值的下标),根节点的父节点记录最终胜者
为什么用败者树?胜者树在更新时需要重新比较兄弟节点,而败者树只需要沿路径向上比较,更高效

关键性质

败者树的结构

对于 k 路归并,败者树有:

  • k 个叶子节点:对应 k 个归并段的当前元素
  • k-1 个内部节点:记录每轮比较的败者
  • 1 个额外节点(根的父节点):记录最终胜者

总共 2k 个节点(或 $k + (k-1) + 1 = 2k$)。

败者树的构造

  1. 初始化所有内部节点为虚值(如 0,表示"虚拟段",其值为无穷大)
  2. 依次将 k 个叶子节点加入,自下而上调整

败者树的调整(Adjust)

当一个叶子节点的值被更新后(即该归并段读入下一个元素),需要:

  1. 从该叶子节点出发,沿路径向上
  2. 将当前节点与父节点记录的败者比较
  3. 胜者继续向上,败者记录在父节点中
  4. 直到根节点

时间复杂度:$O(\log_2 k)$

手算示例

示例一:构造 4 路归并的败者树

题目:4 个归并段的当前元素为:段1→3,段2→5,段3→2,段4→7。

叶子节点(从左到右):段1=3,段2=5,段3=2,段4=7。

自下而上调整

  • 节点1 = 比较段1(3) 和段2(5):败者=段2(5),胜者=段1(3)
  • 节点3 = 比较段3(2) 和段4(7):败者=段4(7),胜者=段3(2)
  • 节点2 = 比较段1(3) 和段3(2):败者=段1(3),胜者=段3(2)
  • 节点0 = 最终胜者 = 段3(2)
败者树:
       [0] → 段3 (最终胜者, 值=2)
       /
     [2] → 败者=段1 (值=3)
    /   \
  [1]    [3]
败者=段2  败者=段4
 (值=5)   (值=7)
 / \     / \
段1 段2  段3 段4
 3   5   2   7

示例二:调整败者树

题目:接上例,输出段3的元素 2 后,段3读入下一个元素 8,调整败者树。

  1. 段3的值从 2 变为 8
  2. 从叶子节点(段3)出发向上,父节点是 3,败者是段4(7)
  3. 比较段3(8) 和段4(7):败者=段3(8),胜者=段4(7)
  4. 节点3更新为段3(败者)
  5. 继续向上到节点2,败者是段1(3)
  6. 比较段4(7) 和段1(3):败者=段4(7),胜者=段1(3)
  7. 节点2更新为段4(败者)
  8. 最终胜者=段1(3)

输出:3(来自段1)。比较次数:2 次 $= \log_2 4 = 2$。

示例三:k 路归并的比较次数

题目:8 路归并,用败者树选出一个最小元素需要多少次比较?

:$\lceil\log_2 8\rceil =$ 3 次,而朴素方法需要 7 次($8-1$)。

常见考法

题型一:构造败者树给定 k 个归并段的当前元素,构造败者树。
题型二:调整败者树输出最小元素后,调整败者树并求新的最小元素。
题型三:计算比较次数k 路归并中,用败者树的比较次数是多少。
题型四:与朴素方法对比用败者树比朴素方法少多少次比较。

易错点

注意
  1. 败者树根节点的父节点才是胜者:根节点记录的是最后一次比较的败者。
  2. 调整时只需比较 $\log_2 k$ 次:沿路径向上,不需要重新建树。
  3. k 路归并的败者树需要 $k + (k-1)$ 个节点:k 个叶子 + k-1 个内部节点。
  4. 败者树不是排序算法:它是选出最小值的辅助数据结构。
  5. 外部排序的总时间 = 生成归并段 + 多路归并。

核心结论

概念公式 / 值
k 路归并选出最小值(败者树)$\lceil\log_2 k\rceil$ 次比较
k 路归并选出最小值(朴素)$k-1$ 次比较
败者树节点数$2k-1$(k 叶 + k-1 内部)
调整时间复杂度$O(\log_2 k)$
败者树优势更新时只沿路径向上比较

记忆卡片

k 路归并中选最小值要几次比较?
$\lceil\log_2 k\rceil$ 次,比朴素 $k-1$ 次少得多。
败者树的根节点记录的是什么?
根节点记录最后一轮的败者,根的父节点记录最终胜者(不同教材表述略异,但都能正确选出最小值)。
为什么败者树比胜者树更适合外部排序?
败者树更新时只需沿路径向上比较,不需要重新比较兄弟节点,调整更高效。
8 路归并选最小值要几次?
3 次($\lceil\log_2 8\rceil=3$),朴素方法需 7 次。
败者树是排序算法吗?
不是。它是辅助数据结构,用于在 k 个元素中快速选出最小值,服务于多路归并。
败者树有多少节点?
$2k-1$ 个:k 个叶子(段当前值)+ k-1 个内部节点(败者)。

交互动画 · 4 路归并败者树构建与调整

胜者 — 3 5 2 7 ksize=4 · 败者树:内部结点存败者,根之上另存胜者(最小值)
点击「播放」或「下一步」:先构建 4 路败者树,再演示段3 输出后读入 8 的调整
比较次数 = 树高 = ⌈log₂k⌉,比每次全量比 k−1 次更快

相关知识点

(暂无关联知识点)