ARTICLE DETAIL

资讯详情

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

OI-wiki 锦标赛排序(Tournament Sort)详解:树形选择排序的原理、复杂度与双语言实现

OI-wiki 锦标赛排序(Tournament Sort)详解:树形选择排序的原理、复杂度与双语言实现 OI-wiki 锦标赛排序Tournament Sort详解树形选择排序的原理、复杂度与双语言实现【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. 某大型游戏线上攻略内含炫酷算术魔法项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki锦标赛排序Tournament sort又称树形选择排序是 OI-wiki「基础算法」章节中介绍的一种基于比较的排序算法。它是选择排序的优化版本也是堆排序的变体通过一棵完全二叉树组织两两比较、胜者晋级的淘汰过程将每次查找最小元素的时间从 $O(n)$ 降到 $O(\log n)$从而把整体排序复杂度从 $O(n^2)$ 提升到 $O(n\log n)$。本文以 docs/basic/tournament-sort.md 为骨架结合仓库中的 C/Python 参考实现完整讲解其定义、竞赛思想来源、建树与重建流程、性质以及可直接运行的代码读完后你将能独立实现并分析这种用空间换时间的树形排序方案。定义从选择排序到锦标赛排序选择排序 的核心思想是每一轮从未排序部分中找出最小元素与已排序部分的末尾交换重复 $n-1$ 轮完成排序。其代价在于每一轮都要在剩余元素中从头扫描一遍才能确定最小值因此整体复杂度为 $O(n^2)$。锦标赛排序正是针对这一痛点提出的优化它借助一棵完全二叉树把寻找最小值的过程组织成一场淘汰赛。每一轮两两比较、胜者晋级树根就是全局最小元素选出最小元素后只需沿该元素所在的路径重新比较一次即可更新出次小元素。因此初始化锦标赛建树需要 $O(n)$ 时间之后每从 $n$ 个元素中选取一个最小元素只需 $O(\log n)$ 时间沿树高路径更新整体排序 $n$ 个元素的时间复杂度为 $O(n\log n)$。由于它和堆排序都依赖完全二叉树这一结构锦标赛排序也被视为堆排序的一种变体——区别在于堆排序通过上浮/下沉维护堆性质而锦标赛排序通过胜者路径的逐层重比维护树根。在 OI-wiki 的 mkdocs.yml 中锦标赛排序位于basic/tournament-sort.md归属于基础算法basic章节与选择排序、堆排序同属排序算法的基础知识体系。引入名字源于单败淘汰制锦标赛排序这个名字直接来源于单败淘汰制single-elimination tournament的竞赛形式许多选手参与比赛两两比较胜者进入下一轮直至决出冠军。这种淘汰方式能够可靠地决定最好的选手但有一个著名缺陷最后一轮比赛中被淘汰的选手不一定是第二好的——他可能在此前就被冠军淘汰了。对应到排序场景一次锦标赛能确定全局最小元素但要确定次小元素必须把已胜出的元素踢出赛场置为 $\infty$再重新举行一场锦标赛。这一思想正是锦标赛排序反复建树-取根-重建循环的由来。过程最小锦标赛排序树OI-wiki 以最小锦标赛排序树为例讲解完整流程。待排序元素位于树的叶子节点内部节点记录其两个子节点中较小者优胜者红色边表示每一轮比较中较小元素的胜出路径。第一步举行首次锦标赛建树如上图所示8 个待排序元素7, 2, 3, 4, 6, 10, 8, 5依次放入叶子节点。每一轮对 $n$ 个元素两两比较后得到 $\frac{n}{2}$ 个「优胜者」每对中较小的元素进入下一轮比较如果无法凑齐一对元素那么这个元素直接进入下一轮的比较这保证了元素个数为奇数时也能正确晋级。显然完成一次「锦标赛」后树根红色路径的终点就是这一组元素中的最小值本例中为2。第二步移除胜者并重建完成一次「锦标赛」后需要将被选出的元素去除。做法与堆排序中取出堆顶后调整的思路类似直接将其值设置为 $\infty$然后沿着该叶子到树根的路径重新比较本例中原本的2被INF替换其兄弟7与其父节点所在的子树重新决出优胜者再次举行「锦标赛」选出次小元素上图中为3。之后一直重复取树根 → 置 $\infty$ → 沿路径重建这一操作直至所有元素有序。相比选择排序每轮 $O(n)$ 的扫描重建只需沿树高 $O(\log n)$ 的路径更新这是锦标赛排序获得 $O(n\log n)$ 复杂度的关键。性质稳定性不稳定锦标赛排序是一种不稳定的排序算法。原因与选择排序、堆排序一致比较过程中存在跨越式的位置交换与胜者覆盖相等元素的相对顺序无法得到保证。参照 docs/basic/sort-intro.md 对稳定性的定义——相等的元素经过排序之后相对顺序是否发生了改变——锦标赛排序不满足这一特性选择排序、堆排序、快速排序、希尔排序等同样属于不稳定排序。时间复杂度$O(n\log n)$锦标赛排序的最优、平均、最坏时间复杂度均为 $O(n\log n)$用 $O(n)$ 的时间初始化「锦标赛」自底向上两两比较共 $n-1$ 次比较然后用 $O(\log n)$ 的时间从 $n$ 个元素中选取一个元素沿树高逐层重比共需要选取 $n$ 次故总复杂度为 $O(n\log n)$。值得注意该复杂度是稳定的——无论输入数据分布如何比较次数不变与快速排序的最坏 $O(n^2)$ 不同也与堆排序类似锦标赛排序没有退化路径。空间复杂度$O(n)$锦标赛排序的空间复杂度为 $O(n)$。它需要额外的一棵完全二叉树在数组实现中即长度约为 $2n$ 的辅助数组来存放所有内部节点的比较结果无法像原地堆排序那样直接在输入数组上完成这是它相对堆排序的主要代价。实现C 与 Python 参考代码OI-wiki 在 docs/basic/tournament-sort.md 中给出了完整的 C 与 Python 两种实现。核心思想是把完全二叉树平铺进数组叶子节点从下标 $n$ 开始存放原始元素内部节点下标 $k$ 对应 $2k$ 与 $2k1$ 两个子节点数组tmp的下标即胜者的下标。C 实现int n, a[MAXN], tmp[MAXN 1]; int winner(int pos1, int pos2) { int u pos1 n ? pos1 : tmp[pos1]; int v pos2 n ? pos2 : tmp[pos2]; if (tmp[u] tmp[v]) return u; return v; } void creat_tree(int value) { for (int i 0; i n; i) tmp[n i] a[i]; for (int i 2 * n - 1; i 1; i - 2) { int k i / 2; int j i - 1; tmp[k] winner(i, j); } value tmp[tmp[1]]; tmp[tmp[1]] INF; } void recreat(int value) { int i tmp[1]; while (i 1) { int j, k i / 2; if (i % 2 0) j i 1; else j i - 1; tmp[k] winner(i, j); i k; } value tmp[tmp[1]]; tmp[tmp[1]] INF; } void tournament_sort() { int value; creat_tree(value); for (int i 0; i n; i) { a[i] value; recreat(value); } }Python 实现n 0 a [0] * MAXN tmp [0] * MAXN * 2 def winner(pos1, pos2): u pos1 if pos1 n else tmp[pos1] v pos2 if pos2 n else tmp[pos2] if tmp[u] tmp[v]: return u return v def creat_tree(): for i in range(0, n): tmp[n i] a[i] for i in range(2 * n - 1, 1, -2): k int(i / 2) j i - 1 tmp[k] winner(i, j) value tmp[tmp[1]] tmp[tmp[1]] INF return value def recreat(): i tmp[1] while i 1: j k int(i / 2) if i % 2 0: j i 1 else: j i - 1 tmp[k] winner(i, j) i k value tmp[tmp[1]] tmp[tmp[1]] INF return value def tournament_sort(): value creat_tree() for i in range(0, n): a[i] value value recreat()代码逐段解析1.winner(pos1, pos2)决出两个选手的胜者int u pos1 n ? pos1 : tmp[pos1]; int v pos2 n ? pos2 : tmp[pos2]; if (tmp[u] tmp[v]) return u;这是整套实现的基础操作。参数pos1、pos2是数组tmp中的下标而tmp[pos]存放的又是下标因此出现了一级间接寻址tmp[u]才是真正的元素值。当pos n时说明该位置就是叶子节点本身叶子节点存储的是元素值而非下标即tmp[n i] a[i]后叶子位置的值即元素值其下标就是自身此时直接返回自身下标。2.creat_tree自底向上建树首次锦标赛for (int i 0; i n; i) tmp[n i] a[i]; // 叶子节点放入原始元素 for (int i 2 * n - 1; i 1; i - 2) { int k i / 2; int j i - 1; tmp[k] winner(i, j); // 内部节点记录两个子节点中较小者的下标 }叶子节点按下标 $n \sim 2n-1$ 存放元素值然后从最后一个叶子对 $(2n-2, 2n-1)$ 开始向前每两个节点一组比较胜者下标写入它们的父节点k i/2。这里奇数个元素时最后一个元素直接晋级的规则由循环的配对方式天然保证不足一对的节点不会被比较直接由上一层继承。建树完成后value tmp[tmp[1]]; // tmp[1] 是根节点记录的胜者下标再取一次得到最小值 tmp[tmp[1]] INF; // 将胜者位置的元素值置为无穷大表示已移除3.recreat沿胜者路径重建后续锦标赛int i tmp[1]; // 从被移除的叶子节点下标出发 while (i 1) { int j, k i / 2; if (i % 2 0) j i 1; else j i - 1; // j 是与 i 配对的兄弟节点 tmp[k] winner(i, j); // 重新比较兄弟更新父节点 i k; // 向上走到父节点继续更新 }由于只有被置为INF的那条路径上的内部节点可能改变胜者其余子树均不受影响因此只需从该叶子出发逐层向上与兄弟重比、更新父节点直到树根。这正是锦标赛排序每选一个元素只需 $O(\log n)$的算法依据。recreat末尾同样执行value tmp[tmp[1]]与tmp[tmp[1]] INF为下一轮迭代做准备。4.tournament_sort主流程creat_tree(value); for (int i 0; i n; i) { a[i] value; // 依次把根节点当前最小值写回结果数组 recreat(value); // 重建锦标赛准备下一次取值 }建树后循环 $n$ 次每次把当前树根的最小值写入a[i]从小到大排序随即重建。整个流程完整覆盖初始化 反复取根重建两种语言实现逻辑完全一致。关键实现细节与易错点结合源码可以提炼出几个值得注意的实现细节tmp数组大小至少为 $2n$C 中声明为tmp[MAXN 1]因为叶子需要 $n$ 个位置下标 $n \sim 2n-1$内部节点最多 $n-1$ 个。注意creat_tree的建树循环从2 * n - 1开始、以步长-2递减实际上在配对 $(2n-2, 2n-1)$ 时最右叶子下标为 $2n-1$恰好用到数组最后一个位置。winner中的边界处理pos n时tmp[pos]本身就是叶子元素值所以直接返回pos此时tmp[pos]即元素值。如果漏掉这个判断内部节点会错误地拿下标当元素值比较。奇偶配对recreat中i % 2 0时兄弟为i 1否则为i - 1这是完全二叉树左子为 $2k$、右子为 $2k1$的数组布局直接推导出的结论。$\infty$ 的取值被选出的元素置为INF保证其在后续比较中永远输从而不会再被选为最小值。实际使用时INF需大于所有可能的元素值例如INT_MAX。与选择排序、堆排序的对比算法每次选最小值代价总时间复杂度空间复杂度稳定性选择排序$O(n)$ 全量扫描$O(n^2)$$O(1)$不稳定数组实现堆排序$O(\log n)$下沉调整$O(n\log n)$$O(1)$原地不稳定锦标赛排序$O(\log n)$沿路径重建$O(n\log n)$$O(n)$辅助树不稳定从这张表可以看出对选择排序的优化锦标赛排序用一棵完全二叉树记住了此前比较的结果避免了每轮从头扫描把单次选最小从 $O(n)$ 降到 $O(\log n)$是以空间换时间的典型与堆排序的关系两者本质都是建立在完全二叉树上的选择排序。堆排序通过sift_down维持堆性质、可以原地完成锦标赛排序则额外保存了所有比较的胜者路径重建时只需更新单条路径无需全局调整。从 docs/basic/heap-sort.md 的sift_down实现可以看出堆的调整最坏也要沿着子树下沉而锦标赛的重建严格限制在被移除叶子到根的单一路径上代价$O(n)$ 的额外空间是锦标赛排序相比堆排序的主要劣势也是它较少作为工业级默认排序方案的原因但其一次比较结果被反复利用的思想在败者树loser tree、外部多路归并排序、并行锦标赛框架等场景中仍有广泛的应用价值OI-wiki 的 动力锦标赛树 Kinetic Tournament Tree 也是同一树形思想的延伸。小结锦标赛排序是用竞赛淘汰的思想做选择排序的经典范例建树 $O(n)$、每次取最小值 $O(\log n)$、总复杂度 $O(n\log n)$代价是 $O(n)$ 的辅助空间与不稳定的排序性质。理解它不仅能掌握一种树形选择排序的实现套路更能为理解败者树、多路归并等进阶数据结构打下基础。如果需要查看本文所依赖的原始文档与邻接章节可前往 tournament-sort.md、selection-sort.md、heap-sort.md 以及排序专题入口 sort-intro.md。【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. 某大型游戏线上攻略内含炫酷算术魔法项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表