读这一页之前,先知道读完要能做什么
- 说清「比较器」是什么 —— 它就是一个回答「谁在前」的普通函数
- 写出来一个
cmp函数,用sort(v.begin(), v.end(), cmp)交给 sort - 写出来多关键字的比较器(加一个关键字只要加一行)
- 知道
greater<int>()是什么,以及它和你自己写的函数是同一件事
这是什么
★★ 先回答「比较器是什么」—— 这是 sort 唯一需要你告诉它的事。
★ sort 根本不知道你要按什么排。它只会反复问同一个问题:「a 该排在 b 前面吗?」你回答「是」,它就把 a 放前面。
★ 所以「比较器」不是什么神秘的东西 —— 它就是一个普通函数,参数是两个要比的元素,返回「谁在前」。
所以你只要写这两个函数里的任意一个就够了:bool cmpAsc(int a, int b) { return a < b; }(小的在前) 和 bool cmpDesc(int a, int b) { return a > b; }(大的在前)。⚠ 它们只是差一个符号 —— 下面例题 ① 会把两个都真的跑一遍给你看。
★★★ 写比较器只有一条规矩,别的都可以忘:两个元素「一样」时必须返回 false。
所以用 < 和 >,绝对不要用 <= 或 >=。
完整动作表
复杂度写的是这个动作自己的代价,不是整个程序的。「这一栏最容易错的地方」那一列是重点。
| 写法 | 干什么 | 代价 |
|---|---|---|
bool cmp(const T& a, const T& b) { return a < b; } | 这就是一个比较器 —— 一个普通函数而已 ★ const T&a 是「只读、不复制」;写值的话每次比较都复制一份 | —— |
sort(v.begin(), v.end(), cmp) | 把 cmp 交给 sort★★★ 函数名后面不写括号。 cmp 是函数本身,cmp() 是「调用它算出一个 bool」 | O(n log n) |
sort(v.begin(), v.end(), greater<int>()) | 降序 —— 标准库替你写好的那个比较器 ★★ 它和你自己写的 bool cmp(int a,int b){ return a>b; } 是同一件事 | O(n log n) |
greater<long long>() / greater<string>() | 换个类型就换个后面那个类型greater 对 string 是按字典序 | O(n log n) |
sort(v.begin(), v.end(), [](const T& a, const T& b) { return a > b; }) | 不写名字、就地写一个小函数 ★★ 和函数指针完全等价 —— 学生写哪种都行,OI 里 lambda 更常见 | O(n log n) |
sort(v.begin(), v.end(), [&](const T& a, const T& b) { ... }) | 里面要用外层的变量:方括号里的 & 是「把它们拿进来」★ 只在比较器要读 main 里的变量时才用;不写 & 编译器会报「未捕获」 | O(n log n) |
bool (*p)(const T&, const T&) | 「函数指针」的类型 —— 想把比较器存进变量时才写 ⚠ 竞赛里几乎不用写,知道有这种东西就行 —— 这一块只教上面那两种 | —— |
if (a.score != b.score) return a.score > b.score; return a.id < b.id; | 多关键字比较器的固定套路 ★★★ 第一行「分得出胜负才返回」;分不出就掉到第二行去比下一个关键字 | —— |
is_sorted(v.begin(), v.end(), cmp) | 已经按 cmp 排好了吗★ 调试时查这个 —— 比较器方向写反了它就红一下 | O(n) |
⭐ 这一节最容易错的地方(3 处)
这一节排在例题前面是故意的:读到一个坑,再去看正确的例子,记得住。 下面每一条都是「编译得过去、答案可能是错的」那种 —— 它们才是 OI 里真正扣分的地方。
① 函数名后面写了括号
sort(v.end(), cmp) 里那个 cmp 是函数本身,而 cmp() 是调用它算出一个结果 —— 那个结果是个 true / false,是个数字,不是函数。
★ 所以:传函数不写括号,自己调用它才写括号。sort(v.begin(), v.end(), cmp); // 对:交出函数本身
bool r = cmp(a, b); // 对:调用它
sort(v.begin(), v.end(), cmp()); // ✗ 编译不过(类型不对)② 比较器里不许写减号、不许写 `<=`
return a - b > 0; 是 OI 里最常见的 sort 错误。a - b 是有符号减法,两个数接近时会溢出,溢出来的符号会翻 —— 于是「相等」被判成了「后面那个在前」,而 sort 遇到这种比较器行为就没定义了。
★ 而这一条不一定崩,常常只是「排出来的顺序不对」—— 所以特别难查。
⚠ 「相等必须返回 false」那条规矩,用 <= 就会违反。// 错(两处)
sort(a.begin(), a.end(), [](int x, int y) { return x - y > 0; });
sort(a.begin(), a.end(), [](int x, int y) { return x <= y; });
// 对
sort(a.begin(), a.end(), [](int x, int y) { return x < y; });< 和 >,一个减号都不许有。③ 「谁在前」不是「谁更大」
cmp(a, b) 问的是「a 必须排在 b 前面吗」,返回 true 就是「要排前面」。
★ 它不是「a 比 b 大吗」。这两个在「小的在前」时恰好一样,但在「大的在前」时就正好相反 —— 于是传错了函数不会报错,只是顺序反了。
★ 一句话记住:想清楚你想排的那个方向,再决定写 < 还是 >。bool cmpAsc(int a, int b) { return a < b; } // 小的在前
bool cmpDesc(int a, int b) { return a > b; } // 大的在前本书哪一章真的用到它
⚠ 下表里的文件路径和那串 probe 是逐字验过的(check:docs 第 ⑳ 条)—— 改了那份代码这儿会红,正是要的。点章号直接去看那一页。
| 章 | 书里的哪一份代码 | 用它来干什么 |
|---|---|---|
| 第 10 章 | code/10-sorting/studentFast.cpp | ★★ 第 10 章已经在用「函数名不带括号」这种写法了 —— 就是从这一份来的 |
| 第 10 章 | code/10-sorting/badcmp.cpp | ★ 同一份代码的「违反相等必须返回 false」的版本,可以对照着看后果 |
| 第 16 章 | code/16-dfs-pruning/p1074Row.cpp | ★ 一个要读外部数组的例子 —— 它用了 lambda,因为 blank 是外层的变量 |
例题(2 道)
每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。先自己读题想一遍,再看代码。
例题 ① 把一个函数交给 sort
写两个比较器 cmpAsc(小的在前)和 cmpDesc(大的在前),分别交给 sort,再亲手调一次比较器看看它返回什么。
★ 这一题练的就是「函数名不带括号」这一个动作。
n。第二行 n 个整数。第三行一个字符(没用上,只是让样例固定)。输入
6 1 30 10 50 20 40 30
输出
原来: 30 10 50 20 40 30 升序(传 cmpAsc): 10 20 30 30 40 50 降序(传 cmpDesc): 50 40 30 30 20 10 自己用一下 cmpAsc: cmpAsc(3, 5) = 1 ← 「3 要排在 5 前面吗」是 cmpAsc(5, 3) = 0 ← 「5 要排在 3 前面吗」是 ⚠ 如果你想要降序却传了 cmpAsc,就得到升序 —— 不会报错,只是方向反了
★ 关键就一个细节:传给 sort 的时候函数名后面不要写括号。写 cmpAsc 是把函数本身交出去,写 cmpAsc() 是先算出 true/false 再交那个结果。
// STL 专题 · 前置知识 —— 「把一个函数当参数传给另一个函数」//// ★ sort 要问你「a 该排在 b 前面吗」,而你回答它的那个东西就住在函数里。// 这一份练的就是**怎么把那个函数交给 sort**。//// ⚠⚠ 先看清一件事:**sort 完全不知道你要按什么排。**// 它只是反复问同一个问题「a 该排在 b 前面吗」,你回答是,它就把 a 放前面。// 所以「比较器」不是什么神秘的东西 —— **就是一个回答「谁在前」的普通函数。**//// ⚠ 输入第一行 n,第二行 n 个整数,第三行一个字符 d('a' 升序 / 'd' 降序)。
#include <bits/stdc++.h>using namespace std;
// ★★ 这就是「比较器」的全部长相:一个普通函数,返回「谁在前」。// 名字随便起,参数是两个要比的元素,返回 bool。// ⚠ 它的含义是「a 必须排在 b 前面吗」—— 是就返回 true。// ⚠⚠ 切勿写成「a 比 b 大吗」!这两个是一回事,但方向反过来就全错了。bool cmpAsc(int a, int b) { return a < b; } // 小的在前bool cmpDesc(int a, int b) { return a > b; } // 大的在前
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; char d; cin >> n >> d; vector<int> v(n); for (int &x : v) cin >> x;
cout << "原来: "; for (int x : v) cout << x << ' '; cout << "\n\n";
// ★★★ 关键就在这一行:**函数名后面不写括号。** // // cmpAsc → 函数本身(它的地址) // cmpAsc() → ✗ 调用它算出一个 bool,那是个数字,不是函数 // // sort 的第三个参数要的是「函数」,不是「函数的结果」。 vector<int> a = v, b = v; sort(a.begin(), a.end(), cmpAsc); // 升序 sort(b.begin(), b.end(), cmpDesc); // 降序
cout << "升序(传 cmpAsc): "; for (int x : a) cout << x << ' '; cout << "\n降序(传 cmpDesc): "; for (int x : b) cout << x << ' '; cout << "\n\n";
// 同一个「比较器」也可以自己用,不一定要交给 sort: cout << "自己用一下 cmpAsc:\n"; cout << " cmpAsc(3, 5) = " << cmpAsc(3, 5) << " ← 「3 要排在 5 前面吗」是\n"; cout << " cmpAsc(5, 3) = " << cmpAsc(5, 3) << " ← 「5 要排在 3 前面吗」是\n\n";
// 搞混方向的后果:把 cmpAsc 交给「要降序」的地方用 vector<int> c = v; sort(c.begin(), c.end(), cmpAsc); cout << "⚠ 如果你想要降序却传了 cmpAsc,就得到升序 —— 不会报错,只是方向反了\n";
return 0;}点「运行 ▶」看结果
- 比较器就是一个普通函数:
bool cmpAsc(int a, int b) { return a < b; }—— 不用void,返回bool。 - 交给 sort:
sort(a.begin(), a.end(), cmpAsc);—— ⚠ 没有括号。 - ★ 自己调它:这样就能看清它到底返回什么 ——
cmpAsc(3, 5)是「3 要排在 5 前面吗」=true;cmpAsc(5, 3)是false。拿不准的时候就这样手算一遍,比盯着代码想快得多。 - 注意两个函数只是差一个符号:
a < b是小的在前,a > b是大的在前。
★★ 给别人的时候不写括号,自己叫它的时候写。
用到的写法:bool cmp(const T& a, const T& b) { return a < b; } sort(v.begin(), v.end(), cmp)
例题 ② greater<int>() 就是写好的比较器
用 greater<int>()、自己写的降序函数、升序后翻转,三种办法各排一次,看它们的结果。
★ 这一题回答一个最常被问的问题:那个 greater<int>() 到底是什么?
n。第二行 n 个整数。输入
6 30 10 50 20 40 30
输出
原来: 30 10 50 20 40 30 ① greater<int>(): 50 40 30 30 20 10 ② 自己写 cmpDesc: 50 40 30 30 20 10 ③ 升序 + 翻转: 50 40 30 30 20 10 ①② 结果一样吗:一样 ✓(它们就是同一个东西) ③ 结果和①②一样吗:内容一样,但**走的是另一条路** vector<string> 用 greater<string>()(按字典序,大的在前): pear fig apple
★ 一句话:greater<int>() 就是标准库替你写好的那个「大的排前面」 —— 和你自己写的 bool cmp(int a,int b){ return a>b; } 是同一件事。那个 <int> 是「装什么」,排 string 就写 greater<string>()。
// STL 专题 · 前置知识 —— greater<int>() 是什么//// 学生最常问的一句:「`sort(v.begin(), v.end(), greater<int>())` 那个 greater<int>() 是什么?」//// ★ 一句话:**它就是一个写好了的比较器** —— 标准库替你写好的那个「大的排前面」。// 你自己写的 `bool cmpDesc(int a, int b){ return a > b; }` 和它是**同一件事**。//// 这一份把三件事分清楚:// ① 比较器 = 「一个回答谁在前的函数」;// ② `greater<int>()` = 标准库写好的降序比较器;// ③ 什么时候要自己写,什么时候直接用 greater<int>()。//// ⚠ 输入第一行 n,第二行 n 个整数。
#include <bits/stdc++.h>using namespace std;
// 自己写的降序比较器 —— 和 greater<int>() 做同一件事bool cmpDesc(int a, int b) { return a > b; }
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; cin >> n; vector<int> v(n); for (int &x : v) cin >> x;
cout << "原来: "; for (int x : v) cout << x << ' '; cout << "\n\n";
// 两种写法(①和②)排出来的结果**完全一样**。 // ③ 是另一件事:先升序、再把整个数组翻转 —— 得到的是**降序**, // 但那是「排完再翻」,不是「用比较器排」。⚠ 两者代价不同: // 比较器排是 O(n log n),排完再翻是 O(n log n) + O(n)。 vector<int> a = v, b = v, c = v; sort(a.begin(), a.end(), greater<int>()); // 用标准库写好的 sort(b.begin(), b.end(), cmpDesc); // 用自己写的 sort(c.begin(), c.end()); // 升序 reverse(c.begin(), c.end()); // 再翻转
cout << "① greater<int>(): "; for (int x : a) cout << x << ' '; cout << "\n② 自己写 cmpDesc: "; for (int x : b) cout << x << ' '; cout << "\n③ 升序 + 翻转: "; for (int x : c) cout << x << ' ';
cout << "\n\n①② 结果一样吗:" << (a == b ? "一样 ✓(它们就是同一个东西)" : "不一样(不该出现!)") << "\n③ 结果和①②一样吗:" << (a == c ? "内容一样,但**走的是另一条路**" : "不一样(不该出现!)") << '\n';
// ★ greater<int>() 里那个 <int> 是「装什么」 // 换成别的类型,函数名后面跟着的类型就跟着换。 vector<string> w = {"pear", "apple", "fig"}; sort(w.begin(), w.end(), greater<string>()); cout << "\nvector<string> 用 greater<string>()(按字典序,大的在前): "; for (const auto& s : w) cout << s << ' '; cout << '\n';
return 0;}点「运行 ▶」看结果
- ① 用标准库的:
sort(a.begin(), a.end(), greater<int>()); - ② 用自己写的:
sort(b.begin(), b.end(), cmpDesc);—— 两者结果完全一样,因为它们就是同一个规则。 - ⚠ ③ 是另一条路:
sort升序 +reverse翻转,内容也是降序,但它不是「用比较器排」,而且多走一趟O(n)。能一样,但别混为一谈。 - ★★
greater<int>()里的<int>换成别的类型就跟着换:vector<string>排序写greater<string>(),按字典序。
★★ greater<int>() = 降序;不写第三个参数 = 升序。要更复杂的规则(多个关键字、结构体)才需要自己写 cmp。
用到的写法:sort(v.begin(), v.end(), greater<int>()) greater<long long>() / greater<string>()
练习(2 道)
只给题面、样例和一句提示,答案是折叠着的 —— 先自己写一版。
练习 ① 多关键字:一个 cmp 函数就够
给 n 个学生(学号 + 分数),按分数降序输出学号;分数相同则学号小的在前。
★ 这一题是「一个函数当参数」的实战 —— 整道题只需要一个 cmp,而且要自己检查答案对不对。
n。之后 n 行,每行是 学号 分数。输入
6 1 60 2 90 3 70 4 90 5 50 6 80
输出
排好之后(学号 分数): 2 90 4 90 6 80 3 70 1 60 5 50 按题目要求输出学号: 2 4 6 3 1 5 自查:同分的相邻元素,学号是不是递增的 ✓ 全都对
★ 多关键字的套路是固定的:if (第一关键字分得出胜负) return 它的比较; return 第二关键字的比较;⚠ 写完亲手验一遍:同分的人是不是按学号递增的 —— 那是最容易漏的一步。
- 写
cmp:if (a.score != b.score) return a.score > b.score;然后return a.id < b.id; - ★ 第一行的意思是「分得出胜负才返回」 —— 两个人分数一样时这个
if不成立,就掉到第二行去比学号。「相同往下走」是多关键字的全部秘密。 - 交给 sort:
sort(v.begin(), v.end(), cmp); - ⚠ 一定要自查:扫一遍排好后的数组,凡是相邻两个分数相同的地方,检查学号是不是递增的。这一份的代码末尾就做了这件事 —— 养成习惯。
★★ 多关键字 = 几个 if 各管一层,最后一行兜底。★ 「相等就往下一层」是这个套路的唯一关键。
用到的写法:bool cmp(const T& a, const T& b) { return a < b; } sort(v.begin(), v.end(), cmp) is_sorted(v.begin(), v.end(), cmp)
练习 ② 结构体数组 + 三个关键字
给 n 个人(姓名、分数、年级),按分数降序排;分数相同则年级小的在前;分数年级都相同则姓名字典序小的在前。
★ 这一题证明一件事:「加一个关键字」就是「加一行 if」,套路完全不变。
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 自查:分数是不是不增的 ✓ 全都对
★ 三个关键字就是三行,写法一模一样:每一层都是「分得出胜负就 return,分不出就掉到下一层」。⚠ 最后一行的 return 是兜底 —— 走到那里说明前面全相同。
- 参数类型换成结构体:
bool cmp(const Kid& a, const Kid& b),⚠ 那个&别省(省了每次比较都复制一整个结构体)。 - 三层
if:分数 → 年级 → 姓名。和两个关键字时的写法逐字相同。 - 交给 sort:
sort(v.begin(), v.end(), cmp);—— 和上一个例题一模一样。
★★ 会写一个关键字,就会写十个 —— 只是多几行 if,套路一个字不变。
用到的写法:bool cmp(const T& a, const T& b) { return a < b; } sort(v.begin(), v.end(), cmp)
← 上一块:C++ 上路先知道这几句 · 回 STL 专题目录 · 下一块:vector:常用的几种写法 →
⚠ 本页说的是 STL 怎么用。算法本身(Dijkstra、单调栈、状压……)在53 章正课里,而每一章末尾都会告诉你它用到了哪些 STL。