排序
你听说过猴子排序吗?
简介 #
排序数组 $a$(长度 $n$)中的元素. 本章只研究升序排序.
选择排序 #
在第 $i$ 次遍历中,交换 $a[i]$ 和第 $i$ 小的数.
时间复杂度:$O(n^2)$.
for (int i = 1; i <= n; i ++) {
int tmp = i; // tmp: 第 i 小的数
for (int j = i + 1; j <= n; j ++)
if (a[j] < a[tmp]) tmp = j;
swap(a[i], a[tmp]);
}
冒泡排序 #
重复扫描数组 $a$. 若 $a[i]>a[i+1]$ 就交换它们. 当没有可交换元素时结束排序.
时间复杂度:$O(n^2)$.
while (true) {
bool swapped = false;
for (int i = 1; i <= n; i ++) {
if (a[i] > a[i + 1]) {
swap(a[i], a[i + 1]);
swapped = true;
}
}
if (!swapped)
break;
}
插入排序 #
将数组 $a$ 分为「已排序」和「未排序」区. 每次将「未排序」区的一个元素插入「已排序区」的正确位置.
时间复杂度:$O(n^2)$.
for (int i = 2; i <= n; i ++) {
int tmp = a[i], j = i - 1;
while (j >= 1 && a[j] > tmp)
a[j + 1] = a[j --];
a[j + 1] = tmp;
}
计数排序 #
记录每个元素出现的次数,然后依次输出.
$f[x]$:数字 $x$ 出现了几次.
时间复杂度:$O(n)$. 不适用于大范围元素.
int maxn = - 1e9;
for (int i = 1; i <= n; i ++) {
f[a[i]] ++;
maxn = max(maxn, a[i]);
}
int p = 0;
for (int i = 1; i <= maxn; i ++)
for (int i = 1; i <= f[i]; i ++)
a[++ p] = i;
快速排序 #
- 设定基准数为数组的中间数 $a[m]$.
- 扫描数组,将所有比 $a[m]$ 小的元素移至其左,大于 $a[m]$ 的元素移至其右.
- 对 $a[m]$ 左右的子数组进行相同操作.
平均时间复杂度:$O(n\log n)$.
最坏时间复杂度:$O(n^2)$.
void qsort(int l, int r) { // 对 a[l ... r] 排序
if (l >= r) return;
int i = l, j = r, m = (l + r) / 2;
while (i <= j) {
while (a[i] < a[m]) i ++;
while (a[j] > a[m]) j --;
if (i <= j)
swap(a[i ++], a[j --]);
}
qsort(l, j), qsort(i, r);
}
归并排序 #
- 将数组分为 $a[l\sim m]$ 和 $a[m+1\sim r]$ 两个子数组.
- 递归排序两个子数组.
- 合并两个已排序的子数组.
- 设定指针 $i$ 和 $j$,最初位置分别为两个子数组的起始位置.
- 比较 $a[i]$ 和 $a[j]$,选择更小的放入数组 $b$ ,并移动其指针向下一位置.
- 重复上一步直到某指针超出数组末尾.
- 将子数组剩下的所有元素放入数组 $b$,再将数组 $b$ 拷贝到数组 $a$.
时间复杂度:$O(n\log{n})$.
void msort(int l, int r) {
if (l == r) return;
int m = (l + r) / 2;
msort(l, m);
msort(m + 1, r);
int i = l, j = m + 1, k = l;
while (i <= m && j <= r) {
if (a[i] <= a[j])
b[k ++] = a[i ++];
else
b[k ++] = a[j ++];
}
while (i <= m) b[k ++] = a[i ++];
while (j <= n) b[k ++] = a[j ++];
for (i = l; i <= r; i ++)
a[i] = b[i];
}
猴子排序 #
随机交换两个元素,直到排完序.
平均时间复杂度:$O(n\cdot n!)$.
最好时间复杂度:$O(n)$.
最坏时间复杂度:$O(\infty)$.
srand(unsigned(time(0)));
while (true) {
swap(a[rand() % n + 1], a[rand() % n + 1]);
bool solve = true;
for (int i = 2; i <= n; i ++)
if (a[i] < a[i - 1])
solve = false;
if (solve) break;
}
逆序对数 #
若 $i<j$ 且 $a[i]>a[j]$,则 $(a[i],a[j])$ 是一组逆序对.
在 归并排序 的第 3 步:合并两个有序数组时,若出现 $a[i]>a[j]$ 的情况,由 $i<j$ 可知 $a[i\sim m]$ 和 $a[j]$ 构成了 $m-i+1$ 个逆序对,便可以统计到答案中.
void msort(int l,int r){
if(l == r) return;
int m = (l + r) / 2;
msort(l, m);
msort(m + 1, r);
int i = l, j = m + 1, k = l;
while (i <= m && j <= r) {
if(a[i] <= a[j])
b[k ++] = a[i ++];
else {
b[k ++] = a[j ++];
ans += m - i + 1; // 统计逆序对
}
}
while (i <= m) b[k ++] = a[i ++];
while (j <= r) b[k ++] = a[j ++];
for (i = l; i <= r; i ++)
a[i] = b[i];
}
稳定性 #
若在原数组中,任意相同元素在排序前后的相对位置不变,则此排序是稳定的.
稳定排序不会破坏相同元素的原顺序. 例如对于结构体 Student
:
struct Student {
int score;
int id;
}
现给出已按 id
排序的 Student
数组,要求按 score
再排序. 稳定排序能保证所有 score
相同的 Student
仍按照 id
排序.
算法 | 是否稳定 | 算法 | 是否稳定 |
---|---|---|---|
选择排序 | $\times$ | 快速排序 | $\times$ |
冒泡排序 | $\sqrt{}$ | 归并排序 | $\sqrt{}$ |
插入排序 | $\sqrt{}$ | 猴子排序 | $\times$ |
记数排序 | $\sqrt{}$ |