课程 · C++ 速查 · STL 专题 · 第 4 块

sort:排序和自定义比较器

学会 sort 的升序/降序/只排一段,以及要按多个条件排时怎么写比较器。

第 4 块(共 12 块已写好)12 条动作4 处陷阱2 道例题2 道练习4 份可运行代码

读这一页之前,先知道读完要能做什么

  • 写出来升序、降序、只排中间一段
  • 写出来一个比较器,按多个条件排(比如「分数降序、同分学号升序」)
  • 看出来什么数据要打包成 pair 再排(排序结果要带着别的东西走的时候)

这是什么

3 段

一句话:sort 把一整段按你给的顺序排好。

★ 三个参数:sort(开头位置, 尾后位置, 可选的比较器)。⚠ 「尾后位置」是最后一个元素之后那个位置 —— 所以是 v.end(),不是 v.end() - 1。

★ 不写比较器就是升序;写 greater<int>() 就是降序。

完整动作表

12 条

复杂度写的是这个动作自己的代价,不是整个程序的。「这一栏最容易错的地方」那一列是重点。

写法干什么代价
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() 就是「末尾之后」。

本书哪一章真的用到它

3 处

⚠ 下表里的文件路径和那串 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 道)

每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。先自己读题想一遍,再看代码。

例题 ① 升序、降序、只排一段

cmp-basic.cpp

给一串整数,用三种 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 的第二个参数是「尾后位置」,不是「最后一个」。

cmp-basic.cpp参考代码
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. ① 升序:sort(b.begin(), b.end()); —— 最普通的一种。
  2. ② 降序:多写第三个参数 greater<int>()。⚠ 注意 > 结尾,不是 >=。sort 要的是「严格小于」,相等时必须返回 false。
  3. ③ 只排一段: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 再排

cmp-follow.cpp

给 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 的是什么?只给分数的话,学号是不会跟着走的 —— 那排完怎么知道哪个学号对应哪个人?

cmp-follow.cpp参考代码
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. 先打包:vector<pair<int, int>> v; 每个元素是 {分数, 学号}。⚠ 顺序是「分数在前」—— 因为默认先比 first,而我们要按分数排。
  2. 读进来:v.push_back({score, id});
  3. 写比较器:if (a.first != b.first) return a.first > b.first; 分数降序;return a.second < b.second; 同分时学号升序。
  4. ★ ⚠ 两个关键字方向不一样(一个降一个升),所以不能直接写 greater<pair<int,int>>() —— 那个会把两个关键字一起反过来,同分的学号就变成降序了。这种时候手写比较器最省心。
  5. 输出:p.second 是学号。
记住这一句

★★ sort 搬的是你给它的那些元素。要排序的东西要和别的东西一起走,就打包成 pair。

用到的写法:sort(v.begin(), v.end(), [](const T &a, const T &b) { return ...; }) pair<int, int>

练习(2 道)

只给题面、样例和一句提示,答案是折叠着的 —— 先自己写一版。

练习 ① 去重:sort → unique → erase

cmp-unique.cpp

给一串整数,把重复的只留一个,按从小到大输出。⚠ 输入刻意不给已经排好序的。

★ 这是 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 的返回值是什么,就知道第三句该怎么写了。⚠ 它只去掉相邻的重复 —— 所以为什么必须先排序?

cmp-unique.cpp参考答案
记住这一句

★★ sort → unique → erase,三句连着背。unique 不改变长度,它只把结果压到前段并返回末尾位置。

用到的写法:a.erase(unique(a.begin(), a.end()), a.end()) sort(v.begin(), v.end())

练习 ② 结构体排序:自己定顺序

cmp-struct.cpp

给 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 什么时候才会轮到它执行。

cmp-struct.cpp参考答案
这段代码是怎么想出来的
  1. sort 和排 vector<int> 一模一样,只是元素从 int 变成了 Kid。
  2. 比较器:bool cmp(const Kid &a, const Kid &b) { ... } —— ⚠ 参数是 const Kid &a,那个 & 不能省(省了每次比较都复制一整个结构体)。写法见第 1 块。
  3. 第一行:if (a.score != b.score) return a.score > b.score; —— 不同才返回。
  4. ★ 第二行: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。