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

前置知识:比较器和「把函数当参数传」

搞清 sort 问的那个问题「a 该排在 b 前面吗」是什么,并学会把一个自己写的函数交给 sort。

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

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

  • 说清「比较器」是什么 —— 它就是一个回答「谁在前」的普通函数
  • 写出来一个 cmp 函数,用 sort(v.begin(), v.end(), cmp) 交给 sort
  • 写出来多关键字的比较器(加一个关键字只要加一行)
  • 知道 greater<int>() 是什么,以及它和你自己写的函数是同一件事

这是什么

6 段

★★ 先回答「比较器是什么」—— 这是 sort 唯一需要你告诉它的事。

★ sort 根本不知道你要按什么排。它只会反复问同一个问题:「a 该排在 b 前面吗?」你回答「是」,它就把 a 放前面。

★ 所以「比较器」不是什么神秘的东西 —— 它就是一个普通函数,参数是两个要比的元素,返回「谁在前」。

所以你只要写这两个函数里的任意一个就够了:bool cmpAsc(int a, int b) { return a < b; }(小的在前) 和 bool cmpDesc(int a, int b) { return a > b; }(大的在前)。⚠ 它们只是差一个符号 —— 下面例题 ① 会把两个都真的跑一遍给你看。

★★★ 写比较器只有一条规矩,别的都可以忘:两个元素「一样」时必须返回 false。

  所以用 < 和 >,绝对不要用 <= 或 >=。

完整动作表

9 条

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

写法干什么代价
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; }   // 大的在前
★ 拿不准的时候,先自己手算两个数,把函数单独调一下看它返回什么 —— 就像下面例题 ① 做的那样。

本书哪一章真的用到它

3 处

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

fp-basics.cpp

写两个比较器 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 再交那个结果。

fp-basics.cpp参考代码
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. 比较器就是一个普通函数:bool cmpAsc(int a, int b) { return a < b; } —— 不用 void,返回 bool。
  2. 交给 sort:sort(a.begin(), a.end(), cmpAsc); —— ⚠ 没有括号。
  3. ★ 自己调它:这样就能看清它到底返回什么 —— cmpAsc(3, 5) 是「3 要排在 5 前面吗」= true;cmpAsc(5, 3) 是 false。拿不准的时候就这样手算一遍,比盯着代码想快得多。
  4. 注意两个函数只是差一个符号:a < b 是小的在前,a > b 是大的在前。
记住这一句

★★ 给别人的时候不写括号,自己叫它的时候写。

用到的写法:bool cmp(const T& a, const T& b) { return a < b; } sort(v.begin(), v.end(), cmp)

例题 ② greater<int>() 就是写好的比较器

fp-greater.cpp

用 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>()。

fp-greater.cpp参考代码
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. ① 用标准库的:sort(a.begin(), a.end(), greater<int>());
  2. ② 用自己写的:sort(b.begin(), b.end(), cmpDesc); —— 两者结果完全一样,因为它们就是同一个规则。
  3. ⚠ ③ 是另一条路:sort 升序 + reverse 翻转,内容也是降序,但它不是「用比较器排」,而且多走一趟 O(n)。能一样,但别混为一谈。
  4. ★★ greater<int>() 里的 <int> 换成别的类型就跟着换:vector<string> 排序写 greater<string>(),按字典序。
记住这一句

★★ greater<int>() = 降序;不写第三个参数 = 升序。要更复杂的规则(多个关键字、结构体)才需要自己写 cmp。

用到的写法:sort(v.begin(), v.end(), greater<int>()) greater<long long>() / greater<string>()

练习(2 道)

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

练习 ① 多关键字:一个 cmp 函数就够

fp-cmp.cpp

给 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 第二关键字的比较;⚠ 写完亲手验一遍:同分的人是不是按学号递增的 —— 那是最容易漏的一步。

fp-cmp.cpp参考答案
这段代码是怎么想出来的
  1. 写 cmp:if (a.score != b.score) return a.score > b.score; 然后 return a.id < b.id;
  2. ★ 第一行的意思是「分得出胜负才返回」 —— 两个人分数一样时这个 if 不成立,就掉到第二行去比学号。「相同往下走」是多关键字的全部秘密。
  3. 交给 sort:sort(v.begin(), v.end(), cmp);
  4. ⚠ 一定要自查:扫一遍排好后的数组,凡是相邻两个分数相同的地方,检查学号是不是递增的。这一份的代码末尾就做了这件事 —— 养成习惯。
记住这一句

★★ 多关键字 = 几个 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)

练习 ② 结构体数组 + 三个关键字

fp-struct.cpp

给 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 是兜底 —— 走到那里说明前面全相同。

fp-struct.cpp参考答案
这段代码是怎么想出来的
  1. 参数类型换成结构体:bool cmp(const Kid& a, const Kid& b),⚠ 那个 & 别省(省了每次比较都复制一整个结构体)。
  2. 三层 if:分数 → 年级 → 姓名。和两个关键字时的写法逐字相同。
  3. 交给 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。