中 · 排序
冒泡排序
第一个「不只是看数据,而是改数据」的算法。你早就会比较两个数了 —— 排序就是把每一对都按正确的顺序比一遍。
一句话答案反复比较相邻的一对,顺序不对就交换,直到整趟都没有交换为止 —— O(n²),但 n ≤ 1000 完全够用;输出用单个空格分隔。
题目
输入: 第一行一个整数 n(1 ≤ n ≤ 1000)。第二行 n 个用空格隔开的整数。
输出: 把同样的数按从小到大输出,单个空格隔开。
样例:
| 输入 | 输出 |
|---|---|
5 | 1 3 5 8 9 |
1 | 42 |
4 | 2 2 7 7 |
原地排序指的是在原数组上重排,不是复制到新数组。所以交换需要临时变量 —— 下面那个错误示例会让你看清为什么。
你的解答
「运行我的输入」用的自定义输入
判题在浏览器本地运行。死循环会在 3 秒后被自动中断 —— 但还是确保每个循环都有出口。代码随打随存。
卡住了?提示
提示 1 — 一趟就能把最大的数送到末尾
走一遍数组,每个元素和它的下一个比较。如果左边更大就交换。走完一趟,最大的值就「冒」到了最后 —— 因为它在参与的每次比较里都赢。做 n−1 趟,全部有序。
提示 2 — 内层循环每轮都变短
跑了 i 趟之后,最后 i 个元素已经到位了。所以内层只需要 j < n - 1 - i。去掉 - i 结果仍然正确,只是白干活 —— 你会一遍遍重复比较已经排好的尾部。
提示 3 — 交换是三条赋值
int t = a[j]; a[j] = a[j+1]; a[j+1] = t; —— 顺序很重要。第一行存在的唯一目的,就是把你马上要覆盖掉的那个值先存起来。少了它,那个值就永久消失了:你会得到一个重复的数和一个消失的数。
题解(先自己试试!)
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int a[1005];
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
if (a[j] > a[j + 1]) {
int t = a[j];
a[j] = a[j + 1];
a[j + 1] = t;
}
}
}
for (int i = 0; i < n; i++) cout << a[i] << " ";
cout << endl;
return 0;
}
冒泡排序是 O(n²),生产环境没人用它 —— 但它是「比较」和「交换」这两个所有排序算法共有的概念最清楚的展示。手写过一次之后,库函数排序就不再是魔法了。一个值得做的练习:加一个 bool swapped 标志,每次交换置真,如果某一整趟没有任何交换就跳出外层 —— 已经有序的数组于是只需一趟。接下来别再排序了,开始利用有序性:二分查找。
深度解析
为什么这样做对
比较相邻两个,逆序就交换。走一趟,最大的必然冒到末尾 —— 因为它在参与的每一次比较里都赢。重复 n−1 趟,全部有序。
代价
O(n²)。一千个数约五十万次比较,而快排只要一万次左右。它的价值在教学,不在生产。
常见错法
- 交换时忘了临时变量,写成
a[j] = a[j+1]; a[j+1] = a[j];—— 两个位置都变成较小值,大的那个永久消失。 - 内层边界很要命:
j < n会越界读a[n];而j < n - 1只是慢(重复比较已排好的尾部),不会错。
下一步
排好序是为了用它:二分查找 每一步扔掉一半。
记住这两句
- 一趟冒泡 = 最大的那个就位。
- 交换是三条赋值,第一条存在的唯一目的是别把值弄丢。
🐞 这段是错的 —— 点开,亲手让它挂一次
交换时忘了临时变量 —— 两个位置都变成较小值,大的那个丢了
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int a[1005];
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
if (a[j] > a[j + 1]) {
a[j] = a[j + 1];
a[j + 1] = a[j];
}
}
}
for (int i = 0; i < n; i++) cout << a[i] << " ";
cout << endl;
return 0;
}