目录
Q1:为什么priority_queue的比较函数和sort的相反?
前言
大家好!今天我们要聊一个C++中非常强大但初学者常常感到困惑的概念——仿函数(Functor)。特别是它在自定义排序中的应用,比如在priority_queue
、map
和set
等容器中。想要看懂本篇文章需要掌握一定的C++stl库基础!!
什么是仿函数?
仿函数(Functor),也称为函数对象(Function Object),是一个类或结构体,它重载了operator()
,使得这个类的对象可以像函数一样被调用。
基本示例
#include <iostream>
using namespace std;
// 定义一个简单的仿函数
struct Add
{
int operator()(int a, int b) const
{
return a + b;
}
};
int main()
{
Add adder; // 创建仿函数对象
cout << adder(3, 4) << endl; // 像函数一样调用,输出7
// 也可以直接使用匿名对象
cout << Add()(5, 6) << endl; // 输出11
return 0;
}
代码解释:
-
我们定义了一个
Add
结构体,它重载了operator()
。 -
创建
Add
对象后,可以像函数一样使用adder(3, 4)
。 -
也可以直接使用匿名对象
Add()(5, 6)
。
为什么要用仿函数?
你可能会问:"为什么不直接用函数呢?"
仿函数有以下几个优势:
-
可以保存状态:仿函数是对象,可以有成员变量记录状态。
-
可以作为模板参数:这是标准库容器需要自定义排序时的关键点。
-
性能更好:编译器可以更容易内联仿函数的调用。
标准库中的仿函数:greater和less
在C++标准库中,已经预定义了一些常用的仿函数模板,位于<functional>
头文件中。最常用的两个是:
-
less<T>
:比较两个元素是否a < b。
-
greater<T>
:比较两个元素是否a > b。
greater和less的基本使用
#include <iostream>
#include <functional> // 包含greater和less
using namespace std;
int main()
{
// 创建greater和less的仿函数对象
greater<int> greater_comp;
less<int> less_comp;
cout << greater_comp(3, 4) << endl; // 输出0 (false),因为3不大于4
cout << greater_comp(5, 4) << endl; // 输出1 (true),因为5大于4
cout << less_comp(3, 4) << endl; // 输出1 (true),因为3小于4
cout << less_comp(5, 4) << endl; // 输出0 (false),因为5不小于4
return 0;
}
为什么需要greater和less?
这些标准仿函数的主要用途是:
-
作为算法的可配置比较方式
-
作为容器的模板参数来指定排序规则
-
使代码更通用和可配置
priority_queue中的自定义排序
priority_queue
(优先队列)默认是最大堆,即最大的元素在顶部。但有时我们需要最小堆或自定义的排序规则。
在priority_queue比较大小的时候,是通过一个仿函数进行比较的(默认使用的是less),这个仿函数重载了()运算符。
默认的priority_queue
#include <iostream>
#include <queue>
using namespace std;
int main()
{
// 默认是最大堆
priority_queue<int> pq;//底层是用的less
pq.push(3);
pq.push(1);
pq.push(4);
pq.push(2);
while (!pq.empty())
{
cout << pq.top() << " "; // 输出顺序:4 3 2 1
pq.pop();
}
return 0;
}
使用greater实现最小堆
#include <iostream>
#include <queue>
#include <functional> // 需要包含这个头文件使用greater
using namespace std;
int main()
{
// 显示说明用greater<int>作为比较函数,实现最小堆
priority_queue<int, vector<int>, greater<int>> pq;
pq.push(3);
pq.push(1);
pq.push(4);
pq.push(2);
while (!pq.empty())
{
cout << pq.top() << " "; // 输出顺序:1 2 3 4
pq.pop();
}
return 0;
}
代码解释:
-
priority_queue
的模板参数有三个:-
第一个是元素类型
int
-
第二个是底层容器类型
vector<int>
-
第三个是比较函数
greater<int>
-
-
greater<int>
是一个标准库提供的仿函数,它比较两个数,返回a > b。
自定义排序仿函数
假设我们有一个Person
类,想根据年龄建立优先队列,这里由于我们想要比较的对象整体是a与b两个Person结构体,所以不能再直接用默认的a>b比较,就需要自己重新定义仿函数来辅佐比较:
#include <iostream>
#include <queue>
#include <string>
using namespace std;
class Person
{
public:
string name;
int age;
Person(string n, int a) : name(n), age(a)
{}//这是一个默认构造函数
};
// 自定义比较仿函数:按年龄从小到大排序(最小堆)
struct CompareAge
{
bool operator()(const Person& a, const Person& b) const
{
return a.age > b.age; // 注意这里是>,因为我们要最小堆
}
};
int main()
{
// 使用自定义的CompareAge仿函数
priority_queue<Person, vector<Person>, CompareAge> pq;
pq.push(Person("Alice", 30));
pq.push(Person("Bob", 20));
pq.push(Person("Charlie", 25));
while (!pq.empty())
{
auto p = pq.top();
cout << p.name << " (" << p.age << ")" << endl;
pq.pop();
}
/* 输出:
Bob (20)
Charlie (25)
Alice (30)
*/
return 0;
}
关键点:
-
我们定义了一个
CompareAge
仿函数,重载了operator()
。 -
注意比较逻辑是
a.age > b.age
,这看起来是"大于",但实际上是为了实现最小堆。-
priority_queue
默认是最大堆,它会将"较小"的元素放在后面。 -
所以如果我们想让年龄小的在前面,需要定义"年龄大的更小"。
-
有些同学可能会注意到:为什么greater你使用的时候用例greater<int>,但是自己定义的仿函数只需要传一个结构体名字呢?
1. 标准库仿函数(greater/less)是模板类
标准库中的greater
和less
是类模板,使用时需要指定模板参数:
template <class T>
struct greater
{
bool operator()(const T& x, const T& y) const
{
return x > y;
}
};
所以使用时必须实例化:
greater<int> // 创建一个专门比较int类型的greater实例
2. 自定义仿函数通常是具体类型
当我们自己定义仿函数时,通常直接定义一个具体的结构体/类:
struct MyComparator
{
bool operator()(int a, int b) const
{
return a > b;
}
};
这里MyComparator
已经是一个完整的类型,不需要模板参数。
map和set中的自定义排序
map
和set
默认是按键值升序排列的,但有时我们需要降序或自定义排序规则。
默认的map
#include <iostream>
#include <map>
using namespace std;
int main()
{
map<int, string> m;
m[3] = "three";
m[1] = "one";
m[4] = "four";
m[2] = "two";
for (auto& p : m)
{
cout << p.first << ": " << p.second << endl;
}
/* 输出:
1: one
2: two
3: three
4: four
*/
return 0;
}
使用greater实现降序
#include <iostream>
#include <map>
#include <functional> // 需要这个头文件使用greater
using namespace std;
int main()
{
// 使用greater<int>作为比较函数,实现降序排列
map<int, string, greater<int>> m;
m[3] = "three";
m[1] = "one";
m[4] = "four";
m[2] = "two";
for (auto& p : m)
{
cout << p.first << ": " << p.second << endl;
}
/* 输出:
4: four
3: three
2: two
1: one
*/
return 0;
}
自定义排序仿函数
假设我们想按字符串长度排序,也需要自己定义一个仿函数:
#include <iostream>
#include <map>
#include <string>
using namespace std;
// 自定义比较仿函数:按字符串长度排序
struct CompareLength
{
bool operator()(const string& a, const string& b) const
{
if(a.length() != b.length())
{
return a.length() < b.length(); // 长度短的在前
}
return a < b; // 长度相同则按字典序
}
};
int main()
{
// 使用自定义的CompareLength仿函数
map<string, int, CompareLength> m;
m["apple"] = 1;
m["banana"] = 2;
m["cherry"] = 3;
m["date"] = 4;
m["fig"] = 5;
for (auto& p : m)
{
cout << p.first << ": " << p.second << endl;
}
/* 输出:
fig: 5
date: 4
apple: 1
banana: 2
cherry: 3
*/
return 0;
}
代码解释:
-
我们定义了
CompareLength
仿函数,比较字符串长度。 -
如果长度相同,再按字典序比较。
-
这样
map
就会按照字符串长度从小到大排列。
更复杂的自定义排序示例
让我们看一个更复杂的例子,假设我们有一个Student
类,想按多个条件排序:
#include <iostream>
#include <set>
#include <string>
using namespace std;
class Student
{
public:
string name;
int score;
int age;
Student(string n, int s, int a) : name(n), score(s), age(a)
{}
};
// 自定义比较仿函数:先按分数降序,分数相同按年龄升序,都相同按名字字典序
struct CompareStudent
{
bool operator()(const Student& a, const Student& b) const
{
if (a.score != b.score)
{
return a.score > b.score; // 分数高的在前
}
if (a.age != b.age)
{
return a.age < b.age; // 年龄小的在前
}
return a.name < b.name; // 名字字典序小的在前
}
};
int main()
{
// 使用自定义的CompareStudent仿函数
set<Student, CompareStudent> s;
s.insert(Student("Alice", 90, 20));
s.insert(Student("Bob", 85, 21));
s.insert(Student("Charlie", 90, 19));
s.insert(Student("David", 85, 20));
s.insert(Student("Eve", 90, 20));
for (auto& student : s)
{
cout << student.name << " (Score: " << student.score
<< ", Age: " << student.age << ")" << endl;
}
/* 输出:
Charlie (Score: 90, Age: 19)
Alice (Score: 90, Age: 20)
Eve (Score: 90, Age: 20)
David (Score: 85, Age: 20)
Bob (Score: 85, Age: 21)
*/
return 0;
}
关键点:
-
我们实现了多级排序:先按分数降序,然后按年龄升序,最后按名字字典序。
-
注意比较逻辑的层次结构,确保所有可能的比较情况都被覆盖。
-
在
set
中使用自定义排序时,必须保证比较是严格弱序的:-
反自反性:
comp(a, a)
必须为false -
不对称性:如果
comp(a, b)
为true,则comp(b, a)
必须为false -
传递性:如果
comp(a, b)
和comp(b, c)
为true,则comp(a, c)
必须为true
-
lambda表达式作为仿函数
(未学过lambda可跳过)
C++11引入了lambda表达式,它也可以作为仿函数使用:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6};
// 使用lambda表达式作为比较函数
auto cmp = [](int a, int b) { return a > b; }; // 降序排序
sort(nums.begin(), nums.end(), cmp);
for (int num : nums)
{
cout << num << " ";
}
// 输出:9 6 5 4 3 2 1 1
return 0;
}
注意:
-
lambda表达式可以方便地创建简单的仿函数。
-
但对于
priority_queue
、map
、set
等需要在模板参数中指定比较函数的容器,不能直接使用lambda表达式(因为lambda表达式有独特的类型,不能作为模板参数)。 -
这种情况下,还是需要使用结构体/类定义的仿函数。
常见问题解答
Q1:为什么priority_queue的比较函数和sort的相反?
这是一个常见的困惑点。priority_queue
默认是最大堆,它会将"较小"的元素放在后面。所以:
-
如果你想让小的元素先出队(最小堆),需要定义"大的更小"(即
a > b
)。 -
而在
sort
中,a > b
表示降序排列。
可以这样理解:priority_queue
的比较函数定义的是"优先级",返回true表示第一个参数的优先级低于第二个参数。
Q2:如何选择使用仿函数还是lambda表达式?
-
如果比较逻辑简单且只在一个地方使用,lambda表达式更方便。
-
如果比较逻辑复杂或需要在多个地方重用,定义仿函数更好。
-
对于需要在模板参数中指定的比较函数(如
priority_queue
、map
、set
),必须使用仿函数。
Q3:自定义比较函数时有哪些陷阱?
-
没有实现严格弱序:这会导致未定义行为。
-
修改了被比较的元素:比较函数应该是"纯"的,不修改被比较对象。
-
性能问题:如果比较函数很复杂,可能会影响容器操作的性能。
总结
-
仿函数是重载了
operator()
的类或结构体,可以像函数一样调用。 -
在
priority_queue
、map
、set
等容器中,可以通过仿函数实现自定义排序。 -
priority_queue
的比较逻辑与直觉可能相反,需要特别注意。 -
多级排序时,确保比较函数覆盖所有情况并满足严格弱序要求。
-
lambda表达式适合简单的一次性比较,仿函数适合复杂的或重用的比较逻辑。
深呼吸,头晕是正常的,大家初次接触这个东西肯定会有迷惑,但是仿函数在我们做一些算法题时格外有用,通过自定义的比较方法,来快速进行排序查找,大家平时可以多试着去使用仿函数,这样才能在实践中用出来!
希望这篇博客能帮助你理解C++中的仿函数和自定义排序!如果有任何问题,欢迎在评论区留言讨论。