1. ACWING打卡:排序算法实战指南
最近在ACWING社区看到不少关于排序算法的打卡记录,作为算法竞赛的老兵,我想分享一些实战中的排序技巧。排序不仅是算法入门的第一课,更是面试和竞赛中的高频考点。今天我们就来深入探讨几种经典排序算法的实现细节和适用场景。
提示:本文所有代码示例均以C++实现,这是ACWING平台的主流语言,也是算法竞赛中最常用的语言之一。
1.1 为什么排序如此重要
排序算法是计算机科学的基石之一。在实际开发中,数据库索引、搜索引擎排名、推荐系统等核心功能都依赖高效的排序。在ACWING的题目中,约30%的问题都直接或间接需要排序操作。掌握排序不仅能解决"ACWING打卡:排序"这类基础题,更是攻克更复杂算法问题的敲门砖。
我整理了一份常见排序算法的时间复杂度对比表:
| 算法名称 | 平均时间复杂度 | 最坏情况 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 快速排序 | O(nlogn) | O(n²) | O(logn) | 不稳定 |
| 归并排序 | O(nlogn) | O(nlogn) | O(n) | 稳定 |
| 堆排序 | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础排序算法实现与优化
2.1 快速排序的ACWING标准实现
快速排序是ACWING题目中最常用的排序算法,以下是经过优化的实现:
cpp复制void quick_sort(int q[], int l, int r) {
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j) {
do i++; while (q[i] < x);
do j--; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}
这个实现有几个关键优化点:
- 使用位运算
l + r >> 1代替(l + r)/2,效率更高 - 采用do-while结构避免边界条件判断
- 递归时选择j作为分界点,避免死循环
注意:在ACWING的题目中,当数据量超过1e5时,务必使用快速排序或更高效的算法,否则会超时。
2.2 归并排序的稳定特性
归并排序虽然空间复杂度较高,但在需要稳定排序的场景下不可替代。ACWING中的逆序对问题就需要用到归并排序:
cpp复制int tmp[N]; // 临时数组
void merge_sort(int q[], int l, int r) {
if (l >= r) return;
int mid = l + r >> 1;
merge_sort(q, l, mid);
merge_sort(q, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
if (q[i] <= q[j]) tmp[k++] = q[i++];
else tmp[k++] = q[j++];
while (i <= mid) tmp[k++] = q[i++];
while (j <= r) tmp[k++] = q[j++];
for (i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];
}
归并排序的一个典型应用是计算逆序对数量,只需在合并过程中添加一行计数代码:
cpp复制if (q[i] > q[j]) {
res += mid - i + 1;
tmp[k++] = q[j++];
}
3. 排序算法的进阶应用
3.1 自定义排序规则的实现
ACWING的很多题目要求按照特定规则排序。比如第786题要求找出第k小的数,这时可以用快速选择算法,时间复杂度仅为O(n):
cpp复制int quick_select(int q[], int l, int r, int k) {
if (l == r) return q[l];
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j) {
do i++; while (q[i] < x);
do j--; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
int sl = j - l + 1;
if (k <= sl) return quick_select(q, l, j, k);
return quick_select(q, j + 1, r, k - sl);
}
另一个常见需求是多关键字排序。比如先按成绩降序,成绩相同再按学号升序:
cpp复制struct Student {
int id;
int score;
bool operator< (const Student &t) const {
if (score != t.score) return score > t.score;
return id < t.id;
}
} students[N];
3.2 线性时间排序的妙用
当数据范围有限时,计数排序和基数排序可以达到O(n)的时间复杂度。这在ACWING的某些特殊题目中非常有用。
计数排序实现:
cpp复制void counting_sort(int q[], int n) {
int max_val = *max_element(q, q + n);
vector<int> cnt(max_val + 1, 0);
for (int i = 0; i < n; i++) cnt[q[i]]++;
for (int i = 0, k = 0; i <= max_val; i++)
while (cnt[i]--) q[k++] = i;
}
基数排序实现(以十进制为例):
cpp复制void radix_sort(int q[], int n) {
int max_val = *max_element(q, q + n);
int exp = 1;
vector<int> tmp(n);
while (max_val / exp > 0) {
vector<int> cnt(10, 0);
for (int i = 0; i < n; i++)
cnt[(q[i] / exp) % 10]++;
for (int i = 1; i < 10; i++)
cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--)
tmp[--cnt[(q[i] / exp) % 10]] = q[i];
for (int i = 0; i < n; i++)
q[i] = tmp[i];
exp *= 10;
}
}
4. 排序算法在ACWING题目中的实战应用
4.1 第785题:快速排序模板题
这是ACWING最基础的排序题目,要求实现快速排序。我在第一次做这道题时犯了一个常见错误:
cpp复制// 错误示例:可能导致死循环
void quick_sort(int q[], int l, int r) {
if (l >= r) return;
int i = l, j = r, x = q[l];
while (i < j) {
while (q[i] < x) i++;
while (q[j] > x) j--;
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, i - 1);
quick_sort(q, i, r); // 这里可能导致无限递归
}
正确的分界点选择应该是:
cpp复制quick_sort(q, l, j);
quick_sort(q, j + 1, r);
// 或者
quick_sort(q, l, i - 1);
quick_sort(q, i, r);
但不能混用i和j作为分界点。
4.2 第786题:第k个数
这道题可以用快速选择算法优化到O(n)时间复杂度。我总结了几个关键点:
- 每次partition后,左边元素的个数sl = j - l + 1
- 如果k <= sl,只需要递归左边
- 否则递归右边,k变为k - sl
一个常见的错误是忘记调整k的值:
cpp复制// 错误示例
if (k <= sl) return quick_select(q, l, j, k);
return quick_select(q, j + 1, r, k); // 忘记减去sl
4.3 第787题:归并排序模板题
归并排序的典型应用是计算逆序对数量。我在ACWING竞赛中遇到过一道变形题,需要计算特定范围内的逆序对。解决思路是:
- 在归并过程中记录左右子数组的起始和结束位置
- 只统计特定范围内的逆序对
cpp复制int reverse_pairs(int q[], int l, int r, int L, int R) {
if (l >= r) return 0;
int mid = l + r >> 1;
int res = reverse_pairs(q, l, mid, L, R) +
reverse_pairs(q, mid + 1, r, L, R);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r) {
if (q[i] <= q[j]) tmp[k++] = q[i++];
else {
if (q[i] >= L && q[j] <= R) // 只在特定范围内统计
res += mid - i + 1;
tmp[k++] = q[j++];
}
}
while (i <= mid) tmp[k++] = q[i++];
while (j <= r) tmp[k++] = q[j++];
for (i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];
return res;
}
5. 排序算法常见问题与调试技巧
5.1 边界条件处理
排序算法最容易出错的就是边界条件。我总结了几个常见陷阱:
- 递归终止条件应该是
l >= r而不是l == r,因为可能出现l > r的情况 - partition过程中,初始值应该是
i = l - 1, j = r + 1,这样才能保证do-while的正确性 - 选择pivot时,最好选
q[l + r >> 1]而不是q[l],避免最坏情况
5.2 性能优化技巧
在ACWING竞赛中,排序算法的性能至关重要。我常用的优化手段包括:
- 对小规模数据(n <= 20)切换到插入排序
- 三数取中法选择pivot:取左、中、右三个数的中值
- 尾递归优化:将第二个递归调用改为循环
优化后的快速排序:
cpp复制void quick_sort(int q[], int l, int r) {
while (l < r) {
if (r - l <= 20) {
insertion_sort(q, l, r);
break;
}
// 三数取中
int mid = l + r >> 1;
if (q[mid] < q[l]) swap(q[mid], q[l]);
if (q[r] < q[l]) swap(q[r], q[l]);
if (q[mid] < q[r]) swap(q[mid], q[r]);
int i = l - 1, j = r + 1, x = q[r];
while (i < j) {
do i++; while (q[i] < x);
do j--; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j);
l = j + 1; // 尾递归优化
}
}
5.3 调试与验证
在ACWING提交前,我通常会做以下检查:
- 对n=0和n=1的特殊情况进行测试
- 测试已经有序和逆序的输入
- 用随机生成的大数据测试性能
- 使用ACWING的在线调试工具检查中间结果
一个实用的测试用例生成器:
cpp复制void generate_test_case(int n) {
srand(time(0));
cout << n << endl;
for (int i = 0; i < n; i++)
cout << rand() % 1000000 << " ";
cout << endl;
}
在实际编码中,我发现很多同学容易忽略排序的稳定性要求。比如在ACWING第xx题中,如果使用不稳定的快速排序会导致错误结果。这时应该改用归并排序或其他稳定算法。
最后分享一个实用技巧:在ACWING竞赛中,如果时间紧迫,可以直接使用STL的sort函数,它已经针对不同情况做了优化:
cpp复制sort(q, q + n); // 默认升序
sort(q, q + n, greater<int>()); // 降序
但要注意,STL的sort要求随机访问迭代器,不适用于链表结构。对于链表排序,应该使用list::sort成员函数。
