锦标赛排序(Tournament Sort)的败者树(Loser Tree)优化策略
字数 5209 2025-12-08 21:40:55

锦标赛排序(Tournament Sort)的败者树(Loser Tree)优化策略

题目描述
锦标赛排序是一种基于“锦标赛”(或称为“树形选择”)的排序算法,它通过模拟体育比赛中的淘汰赛机制,反复从待排元素中选出最小(或最大)值,从而得到一个有序序列。其基本实现需要O(n)的额外空间,并且每次选择最小值的比较次数为O(log n)。然而,在其经典实现中,每选出一个冠军(最小值)后,需要从该冠军所在的叶子节点到根节点重新进行一轮比赛(比较),这仍然会进行一些冗余比较。

败者树 是锦标赛排序的一种经典优化数据结构。在败者树中,每个内部节点记录的是在该子树的比赛中“失败者”(即较大的值),而“胜利者”(较小的值)则向上传递继续比赛。这样,当树根的冠军被输出后,我们只需要从该冠军所在的叶子节点引入新的参赛者(即该叶子节点的下一个值,在外部排序中通常是下一个数据块),然后沿着该叶子到根的路径重新进行比赛即可,并且每次比赛只需要比较上一轮的失败者和新来的参赛者,大大减少了比较次数。

本题要求:详细讲解锦标赛排序的基本原理,然后重点讲解如何使用败者树对其进行优化。需要清晰地说明败者树的构建过程、调整(重构)过程,并分析其时间复杂度和空间复杂度,最后通过一个具体的例子来演示整个过程。


解题过程循序渐进讲解

1. 回顾锦标赛排序的基本思想
首先,我们把待排序的n个元素看作n个参赛者。锦标赛排序模拟淘汰赛:

  • 第一轮:所有参赛者两两比赛(比较),较小的值获胜进入下一轮。如果n是奇数,可能有一个轮空。
  • 每一轮都这样两两比较,直到最终决出一个冠军,这个冠军就是当前最小的元素。
  • 输出冠军,然后将冠军所在位置“标记为空”(或者用一个大值如∞替代),并从该位置开始,重新进行从该叶子到根路径上的比赛,选出下一个冠军(即第二小的元素)。
  • 重复上述过程,直到所有元素输出完毕。

基本实现的问题:每输出一个冠军后,需要从该叶子到根重新比较,但在路径上的每个内部节点,都需要重新比较它的两个子节点(即使其中一个子节点没有变化)。例如,在第一轮中,我们已经知道某个子树中的次小值(即决赛中输给冠军的那个值),但基本实现中可能没有记录它,导致后续重复比较。

2. 引入败者树
败者树是一棵完全二叉树(通常是满二叉树),它能够记录每场比赛的失败者,从而避免冗余比较。

  • 叶子节点:存储实际的参赛元素(数据)。
  • 内部节点:不存储数据,而是存储一个“指针”或“索引”,指向在该子树比赛中失败的那个参赛者(即记录失败者,而不是胜利者)。
  • 此外,我们还需要一个额外的节点(可以单独存储,或者用树的根节点之上的一个虚拟节点)来记录整个比赛的最终胜者(即当前最小元素)。

为什么有效:因为每次调整时,我们只需要将新来的参赛者(比如∞或者下一个值)与从叶子到根路径上记录的各场失败者进行比较,而不需要重新比较两个子树的胜者。

3. 败者树的存储结构
假设有k个参赛者(在外部排序中,k通常是归并路数,这里我们先考虑内部排序,k=n)。为了简单,我们假设k是2的幂(否则可以补充∞使其成为2的幂)。树的高度为h=log₂k。

我们用一个数组loser[0..k-1]来存储内部节点(注意:这个数组大小和叶子数相同,但实际上内部节点数比叶子数少1,这里通常用loser[0]存储整棵树的最终失败者,而loser[1..k-1]存储其他内部节点,但不同教材实现可能不同)。另一种常见的做法是:用一个长度为k的数组tree[0..k-1]表示败者树,其中tree[0]存储最终胜者的索引,tree[1..k-1]存储内部节点的失败者索引。这里我采用后一种更常见的表述:

  • 令叶子节点数为K(即参赛者数量),则内部节点数也为K(因为败者树通常将内部节点数扩展到与叶子数相同,以便于索引计算,多出的一个位置可以存储胜者)。
  • 实际上,标准的败者树使用一个长度为K的数组tree,其中:
    • tree[0]:存储当前整棵树的胜者(最小值的索引)。
    • tree[1]tree[K-1]:存储各个内部节点所记录的失败者索引。
  • 另有一个数组leaves[0..K-1]存储每个叶子节点的实际值(即参赛者的值)。

索引关系:对于内部节点tree[i],其左孩子对应于叶子(或下层内部节点)的索引可以通过2*i2*i+1计算,但在败者树中,我们通常从索引1开始,这样左孩子为2*i,右孩子为2*i+1,与堆的索引方式类似。但注意,败者树的具体实现可能有所不同,我们下面以一个具体例子说明。

4. 构建败者树(初始化)
假设我们有k个初始值leaves[0..k-1]。构建过程如下:

  • 首先,将所有内部节点(tree[1..k-1])初始化为一个最小值(比如-1,表示还没有失败者)。
  • 然后,我们依次将每个叶子节点“插入”到树中,但实际上更高效的做法是从下到上、从左到右初始化。
  • 标准构建方法(类似堆的构建,但比较规则不同):
    1. 从最后一个叶子节点开始,向前处理每个叶子i(i从k-1到0)。
    2. 对于叶子i,将其与它的兄弟节点进行比较,失败者记录在它们的父节点中,胜者继续向上比较,直到根。
    3. 但更常见的是一次性自底向上构建:先将所有叶子两两比较,失败者放入父节点,胜者继续向上比较。

为了避免混淆,我们使用一个更简单的递归描述

  • 从所有叶子节点开始,每个叶子视为一个“胜者”。
  • 从最底层内部节点开始,对于每个内部节点,比较其左右子树的胜者,将较大的(失败者)记录在该内部节点,将较小的(胜者)向上传递。
  • 最终,到达根节点时,根节点记录的是最终失败者,而最终胜者需要额外记录(可以放在tree[0])。

实际操作中,我们通常用一次遍历完成构建:假设我们有k个叶子,内部节点数也是k。我们从最后一个内部节点开始(下标从k-1递减到1),对于每个内部节点i,比较它的左右孩子(即叶子节点或下层胜者),将失败者的索引存入tree[i],将胜者的索引传递到上层。但注意,左右孩子的索引需要根据树的结构计算,常用公式是:左孩子= (i - k/2)*2? 这有点复杂,所以我们换一种更清晰的索引约定

索引约定简化
我们使用数组tree[0..k](长度为k+1),其中tree[1..k]是内部节点(存储失败者索引),tree[0]存储最终胜者索引。叶子节点单独放在数组leaves[0..k-1]中。
对于任何一个内部节点tree[i](i从1到k),其对应的两个子节点(胜者)来自哪里?在败者树中,每个内部节点对应一场比赛,比赛的双方是它的两个子树的胜者。为了简化,我们常常在构建时,从叶子层开始,两两配对,用下标映射。

实际上,更常见的做法是用完全二叉树的数组表示,但只使用内部节点部分。这里我们采用《数据结构》教材中的经典方法:

  • 假设有k个叶子,则败者树有k-1个内部节点,但我们通常使用长度为k的数组ls(loser)存储,其中ls[0]存储最终胜者,ls[1]ls[k-1]存储内部节点的失败者。叶子值存在数组b[0..k-1]中。
  • 对于内部节点ls[i],其左右孩子对应的胜者如何得到?在调整过程中,我们从叶子节点向上走,通过下标计算父节点:parent = (i + k) / 2(当叶子下标从0开始,内部节点下标从1开始时的公式)。这个公式是败者树调整时的关键。

由于细节较繁琐,我们接下来通过例子来具体说明。

5. 败者树的操作步骤
我们以k=5个参赛者为例,值分别为{3, 1, 4, 2, 5}。为了简单,我们补充3个无穷大值(∞),使叶子数变为8(2的幂),但实际操作中也可以处理任意k,只是索引计算稍复杂。这里我们按k=5直接演示,不补充。

步骤1:初始化
设置leaves = [3, 1, 4, 2, 5]k=5
我们需要一个长度至少为k的数组tree(内部节点),这里我们用tree[0..k],其中tree[0]存胜者,tree[1..k]存内部节点失败者(实际只用前k个)。
先将所有内部节点初始化为-1(表示无失败者)。
然后,从最后一个叶子开始,向上调整。但更简单的构建方法是:先假设所有参赛者都是胜者,然后依次让它们“比赛”。

构建过程(手动模拟)
我们将5个叶子视为5个初始胜者。我们建一棵完全二叉树,叶子是0-4。内部节点编号从5开始(如果按堆的存储方式)。但败者树常用另一种方式:叶子下标就是0~k-1,内部节点下标从k开始(偏移量)。我们不陷入下标计算泥沼,而是用图示+文字说明逻辑:

  1. 比较叶子0(值3)和叶子1(值1):1胜,3败。所以它们的父节点记录失败者下标0(值3)。
  2. 比较叶子2(4)和叶子3(2):2胜,4败。父节点记录失败者下标2(值4)。
  3. 比较叶子4(5)和∞(虚拟):5胜,∞败。父节点记录失败者下标4(值5)。
  4. 现在,第一层内部节点有了三个失败者记录。接下来,比较第一层的胜者:第一组胜者是叶子1(值1),第二组胜者是叶子3(值2),第三组胜者是叶子4(值5)。我们比较前两个胜者:1和2,1胜,2败,记录失败者下标3(值2)。比较第三组胜者5与∞(虚拟),5胜。然后比较1和5:1胜,5败,记录失败者下标4(值5)。最终胜者是叶子1(值1),记录在tree[0]

这样,树构建完成。最终胜者是值1。

步骤2:输出冠军并调整
输出当前胜者leaves[1]=1。然后,我们需要从叶子1处引入一个新值(如果是内部排序,通常用∞表示该选手已被淘汰)。这里我们假设是内部排序,所以用∞替换leaves[1]

然后,从叶子1(下标1)开始,向上调整:

  • 叶子1的新值是∞。找到叶子1的父节点(即记录失败者下标0的那个节点)。在该父节点处,原来比赛的双方是下标0(值3)和下标1(旧值1),旧失败者是0(值3),胜者是1(值1)。现在新值是∞,所以让新值∞与旧失败者(下标0,值3)比较:3较小,所以3胜,∞败。因此,这个父节点应该记录失败者下标1(新值∞),并将胜者下标0向上传递。
  • 继续向上,到上一层父节点(即刚才记录失败者下标3的节点)。在这个节点,原来比赛的双方是胜者0(值3)和胜者3(值2),旧失败者是3(值2)。现在新的胜者来自下层的是下标0(值3),它与旧失败者下标3(值2)比较:2较小,所以2胜,3败。因此,这个节点记录失败者下标0(值3),并将胜者下标3(值2)向上传递。
  • 继续向上,到根节点的父节点(即记录失败者下标4的节点)。在这个节点,原来比赛的双方是胜者3(值2)和胜者4(值5),旧失败者是4(值5)。现在新的胜者是下标3(值2),与旧失败者4(值5)比较:2较小,所以2胜,5败。因此,这个节点记录失败者下标4(值5),胜者下标3(值2)向上传递,成为新的最终胜者。

调整结束。新的最终胜者是下标3(值2)。输出2。

重复上述调整过程,直到所有元素输出。

6. 复杂度分析

  • 建树:建树时需要从下到上进行比较,每层比较次数为O(k),总比较次数为O(k)。(实际上约为k-1次比较)
  • 每次调整:从叶子到根路径长度为O(log k),每次调整只需要进行一次比较(新值与旧失败者比较),所以调整一次的复杂度为O(log k)。
  • 总共有n个元素(如果k=n,则是一次性建树然后逐个输出),总比较次数 = 建树O(n) + n次调整 * O(log n) = O(n log n),与堆排序相同。
  • 空间复杂度:需要额外存储败者树内部节点,O(k)空间。

7. 败者树的优势
相对于普通的锦标赛排序,败者树减少了一些冗余比较,因为每次调整只比较新值与路径上的旧失败者,而不需要重新比较兄弟节点。在外部排序的多路归并中,k是归并路数(通常较小且固定),败者树可以在O(log k)时间内从k个归并段中选出最小元素,效率很高。

8. 例子完整演示(简略)
由于篇幅,这里不再一步步写出全部过程。但通过上述步骤,你应该能理解:败者树的调整过程就是每次用新值(或∞)与路径上的失败者比较,胜者继续向上,失败者留在该节点。

总结
败者树是锦标赛排序的高效实现,它通过记录每场比赛的失败者,使得每次替换冠军后,调整路径上的比较次数减少到路径长度,从而优化了比较开销。它在内部排序中可以作为堆排序的替代(但实际不如堆常用),在外部排序的多路归并中则是核心数据结构。掌握败者树的关键在于理解其存储结构、索引计算以及调整过程中胜者与失败者的更新规则。

锦标赛排序(Tournament Sort)的败者树(Loser Tree)优化策略 题目描述 锦标赛排序是一种基于“锦标赛”(或称为“树形选择”)的排序算法,它通过模拟体育比赛中的淘汰赛机制,反复从待排元素中选出最小(或最大)值,从而得到一个有序序列。其基本实现需要O(n)的额外空间,并且每次选择最小值的比较次数为O(log n)。然而,在其经典实现中,每选出一个冠军(最小值)后,需要从该冠军所在的叶子节点到根节点重新进行一轮比赛(比较),这仍然会进行一些冗余比较。 败者树 是锦标赛排序的一种经典优化数据结构。在败者树中,每个内部节点记录的是在该子树的比赛中“失败者”(即较大的值),而“胜利者”(较小的值)则向上传递继续比赛。这样,当树根的冠军被输出后,我们只需要从该冠军所在的叶子节点引入新的参赛者(即该叶子节点的下一个值,在外部排序中通常是下一个数据块),然后沿着该叶子到根的路径重新进行比赛即可,并且每次比赛只需要比较上一轮的失败者和新来的参赛者,大大减少了比较次数。 本题要求:详细讲解锦标赛排序的基本原理,然后重点讲解如何使用败者树对其进行优化。需要清晰地说明败者树的构建过程、调整(重构)过程,并分析其时间复杂度和空间复杂度,最后通过一个具体的例子来演示整个过程。 解题过程循序渐进讲解 1. 回顾锦标赛排序的基本思想 首先,我们把待排序的n个元素看作n个参赛者。锦标赛排序模拟淘汰赛: 第一轮 :所有参赛者两两比赛(比较),较小的值获胜进入下一轮。如果n是奇数,可能有一个轮空。 每一轮都这样两两比较,直到最终决出一个冠军,这个冠军就是当前最小的元素。 输出冠军,然后将冠军所在位置“标记为空”(或者用一个大值如∞替代),并从该位置开始,重新进行从该叶子到根路径上的比赛,选出下一个冠军(即第二小的元素)。 重复上述过程,直到所有元素输出完毕。 基本实现的问题 :每输出一个冠军后,需要从该叶子到根重新比较,但在路径上的每个内部节点,都需要重新比较它的两个子节点(即使其中一个子节点没有变化)。例如,在第一轮中,我们已经知道某个子树中的次小值(即决赛中输给冠军的那个值),但基本实现中可能没有记录它,导致后续重复比较。 2. 引入败者树 败者树是一棵完全二叉树(通常是满二叉树),它能够记录每场比赛的失败者,从而避免冗余比较。 叶子节点 :存储实际的参赛元素(数据)。 内部节点 :不存储数据,而是存储一个“指针”或“索引”,指向在该子树比赛中 失败 的那个参赛者(即记录失败者,而不是胜利者)。 此外,我们还需要一个额外的节点(可以单独存储,或者用树的根节点之上的一个虚拟节点)来记录整个比赛的 最终胜者 (即当前最小元素)。 为什么有效 :因为每次调整时,我们只需要将新来的参赛者(比如∞或者下一个值)与从叶子到根路径上记录的各场失败者进行比较,而不需要重新比较两个子树的胜者。 3. 败者树的存储结构 假设有k个参赛者(在外部排序中,k通常是归并路数,这里我们先考虑内部排序,k=n)。为了简单,我们假设k是2的幂(否则可以补充∞使其成为2的幂)。树的高度为h=log₂k。 我们用一个数组 loser[0..k-1] 来存储内部节点(注意:这个数组大小和叶子数相同,但实际上内部节点数比叶子数少1,这里通常用 loser[0] 存储整棵树的最终失败者,而 loser[1..k-1] 存储其他内部节点,但不同教材实现可能不同)。另一种常见的做法是:用一个长度为k的数组 tree[0..k-1] 表示败者树,其中 tree[0] 存储最终胜者的索引, tree[1..k-1] 存储内部节点的失败者索引。这里我采用后一种更常见的表述: 令叶子节点数为 K (即参赛者数量),则内部节点数也为 K (因为败者树通常将内部节点数扩展到与叶子数相同,以便于索引计算,多出的一个位置可以存储胜者)。 实际上,标准的败者树使用一个长度为 K 的数组 tree ,其中: tree[0] :存储当前整棵树的 胜者 (最小值的索引)。 tree[1] 到 tree[K-1] :存储各个内部节点所记录的 失败者 索引。 另有一个数组 leaves[0..K-1] 存储每个叶子节点的实际值(即参赛者的值)。 索引关系 :对于内部节点 tree[i] ,其左孩子对应于叶子(或下层内部节点)的索引可以通过 2*i 和 2*i+1 计算,但在败者树中,我们通常从索引1开始,这样左孩子为 2*i ,右孩子为 2*i+1 ,与堆的索引方式类似。但注意,败者树的具体实现可能有所不同,我们下面以一个具体例子说明。 4. 构建败者树(初始化) 假设我们有k个初始值 leaves[0..k-1] 。构建过程如下: 首先,将所有内部节点( tree[1..k-1] )初始化为一个最小值(比如-1,表示还没有失败者)。 然后,我们依次将每个叶子节点“插入”到树中,但实际上更高效的做法是从下到上、从左到右初始化。 标准构建方法(类似堆的构建,但比较规则不同): 从最后一个叶子节点开始,向前处理每个叶子 i (i从k-1到0)。 对于叶子 i ,将其与它的兄弟节点进行比较,失败者记录在它们的父节点中,胜者继续向上比较,直到根。 但更常见的是一次性自底向上构建:先将所有叶子两两比较,失败者放入父节点,胜者继续向上比较。 为了避免混淆,我们使用一个更简单的 递归描述 : 从所有叶子节点开始,每个叶子视为一个“胜者”。 从最底层内部节点开始,对于每个内部节点,比较其左右子树的胜者,将较大的(失败者)记录在该内部节点,将较小的(胜者)向上传递。 最终,到达根节点时,根节点记录的是最终失败者,而最终胜者需要额外记录(可以放在 tree[0] )。 实际操作中 ,我们通常用一次遍历完成构建:假设我们有k个叶子,内部节点数也是k。我们从最后一个内部节点开始(下标从k-1递减到1),对于每个内部节点i,比较它的左右孩子(即叶子节点或下层胜者),将失败者的索引存入 tree[i] ,将胜者的索引传递到上层。但注意,左右孩子的索引需要根据树的结构计算,常用公式是:左孩子= (i - k/2)*2 ? 这有点复杂,所以我们换一种更清晰的 索引约定 : 索引约定简化 : 我们使用数组 tree[0..k] (长度为k+1),其中 tree[1..k] 是内部节点(存储失败者索引), tree[0] 存储最终胜者索引。叶子节点单独放在数组 leaves[0..k-1] 中。 对于任何一个内部节点 tree[i] (i从1到k),其对应的两个子节点(胜者)来自哪里?在败者树中,每个内部节点对应一场比赛,比赛的双方是它的两个子树的胜者。为了简化,我们常常在构建时,从叶子层开始,两两配对,用下标映射。 实际上,更常见的做法是 用完全二叉树的数组表示 ,但只使用内部节点部分。这里我们采用《数据结构》教材中的经典方法: 假设有k个叶子,则败者树有k-1个内部节点,但我们通常使用长度为k的数组 ls (loser)存储,其中 ls[0] 存储最终胜者, ls[1] 到 ls[k-1] 存储内部节点的失败者。叶子值存在数组 b[0..k-1] 中。 对于内部节点 ls[i] ,其左右孩子对应的胜者如何得到?在调整过程中,我们从叶子节点向上走,通过下标计算父节点: parent = (i + k) / 2 (当叶子下标从0开始,内部节点下标从1开始时的公式)。这个公式是败者树调整时的关键。 由于细节较繁琐,我们接下来通过例子来具体说明。 5. 败者树的操作步骤 我们以k=5个参赛者为例,值分别为 {3, 1, 4, 2, 5} 。为了简单,我们补充3个无穷大值(∞),使叶子数变为8(2的幂),但实际操作中也可以处理任意k,只是索引计算稍复杂。这里我们按k=5直接演示,不补充。 步骤1:初始化 设置 leaves = [3, 1, 4, 2, 5] , k=5 。 我们需要一个长度至少为k的数组 tree (内部节点),这里我们用 tree[0..k] ,其中 tree[0] 存胜者, tree[1..k] 存内部节点失败者(实际只用前k个)。 先将所有内部节点初始化为-1(表示无失败者)。 然后,从最后一个叶子开始,向上调整。但更简单的构建方法是:先假设所有参赛者都是胜者,然后依次让它们“比赛”。 构建过程(手动模拟) : 我们将5个叶子视为5个初始胜者。我们建一棵完全二叉树,叶子是0-4。内部节点编号从5开始(如果按堆的存储方式)。但败者树常用另一种方式:叶子下标就是0~k-1,内部节点下标从k开始(偏移量)。我们不陷入下标计算泥沼,而是用 图示+文字 说明逻辑: 比较叶子0(值3)和叶子1(值1):1胜,3败。所以它们的父节点记录失败者下标0(值3)。 比较叶子2(4)和叶子3(2):2胜,4败。父节点记录失败者下标2(值4)。 比较叶子4(5)和∞(虚拟):5胜,∞败。父节点记录失败者下标4(值5)。 现在,第一层内部节点有了三个失败者记录。接下来,比较第一层的胜者:第一组胜者是叶子1(值1),第二组胜者是叶子3(值2),第三组胜者是叶子4(值5)。我们比较前两个胜者:1和2,1胜,2败,记录失败者下标3(值2)。比较第三组胜者5与∞(虚拟),5胜。然后比较1和5:1胜,5败,记录失败者下标4(值5)。最终胜者是叶子1(值1),记录在 tree[0] 。 这样,树构建完成。最终胜者是值1。 步骤2:输出冠军并调整 输出当前胜者 leaves[1]=1 。然后,我们需要从叶子1处引入一个新值(如果是内部排序,通常用∞表示该选手已被淘汰)。这里我们假设是内部排序,所以用∞替换 leaves[1] 。 然后,从叶子1(下标1)开始,向上调整: 叶子1的新值是∞。找到叶子1的父节点(即记录失败者下标0的那个节点)。在该父节点处,原来比赛的双方是下标0(值3)和下标1(旧值1),旧失败者是0(值3),胜者是1(值1)。现在新值是∞,所以让新值∞与旧失败者(下标0,值3)比较:3较小,所以3胜,∞败。因此,这个父节点应该记录失败者下标1(新值∞),并将胜者下标0向上传递。 继续向上,到上一层父节点(即刚才记录失败者下标3的节点)。在这个节点,原来比赛的双方是胜者0(值3)和胜者3(值2),旧失败者是3(值2)。现在新的胜者来自下层的是下标0(值3),它与旧失败者下标3(值2)比较:2较小,所以2胜,3败。因此,这个节点记录失败者下标0(值3),并将胜者下标3(值2)向上传递。 继续向上,到根节点的父节点(即记录失败者下标4的节点)。在这个节点,原来比赛的双方是胜者3(值2)和胜者4(值5),旧失败者是4(值5)。现在新的胜者是下标3(值2),与旧失败者4(值5)比较:2较小,所以2胜,5败。因此,这个节点记录失败者下标4(值5),胜者下标3(值2)向上传递,成为新的最终胜者。 调整结束。新的最终胜者是下标3(值2)。输出2。 重复上述调整过程,直到所有元素输出。 6. 复杂度分析 建树 :建树时需要从下到上进行比较,每层比较次数为O(k),总比较次数为O(k)。(实际上约为k-1次比较) 每次调整 :从叶子到根路径长度为O(log k),每次调整只需要进行一次比较(新值与旧失败者比较),所以调整一次的复杂度为O(log k)。 总共有n个元素(如果k=n,则是一次性建树然后逐个输出),总比较次数 = 建树O(n) + n次调整 * O(log n) = O(n log n),与堆排序相同。 空间复杂度 :需要额外存储败者树内部节点,O(k)空间。 7. 败者树的优势 相对于普通的锦标赛排序,败者树减少了一些冗余比较,因为每次调整只比较新值与路径上的旧失败者,而不需要重新比较兄弟节点。在外部排序的多路归并中,k是归并路数(通常较小且固定),败者树可以在O(log k)时间内从k个归并段中选出最小元素,效率很高。 8. 例子完整演示(简略) 由于篇幅,这里不再一步步写出全部过程。但通过上述步骤,你应该能理解:败者树的调整过程就是每次用新值(或∞)与路径上的失败者比较,胜者继续向上,失败者留在该节点。 总结 败者树是锦标赛排序的高效实现,它通过记录每场比赛的失败者,使得每次替换冠军后,调整路径上的比较次数减少到路径长度,从而优化了比较开销。它在内部排序中可以作为堆排序的替代(但实际不如堆常用),在外部排序的多路归并中则是核心数据结构。掌握败者树的关键在于理解其存储结构、索引计算以及调整过程中胜者与失败者的更新规则。