前言
本来也不用写的,是碰到排序题发现根本不会快排才学了一下锦标赛排序的啊。
介绍
锦标赛排序可以用这个等式来描述:
锦标赛排序 = ( 选择排序 + 堆排序 ) 2 \small\textsf{锦标赛排序}=\dfrac{\left(\small\textsf{选择排序}+\small\textsf{堆排序}\right)}{2} 锦标赛排序=2(选择排序+堆排序)
就是这样的,锦标赛排序本质上就是维护一个小根堆(锦标赛排序树),每次取走最小值,而且较堆排序更易于实现。
实现
首先,设原数组为 a a a,锦标赛排序树为 tr \text{tr} tr,还有一棵树 id \text{id} id,排序完的数组为 b b b。
这里我们用数组存储树,所以 tr \text{tr} tr 和 id \text{id} id 也是数组,其中对于所有整数 i ∈ [ 1 , n ] i\in[1,n] i∈[1,n],都有 tr i + n − 1 = a i , i d i + n − 1 = i \text{tr}_{i+n-1}=a_i,id_{i+n-1}=i tri+n−1=ai,idi+n−1=i。也就是说, tr \text{tr} tr 的第 n n n 层的元素全等于 a a a 中的所有元素。
在 tr \text{tr} tr 中,任意两个兄弟节点的父节点是他们的最小值,所以在开始时, tr 1 = min a \text{tr}_1=\min{a} tr1=mina,同时可以发现:对于所有整数 i ∈ [ 1 , 2 n − 1 ] i\in[1,2n-1] i∈[1,2n−1], tr i ∈ a \text{tr}_i\in a tri∈a,可以发现,此时锦标赛排序树满足小根堆的性质,大致长这样(图片来自 oi-wiki):
可以理解为有 n n n 个人在打淘汰赛,经过 log n \log n logn 场比赛后就会产生出冠军,上图中黑线表示被淘汰。
根据选择排序的规则,我们找到了 a a a 的最小值后,就应当删掉它去找次小值,在锦标赛排序中也一样。我们要找到最小值的位置,然后把最小值改成 ∞ \infty ∞,此时可以像堆一样进行向上调整,调整后的树是这样的:
也就是说,我们在 Θ ( log n ) \Theta(\log n) Θ(logn) 的时间里,就找到了次小数!以此类推,最坏情况下完成排序的时间复杂度为 Θ ( n log n ) \Theta(n\log n) Θ(nlogn)。
但这个时候有个问题,我们怎么确定最小值的位置?我们可以用另一棵树 id \text{id} id,它标记了锦标赛排序树上每个节点在 a a a 中的位置,用上面的图举个例子,我们用下标表示 id \text{id} id:
大家现在都懂了吧?让我们放代码:
#include <bits/stdc++.h>
#define one(x) ((int)(1e##x))
using namespace std;
int n;
int tr[one(6)];
int id[one(6)];
int main() {
cin>>n;
for(int i=1; i<=n; i++) {
cin>>tr[i+n-1];
id[i+n-1]=i;
}
for(int i=2*n-1; i>1; i-=2) {
int j=i-1;
int next=i/2;
if(tr[i]<=tr[j]) {
tr[next]=tr[i];
id[next]=id[i];
}else {
tr[next]=tr[j];
id[next]=id[j];
}
}
for(int i=1; i<=n; i++) {
cout<<tr[1]<<" \n"[i==n];
tr[id[1]+n-1]=1000000020;
int now=id[1]+n-1;
while(now>1) {
int j=now^1;
int next=now>>1;
if(tr[now]<=tr[j]) {
tr[next]=tr[now];
id[next]=id[now];
}else {
tr[next]=tr[j];
id[next]=id[j];
}
now=next;
}
}
return 0;
}
复杂度分析
上面已经提到过了:锦标赛排序的时间复杂度最好和最坏均为 Θ ( n log n ) + Θ ( n ) \Theta(n\log n)+\Theta(n) Θ(nlogn)+Θ(n),显然优于归并排序。空间复杂度为 Θ ( n ) \Theta(n) Θ(n),常数为 5 5 5。