读这一页之前,先知道读完要能做什么
- 写出来升序、降序、只排中间一段
- 写出来一个比较器,按多个条件排(比如「分数降序、同分学号升序」)
- 看出来什么数据要打包成
pair再排(排序结果要带着别的东西走的时候)
这是什么
一句话:sort 把一整段按你给的顺序排好。
★ 三个参数:sort(开头位置, 尾后位置, 可选的比较器)。⚠ 「尾后位置」是最后一个元素之后那个位置 —— 所以是 v.end(),不是 v.end() - 1。
★ 不写比较器就是升序;写 greater<int>() 就是降序。
完整动作表
复杂度写的是这个动作自己的代价,不是整个程序的。「这一栏最容易错的地方」那一列是重点。
| 写法 | 干什么 | 代价 |
|---|---|---|
sort(v.begin(), v.end()) | 整段升序排 ★ 不写比较器时的默认写法 | O(n log n) |
sort(v.begin(), v.end(), greater<int>()) | 整段降序排 ★★ 记这一句就够了 | O(n log n) |
sort(v.begin() + l, v.begin() + r) | 只排 [l, r) 这一段⚠ 第二个参数是 l + k,不是 l;段外的不动 | O(k log k) |
sort(arr, arr + n) | 排裸数组前 n 个 数组没长度信息,n 写错就是越界 | O(n log n) |
sort(v.begin(), v.end(), cmp) | 按你自己写的 cmp 排★★★ 「比较器」是什么、怎么写、怎么交进来 —— 全在第 1 块。先去看那一块,别在这儿猜 | O(n log n) |
sort(v.begin(), v.end(), [&](const T &a, const T &b) { ... }) | 不写名字、就地写;方括号里的 & 能读到外层的变量★ 只在「比较器要读 main 里的局部变量」时才需要它。不写 & 和函数指针一样读不到 —— 而那个坑不报错 | O(n log n) |
stable_sort(v.begin(), v.end(), cmp) | 相等时保持原来先后的排序 「同关键字要按原顺序」才用它,否则手写第二关键字 | O(n log n) |
is_sorted(v.begin(), v.end(), cmp) | 按 cmp 排好了吗★ 调试时先查这个 —— 比较器方向写反了它就红一下 | O(n) |
reverse(v.begin(), v.end()) | 原地翻转 | O(n) |
a.erase(unique(a.begin(), a.end()), a.end()) | 去重(sort 之后)★★ 三步: sort → unique → erase,缺一句都不对 | O(n log n) |
*max_element(v.begin(), v.end()) | 最大的那个值 ⚠ 前面那个 * 不能省(不加是拿到位置,不是值) | O(n) |
accumulate(v.begin(), v.end(), 0LL) | 全部加起来 ⚠ 第三个参数写 0LL:加的东西一大就溢出 | O(n) |
⭐ 这一节最容易错的地方(4 处)
这一节排在例题前面是故意的:读到一个坑,再去看正确的例子,记得住。 下面每一条都是「编译得过去、答案可能是错的」那种 —— 它们才是 OI 里真正扣分的地方。
① 比较器里不许写减号、不许写 `<=`
return a - b > 0; 是 OI 里最常见的 sort 错误。a - b 是有符号减法,两个数接近时会溢出,溢出来的符号会翻 —— 于是「相等」被判成了「后面那个在前」,而 sort 遇到这种比较器行为就没定义了。
★ 用 return a < b;(升序)或 return a > b;(降序)。比较器里只出现 < 和 >,一个减号都不许有。// 错
bool bad(int x, int y) { return x - y > 0; }
// 对
bool good(int x, int y) { return x < y; }
sort(a.begin(), a.end(), good);⚠ 「谁在前」不是「谁更大」
cmp(a, b) 问的是「a 必须排在 b 前面吗」,返回 true 就是「要排前面」。
★ 它不是「a 比 b 大吗」。这两个在「小的在前」时恰好一样,但在「大的在前」时正好相反 —— 于是传错了函数不会报错,只是顺序反了。
★ 拿不准就先自己调一下那个函数看它返回什么(第 1 块的例题 ① 就是这么做的)。bool cmpAsc(int a, int b) { return a < b; } // 小的在前
bool cmpDesc(int a, int b) { return a > b; } // 大的在前false。其余(该 < 还是 >)想清楚了再写。② 排完序,键和值散了
sort 搬的是你给它的那些元素本身。你只把分数交进去,它就只搬分数 —— 不会帮你把学号一起搬。而你输出时还在按另一个数组的顺序取学号,于是全错。
★ 类型全对、编译零警告、答案就是错的 —— 这是 OI 里最难查的一种 WA。// 错:只排了 score,输出时却按老顺序取 id
sort(score.begin(), score.end(), greater<int>());
for (int i = 0; i < n; i++) cout << score[i] << " " << id[i] << endl;
// 对:打包成 pair 一起排
vector<pair<int, int>> v; // {分数, 学号}
for (int i = 0; i < n; i++) v.push_back({score[i], id[i]});
sort(v.begin(), v.end(), [](const pair<int,int>& a, const pair<int,int>& b) {
if (a.first != b.first) return a.first > b.first;
return a.second < b.second;
});pair。下面的例题 ② 完整走一遍。③ 只排一段时,第二个参数是 `l + k`
sort 的两个参数是「开头位置」和「尾后位置」。要排第 l 到第 l+k-1 个,尾后位置是 v.begin() + l + k。
★ 写 v.begin() + l 的话,一个元素都没排(两个参数指同一处)。sort(v.begin() + l, v.begin() + l + k); // 排第 l .. l+k-1 个v.begin(), v.end() 里那个 end() 就是「末尾之后」。本书哪一章真的用到它
⚠ 下表里的文件路径和那串 probe 是逐字验过的(check:docs 第 ⑳ 条)—— 改了那份代码这儿会红,正是要的。点章号直接去看那一页。
| 章 | 书里的哪一份代码 | 用它来干什么 |
|---|---|---|
| 第 10 章 | code/10-sorting/stl.cpp | ★★ 第 10 章专门有一份 stl.cpp,开头就在论证「比赛里排序一律用 sort」 |
| 第 10 章 | code/10-sorting/p1104Stable.cpp | ★ 同一题 P1104 的「稳定版」和「不稳定版」并排放着,可以对着看 |
| 第 34 章 | code/34-mst/kruskal.cpp | ★★ Kruskal 第一步就是排序,而「按边权升序」这句话就是比较器 |
例题(2 道)
每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。先自己读题想一遍,再看代码。
例题 ① 升序、降序、只排一段
给一串整数,用三种 sort 写法各排一次:升序、降序、只排中间 [l, r) 那一段。
★ 这一题是 sort 最基础的三种形态,全都不带比较器。
n。第二行 n 个整数。| 标出哪一段被排过。输入
6 10 30 20 40 50 20
输出
原来: 10 30 20 40 50 20 ① sort(b.begin(), b.end()) 10 20 20 30 40 50 ② sort(c.begin(), c.end(), greater<int>()) 50 40 30 20 20 10 ③ 只排 [1, 4) 这一段 10 | 20 30 40 | 50 20 ↑ | 之间的才是被排过的,外面两个都没动
★ 降序那个是把比较器换成 greater<int>(),位置不用动。只排一段时想想 sort 的第二个参数是「尾后位置」,不是「最后一个」。
// STL 专题 · sort —— 默认升序、降序、只排一部分//// ★ 这一份只练 `sort` 的三种最常用写法,全是**不带比较器**的那种:// sort(a.begin(), a.end()); 升序// sort(a.begin(), a.end(), greater<int>()); 降序// sort(a.begin() + l, a.begin() + r); 只排 [l, r) 这一段//// ⚠ 输入第一行 n,第二行 n 个整数。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; cin >> n; vector<int> a(n); for (int &x : a) cin >> x;
cout << "原来: "; for (int x : a) cout << x << ' '; cout << "\n\n";
// ① 升序 vector<int> b = a; sort(b.begin(), b.end()); cout << "① sort(b.begin(), b.end()) "; for (int x : b) cout << x << ' '; cout << '\n';
// ② 降序:greater<int>() 就是「反过来比」 vector<int> c = a; sort(c.begin(), c.end(), greater<int>()); cout << "② sort(c.begin(), c.end(), greater<int>()) "; for (int x : c) cout << x << ' '; cout << '\n';
// ③ 只排一段:[l, r) 左闭右开。 // ⚠ 第二个参数是 **l + 1**,不是 l —— 因为它是「尾后位置」。 // 这一段排完,l 前面的和 r 后面的都不动。 vector<int> d = a; int l = 1, r = 4; sort(d.begin() + l, d.begin() + r); cout << "③ 只排 [" << l << ", " << r << ") 这一段 "; for (int i = 0; i < n; i++) { if (i == l) cout << "| "; cout << d[i] << ' '; if (i == r - 1) cout << "| "; } cout << '\n'; cout << " ↑ | 之间的才是被排过的,外面两个都没动\n";
return 0;}点「运行 ▶」看结果
- ① 升序:
sort(b.begin(), b.end());—— 最普通的一种。 - ② 降序:多写第三个参数
greater<int>()。⚠ 注意>结尾,不是>=。sort要的是「严格小于」,相等时必须返回false。 - ③ 只排一段:
sort(d.begin() + l, d.begin() + r);—— 这里r = l + 3,所以排的是第l、l+1、l+2这三个,前面和后面的都不动。
★★ sort(开头, 尾后, 可选比较器) —— 三个参数,第二个永远是「尾后位置」。
用到的写法:sort(v.begin(), v.end()) sort(v.begin(), v.end(), greater<int>()) sort(v.begin() + l, v.begin() + r)
例题 ② 按分数排序:打包成 pair 再排
给 n 个学生(学号 + 分数),按分数从高到低输出学号;分数相同则学号小的在前。
★ 这是 OI 里最常见的一类题。关键动作是:把两样东西打包成一个元素,再排。
n。之后 n 行,每行是 学号 分数。输入
6 1 60 2 90 3 70 4 90 5 50 6 80
输出
排好之后(分数 学号): 90 2 90 4 80 6 70 3 60 1 50 5 按题目要求输出学号: 2 4 6 3 1 5
⚠ 先想清楚一件事:你交给 sort 的是什么?只给分数的话,学号是不会跟着走的 —— 那排完怎么知道哪个学号对应哪个人?
// STL 专题 · sort —— 打包成 pair 一起排//// 这是 OI 里用得最多的 sort 写法:**把要一起走的东西打包成一个元素,再排。**//// 题目:给 n 个学生(学号 + 分数),按分数从高到低输出学号;分数相同则学号小的在前。//// ★ 为什么要打包:`sort` 搬的是**你给它的那些元素本身**。// 你只把分数交了进去,它就只搬分数 —— 不会帮你把学号一起搬。//// ⚠ 输入第一行 n,之后 n 行每行是 `学号 分数`。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; cin >> n;
// ★ 全部内容就这两句:打包成 {分数, 学号},然后排。 vector<pair<int, int>> v; // {分数, 学号} for (int i = 0; i < n; i++) { int id, score; cin >> id >> score; v.push_back({score, id}); }
// 分数降序;分数相同时学号小的在前。 // ⚠ 两个关键字的方向不一样,所以要手写,不能直接用 greater<pair>() —— // 那个是**两个关键字一起反**,会把第二级也变成降序。 sort(v.begin(), v.end(), [](const pair<int, int>& a, const pair<int, int>& b) { if (a.first != b.first) return a.first > b.first; // 分数降序 return a.second < b.second; // 学号升序 });
cout << "排好之后(分数 学号):\n"; for (const auto& p : v) cout << setw(4) << p.first << ' ' << p.second << '\n';
cout << "\n按题目要求输出学号:\n"; for (const auto& p : v) cout << p.second << ' '; cout << '\n'; return 0;}点「运行 ▶」看结果
- 先打包:
vector<pair<int, int>> v;每个元素是{分数, 学号}。⚠ 顺序是「分数在前」—— 因为默认先比first,而我们要按分数排。 - 读进来:
v.push_back({score, id}); - 写比较器:
if (a.first != b.first) return a.first > b.first;分数降序;return a.second < b.second;同分时学号升序。 - ★ ⚠ 两个关键字方向不一样(一个降一个升),所以不能直接写
greater<pair<int,int>>()—— 那个会把两个关键字一起反过来,同分的学号就变成降序了。这种时候手写比较器最省心。 - 输出:
p.second是学号。
★★ sort 搬的是你给它的那些元素。要排序的东西要和别的东西一起走,就打包成 pair。
用到的写法:sort(v.begin(), v.end(), [](const T &a, const T &b) { return ...; }) pair<int, int>
练习(2 道)
只给题面、样例和一句提示,答案是折叠着的 —— 先自己写一版。
练习 ① 去重:sort → unique → erase
给一串整数,把重复的只留一个,按从小到大输出。⚠ 输入刻意不给已经排好序的。
★ 这是 STL 里唯一一个「不加就一定错」的函数,而正确写法只有三句。
n。第二行 n 个整数(无序)。输入
10 5 3 9 3 1 8 1 5 9 3
输出
原来: 5 3 9 3 1 8 1 5 9 3 去重后: 1 3 5 8 9 长度 10 -> 5 那两句是怎么配合的: unique(v.begin(), v.end()) 返回的是「去重后那部分的**末尾位置**」 a.erase(那个位置, a.end()) 把尾巴上多出来的截掉 ⇒ unique **不改变长度**,必须配一句 erase 才真正变短
★ 想清楚 unique 的返回值是什么,就知道第三句该怎么写了。⚠ 它只去掉相邻的重复 —— 所以为什么必须先排序?
★★ sort → unique → erase,三句连着背。unique 不改变长度,它只把结果压到前段并返回末尾位置。
用到的写法:a.erase(unique(a.begin(), a.end()), a.end()) sort(v.begin(), v.end())
练习 ② 结构体排序:自己定顺序
给 n 个人(姓名、分数、年级),按分数降序排;分数相同则年级小的在前。
★ 这一题练的是:比较器里怎么读结构体的成员,以及两个关键字怎么分开判。
n。之后 n 行,每行 姓名 分数 年级。输入
6 alice 90 3 bob 70 1 carol 90 2 dan 60 3 eve 90 3 fox 70 1
输出
排好之后(姓名 分数 年级): carol 90 2 alice 90 3 eve 90 3 bob 70 1 fox 70 1 dan 60 3 注意比较器的写法: if (a.score != b.score) return a.score > b.score; 先看第一关键字 —— **不同才返回**,相同就掉到第二行去比第二个。 最后那行 return 是「兜底」:两个关键字都相同才轮到它。
★ 比较器的写法是固定的:if (第一关键字不同) return ...; 然后下一行管第二关键字。⚠ 想清楚最后那行 return 什么时候才会轮到它执行。
sort和排vector<int>一模一样,只是元素从int变成了Kid。- 比较器:
bool cmp(const Kid &a, const Kid &b) { ... }—— ⚠ 参数是const Kid &a,那个&不能省(省了每次比较都复制一整个结构体)。写法见第 1 块。 - 第一行:
if (a.score != b.score) return a.score > b.score;—— 不同才返回。 - ★ 第二行:
return a.grade < b.grade;—— 只有第一关键字相同才会走到这里,它是「兜底」。两个关键字都相同时才轮到它,而那时它必须返回false。
★★ 多关键字的固定写法:第一行 if (不同) return ...;,后面每个关键字一行。最后那行是兜底,相等时必须返回 false。
用到的写法:sort(v.begin(), v.end(), [](const T &a, const T &b) { return ...; })
← 上一块:string:读一整行、切词、找子串 · 回 STL 专题目录 · 下一块:lower_bound 和 upper_bound:在排好序的数组上找 →
⚠ 本页说的是 STL 怎么用。算法本身(Dijkstra、单调栈、状压……)在53 章正课里,而每一章末尾都会告诉你它用到了哪些 STL。