课程 · C++ 速查 · 排序与比较器

排序与比较器 例题与练习

练那句唯一要记的话:sort 不知道谁该排前面,那是你告诉它的 —— 而并列的时候,它更不知道。

2 道例题3 道练习5 份可运行代码回速查看这一组的 8 条 →
怎么用这一页

例题给全套:题面、样例、参考代码、以及一步步的讲解。先自己读题想一遍, 再看代码,最后对着讲解核一遍自己想的对不对。

练习只给题面、样例和一句提示,答案是折叠起来的 —— 请先自己写一版跑通了再展开。写不出来也别直接看答案,先回速查那一组找找。

每份代码都能直接点运行:输入框里已经填好了样例输入, 把它改一改再跑,是这一页最值钱的用法。

先把地基摆一遍

4 段

要记的只有一件事:sort 不知道「谁该排前面」,那是你告诉它的。不告诉它,它就按 < 排(数字从小到大、字符串按字典序)。

★ 它要的两个参数是「从哪儿开始」和「到哪儿结束」,而结束那个是「最后一个的下一格」。所以整个 vector 是 a.begin(), a.end(),裸数组是 arr, arr + n;只排前 5 个就是 a.begin(), a.begin() + 5。这个「左闭右开」的习惯贯穿整个 STL。

⚠ 自己写比较规则时,函数返回的是「a 该排在 b 前面吗」,不是「谁更大」。想降序就写 return a > b; —— 读成「a 比 b 大时,a 排前面」。

★★ 而这里有一条会让程序直接崩溃的规矩:比较函数里绝对不能写 >=<=。sort 要求「两个元素一样时必须返回 false」,写成 >= 时 a 和 b 相等也返回 true,sort 内部就会越过数组边界一直找下去。⚠ 而小数据上它常常正好不崩,第 10 章量过这件事。

例题(2 道)

每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。

例题 ① 成绩排名

sortEx1.cpp

n 个学生,每人一个名字和一个分数。请按分数从高到低输出;分数相同的,按输入顺序(先输入的排前面)。

输入格式第一行一个整数 n1 ≤ n ≤ 10⁵);接下来 n 行,每行一个不含空格的名字和一个整数分数。
输出格式n 行,每行一个名字和一个分数。

输入

4
ann 90
bob 85
cid 90
dan 70

输出

ann 90
cid 90
bob 85
dan 70
ann 和 cid 都是 90 分 ⇒ 先输入的 ann 排前面。
先想一想

两个坑:① 名字和分数要一起排序,怎么打包?② 题面说「分数相同按输入顺序」—— 而 sort不稳定的,它不保证并列的两个谁在前。⇒ 光按分数排够不够?

sortEx1.cpp参考代码
// 例题 ①「成绩排名」—— 排序与比较器组
//
// 三件事一次学完:struct 打包、lambda 当比较规则、**并列时怎么办**。
// ⚠ 题面要求「分数相同按输入顺序」,而 sort 是**不稳定**的 ——
// 不能指望它自己保住输入顺序,得**把输入序号也写进比较规则里**。
#include <bits/stdc++.h>
using namespace std;
struct Student { // ⚠ 花括号后面这个分号不能漏
string name;
int score;
int id; // 输入序号 —— 就是为了并列时能分出高下
};
int main() {
int n;
cin >> n;
vector<Student> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i].name >> a[i].score;
a[i].id = i;
}
sort(a.begin(), a.end(), [](const Student &x, const Student &y) {
// 返回的是「x 该排在 y 前面吗」,不是「谁更大」
if (x.score != y.score) return x.score > y.score; // 分数高的在前
return x.id < y.id; // 分数一样:先来的在前
// ⚠ 绝对不能写成 return x.score >= y.score;
// 相等时也说「x 排前面」,sort 会一直往前找、越过数组边界。
});
for (const Student &s : a) cout << s.name << ' ' << s.score << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. struct Student { string name; int score; int id; }; ——把要一起动的几个量打包成一个新类型。⚠ 花括号后面那个分号不能漏
  2. 那个 id 是专门加的:读入时记下「我是第几个」,排序时用它来给并列的人分高下。⇒ 这比换成 stable_sort 更稳,因为它把「凭什么这么排」写在了代码里,而不是依赖某个库的性质。
  3. 比较规则用 lambda 就地写:[](const Student &x, const Student &y) { … }[] 是它的开头标记,后面是参数表和函数体,整个东西直接塞进 sort 的第三个位置。
  4. ⚠ 返回的是「x 该排在 y 前面吗」,不是「谁更大」。所以「分数高的在前」写成 return x.score > y.score;
  5. ⚠⚠ 而这一行绝对不能写成 return x.score >= y.score;:sort 要求「两个元素一样时必须返回 false」,写成 >= 时它会一直往前找、越过数组边界 —— 表现是 Segmentation fault,而小数据上常常正好不崩
  6. 并列时要再比一个字段:if (x.score != y.score) return x.score > y.score; return x.id < y.id; ——先分主次,主的分不出高下再看次的。这是所有「多关键字排序」的通用形状。
记住这一句

这道题的四行比较规则,是本书第 10、19、20、34 章每一章都要写一遍的东西。

用到的写法:struct [](const T &a, const T &b) { return ...; } 比较器里不许写 >= sort(a.begin(), a.end()) stable_sort

例题 ② 按右端点排序

sortEx2.cpp

n 个区间 [l, r],请按右端点从小到大输出;右端点相同的按左端点从小到大。

★ 这不是随便编的题:第 19、20 章的区间贪心,第一步永远是「按右端点排序」。

输入格式第一行一个整数 n1 ≤ n ≤ 10⁵);接下来 n 行,每行两个整数 lr
输出格式n 行,每行两个整数 lr

输入

4
1 5
2 3
0 3
4 9

输出

0 3
2 3
1 5
4 9
按 r 排:3、3、5、9;两个 r = 3 的再按 l 排 ⇒ 0 32 3 前面。
先想一想

这道题一行比较器都不用写。想想 pair 自带的规矩是什么,然后决定「哪个量该放在 first」。

sortEx2.cpp参考代码
// 例题 ②「按右端点排序」—— 排序与比较器组
//
// 第 19、20 章的区间贪心,第一步永远是「按右端点排序」。
// 这里不用写比较器:pair 自带的规矩就是「先比 first,first 一样再比 second」
// ⇒ 只要把要排的那个量放在 first 就行。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<pair<int, int>> a(n); // 存成 (右端点, 左端点)
for (int i = 0; i < n; i++) {
int l, r;
cin >> l >> r;
a[i] = {r, l}; // ⚠ 谁放 first 是**约定**不是类型 —— 放反了编译器一声不吭
}
sort(a.begin(), a.end()); // 按 r 升序,r 相同按 l 升序
for (const auto &p : a) cout << p.second << ' ' << p.first << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. pair<int,int> 自带的比较规则正好是:先比 firstfirst 一样再比 second。⇒ 「按 r 排、r 同按 l 排」和这条规矩一字不差。
  2. 所以只要把 r 放进 firsta[i] = {r, l};,然后 sort(a.begin(), a.end()) 就完事了。
  3. ⚠ 而这正是 pair 最容易咬人的地方:谁放 first 是一个约定,不是类型的一部分。放反了编译器一声不吭,程序照跑,只是排出来的顺序不对。本书第 29 章存图用 (邻居, 权)、第 32 章堆里用 (距离, 编号) —— 顺序正好相反
  4. 输出时记得换回来:p.second 是 l、p.first 是 r。
  5. sort 的两个参数是「从哪儿开始」和「到哪儿结束」,而结束那个是最后一个的下一格a.end())。这个「左闭右开」的习惯贯穿整个 STL。
记住这一句

能不写比较器就别写 —— 少一处能写错的地方。⚠ 但代价是「谁是 first」这件事只活在你脑子里,所以写注释说清楚。

用到的写法:pair<int, int> sort(a.begin(), a.end())

练习(3 道)

答案折叠着,先自己写一版。

练习 ① 从大到小

sortTry1.cpp

n 个整数,从大到小输出。

输入格式第一行一个整数 n1 ≤ n ≤ 10⁵);第二行 n 个整数。
输出格式一行 n 个整数,用空格隔开,行末不留空格。

输入

5
3 1 4 1 5

输出

5 4 3 1 1
两个 1 并列 —— 这道题里它们长得一样,谁在前都行。
提示

两条路都对:给 sort 传第三个参数,或者升序排完再翻过来。⚠ 用前者的话,那个参数后面的一对空括号不能少

sortTry1.cpp参考答案

用到的写法:sort(a.begin(), a.end(), greater<int>()) sort(a.begin(), a.end())

练习 ② 中位数

sortTry2.cpp

n 个整数(n 保证是奇数),输出排好序之后正中间的那个数。

输入格式第一行一个奇数 n1 ≤ n ≤ 10⁵);第二行 n 个整数。
输出格式一行一个整数。

输入

5
7 1 9 3 5

输出

5
7 1 9 3 5 排好是 1 3 5 7 9,正中间是 5。
提示

排完序之后,「正中间」的下标是几?拿 n = 5 在纸上数一遍就知道了。

sortTry2.cpp参考答案
记住这一句

⚠ 顺带想一想:如果 n 可能是偶数,题目该怎么定义中位数?(这就是第 37 章那道「对顶堆」的入口。)

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

练习 ③ 按绝对值排序

sortTry3.cpp

n 个整数,按绝对值从小到大排序输出;绝对值相同的,小的排前面

输入格式第一行一个整数 n1 ≤ n ≤ 10⁵);第二行 n 个整数。
输出格式一行 n 个整数,用空格隔开,行末不留空格。

输入

5
3 -1 -4 1 5

输出

-1 1 3 -4 5
-11 的绝对值都是 1 ⇒ 按第二条规矩,-1 排前面。
提示

比较规则不一定要直接比元素本身,比「元素算出来的某个量」也行。⚠ 而题面那句「绝对值相同时小的排前面」是必须写进比较器的 —— 想想不写会怎样。

sortTry3.cpp参考答案
记住这一句

这道题把「多关键字」的形状又走了一遍:先比主的,主的分不出高下再比次的。

用到的写法:[](const T &a, const T &b) { return ...; } 比较器里不许写 >=