C++ 算法面试中的自定义比较器(sort 与 priority_queue)
写了一段代码想把 sort 和 priority_queue 自定义比较器的几种写法串一遍,结果发现自己写的版本里藏了两个坑:一个是命名冲突,一个是比较器语义理解反了。整理一下,方便以后复习。
1. sort 的比较器:四种写法,语义都一样
sort 的比较器回答的问题很直观:cmp(a, b) 返回 true,表示 a 应该排在 b 前面。下面四种写法效果完全一致,都是升序:
|
|
有个坑很多人会踩:比较器必须满足严格弱序(strict weak ordering),不能写
a <= b这种"非严格"比较。原因是sort内部会用!cmp(a,b) && !cmp(b,a)来判断"相等",如果cmp是<=,那么cmp(a,a)恒为 true,直接破坏了这个假设,属于未定义行为(UB)。轻则排序结果不对,重则实际运行直接崩溃或死循环,libstdc++ 开了_GLIBCXX_DEBUG的话会直接断言失败。
2. priority_queue 才是重灾区:比较器语义是反的
priority_queue 底层就是 push_heap/pop_heap 那一套堆操作,比较器回答的不是"排序顺序",而是**“谁的优先级更低”**:cmp(a, b) 返回 true,表示 a 的优先级比 b 低。堆顶永远是"优先级最高"的元素,也就是这个比较器眼里的"最大值"。
所以:
- 默认比较器是
less<T>(a < b)→ 大顶堆,top()是最大值。 - 想要小顶堆,要传
greater<T>(a > b),而不是自己写一个a < b的比较器。
|
|
我原来的代码里,
struct cmp和后面那个 lambda 都写的是return a < b;,直接传给priority_queue当比较器。这跟完全不传比较器的效果一模一样,还是大顶堆——如果本意是想要小顶堆,这两个比较器等于白写了。想用仿函数/lambda 实现小顶堆,比较器里要故意把a、b反过来写成a > b:
1 2 3 4struct MinHeapCmp { bool operator()(const int& a, const int& b) const { return a > b; } }; priority_queue<int, vector<int>, MinHeapCmp> pqMin2;
3. 命名冲突:struct cmp 和 auto cmp 撞车
原代码在全局定义了 struct cmp,又在 main() 里写了 auto cmp = [](...){...};。这两行都能编译过,但纯属侥幸:局部变量 cmp 会遮蔽外层的类型名 cmp,恰好我在声明这个变量之前就用完了 cmp()(构造仿函数),所以没出事。如果把这两段顺序换一下,或者在变量声明之后还想用 cmp 当类型名,编译器会直接报"cmp 不是一个类型"。
写这种对比测试代码时,不要图省事把仿函数和 lambda 都叫 cmp,分开命名(比如 CmpFunctor / cmpLambda),省得自己把自己绕进去。
4. lambda 当 priority_queue 比较器,为什么必须传实例
|
|
decltype(minHeapCmp) 只是拿到了这个 lambda 的类型,但 C++17 及之前,无捕获 lambda 是没有默认构造函数的(C++20 才补上)。如果只写 priority_queue<int, vector<int>, decltype(minHeapCmp)> pqMin3; 不传参数,编译会直接失败。所以必须显式传实例 pqMin3(minHeapCmp),让 priority_queue 内部拿这个实例去初始化它的比较器成员。
仿函数(struct + operator())因为自带默认构造函数,不用管这一茬,这也是 priority_queue 场景里仿函数比 lambda 更常见的原因。
5. 重载 operator< vs 写外部比较器,怎么选
|
|
给 Node 重载了 operator< 之后,sort 和 priority_queue 不传比较器就能直接工作。要不要这么做,看这个"大小关系"是不是类型本身唯一、公认的语义:
- 如果是(比如按时间排序的事件、按权值排序的边),重载
operator<一劳永逸,到处都能用默认比较。 - 如果只是某次算法题里临时需要的排序方式(这次按 x 升序,下次可能要按 y 降序),就用 lambda 或仿函数,不要为了一次性需求去改动类型定义。
小结
sort的比较器好理解:返回 true 表示 a 排 b 前面,四种写法(默认/lambda/greater/仿函数)选哪个纯粹是场景和口味问题。priority_queue的比较器语义是反的:返回 true 表示 a 优先级更低。默认大顶堆,想要小顶堆记得传greater<T>,或者自定义比较器时故意把<写成>——写成a < b只是重新实现了一遍默认行为。priority_queue配合仿函数最省心(自带默认构造),配合 lambda 记得用decltype拿类型、构造时传实例。- 类型自身有固定、唯一的大小关系就重载
operator<;只是某次排序的临时需求就用外部比较器,别为了一次性场景改类型定义。 - 别把仿函数和 lambda 起同一个名字,尤其是想把好几种写法放在一起对比复习的时候。