标准库函数中的 set
, map
, priority_queue
等容器都可以保证一定的大小顺序, 并且可以覆盖默认的比较函数实现自定义的排序需求. 在各类博文中, 有大量不同方式自定义排序函数并实现相关需求的方式, 作为一名 c++
小白, 决定将各种方式学习一下, 彻底弄懂自定义比较函数的相关问题.
我们以 set
为例详细说明. set
是c++标准库提供的一个容器, 他提供了一个保序的集合, 集合中的元素不可重复. 具体模板声明如下
template<
class Key,
class Compare = std::less<Key>,
class Allocator = std::allocator<Key>
> class set;
其中 Key
是保存的元素类型; Compare
是比较器的类型, 也是本文讨论的重点; Allocator
是分配器的类型, 相关内容在此不做讨论.
比较器
比较器是 Compare
类型的一个实例(记为 comp
), 并且满足
comp
有一个能接受两个Key
对象作为参数, 并返回(或者可以转换为)bool
类型的函数对象. 注意, 这里并不要求接受的两个Key
对象是传引用还是传值, 也不要求const
.comp
定义了类型Key
上的一个严格弱顺序(strict weak ordering).comp(a, a) = false
对所有的a
成立.- 如果
comp(a, b) = true
, 那么comp(b, a) = false
. (注意, 这个条件反过来不成立, 因为有可能a
和b
相等). - 如果
comp(a, b) = true
且comp(b, c) = true
, 那么comp(a, c) = true
(传递性).
- 定义
equiv(a, b) = !comp(a, b) && !comp(b, a)
, 那么equiv(a, b) = true
对所有的a
成立.- 如果
equiv(a, b) = true
, 那么equiv(b, a) = true
. - 如果
equiv(a, b) = true
且equiv(b, c) = true
, 那么equiv(a, c) = true
.
也就是说, Compare
是比较器的类型, 而比较器是 Compare
类型的一个实例, 并且该实例满足上述条件. 如果比较器满足上面的条件, 那么比较器实际上就定义了一个 Key
上的顺序, 在这个顺序下, 我们就能够进行排序, 并按照顺序进行插入, 删除和去重等 set
上的常规操作. 那么比较器在什么时候传入呢, 自然是在构造函数中.
## 修改排序方式
下面我们看一下 `set` 的常用构造函数
```c++
set(): set(Compare()); // (1)
explicit set(const Compare& comp, const Allocator& alloc = Allocator()); // (2)
template<class InputIt>
set(InputIt first, InputIt last,
const Compare& comp = Compare(),
const Allocator& alloc = Allocator()); // (3)
在第一个构造函数中, 会实际调用第二个构造函数来进行初始化, 也就是使用 Compare
类型的默认构造函数来获得一个比较器. 由于 Compare
的默认值是 std::less<key>
, 所以如果没有用户指定, 那么使用的比较器就是 std::less<Key>
的默认实例(实际上 std::less
也只有默认构造函数), 此时对应的顺序就是 Key
上的小于顺序 (对于内置类型, 就是自然的小于; 对于自定义类型, 需要有对小于号的重载, 并且这个对小于号的重载应该满足比较器的性质, 这点一般都能满足). 因此, 如果需要有把从小到大变成从大到小, 最简单的方式就是将模板中的 less<key>
换成 greater<key>
.
int main() {
set<pair<int, int>> s = {{1, 2}, {1, 3}, {2, 3}, {2, 5}};
for (auto &[a, b] : s) {
cout << a << '\t' << b << endl;
}
return 0;
}
/*输出为:
1 2
1 3
2 3
2 5
*/
int main() {
set<pair<int, int>, greater<pair<int, int>>> s = {{1, 2}, {1, 3}, {2, 3}, {2, 5}};
for (auto &[a, b] : s) {
cout << a << '\t' << b << endl;
}
return 0;
}
/*输出为:
2 5
2 3
1 3
1 2
*/
不过有的时候我们需要对一个已经定义好的类型设置自定义的排序方式, 并且不能去修改或者添加相应的大于小于函数重载, 那么此时就需要运用其他的方法. 通过了解上述原理, 我们对下面的几种方法应该也不难理解了.
第一种方法是定义一个新的仿函数类, 在仿函数类中定义排序函数, 使用该排序函数实现比较器.
# 实现pair<int, int>的自定义排序, 按照第一个元素升序, 第二个元素降序.
struct PairComp {
template <typename T1, typename T2>
bool operator()(const pair<T1, T2> &left, const pair<T1, T2> &right) const {
return left.first < right.first || (
left.first == right.first && left.second > right.second);
}
};
int main() {
set<pair<int, int>, PairComp> s = {{1, 2}, {1, 3}, {2, 3}, {2, 5}};
for (auto &[a, b] : s) {
cout << a << '\t' << b << endl;
}
return 0;
}
/*输出
1 3
1 2
2 5
2 3
*/
第二种方法是自定义一个比较函数, 这样的话, 需要修改模板中的比较器类型, 同时传递比较器的值.
template <typename T1, typename T2>
bool comp(const pair<T1, T2> &left, const pair<T1, T2> &right) {
return (left.first < right.first) || (
left.first == right.first && left.second > right.second);
}
int main() {
set<pair<int, int>, decltype(comp<int, int>)*> s({{1, 2}, {1, 3}, {2, 3}, {2, 5}}, comp);
for (auto &[a, b] : s) {
cout << a << '\t' << b << endl;
}
return 0;
}
/*输出
1 3
1 2
2 5
2 3
*/