深入理解C++中的仿函数:从priority_queue到map/set的自定义排序

目录

前言

什么是仿函数?

        基本示例

        为什么要用仿函数?

标准库中的仿函数:greater和less

        greater和less的基本使用

        为什么需要greater和less?

priority_queue中的自定义排序

默认的priority_queue

使用greater实现最小堆

自定义排序仿函数

map和set中的自定义排序

默认的map

使用greater实现降序

自定义排序仿函数

更复杂的自定义排序示例

lambda表达式作为仿函数

常见问题解答

Q1:为什么priority_queue的比较函数和sort的相反?

Q2:如何选择使用仿函数还是lambda表达式?

Q3:自定义比较函数时有哪些陷阱?

总结


前言

大家好!今天我们要聊一个C++中非常强大但初学者常常感到困惑的概念——仿函数(Functor)。特别是它在自定义排序中的应用,比如在priority_queuemapset等容器中。想要看懂本篇文章需要掌握一定的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;
}

代码解释:

  1. 我们定义了一个Add结构体,它重载了operator()

  2. 创建Add对象后,可以像函数一样使用adder(3, 4)

  3. 也可以直接使用匿名对象Add()(5, 6)

为什么要用仿函数?

你可能会问:"为什么不直接用函数呢?"

仿函数有以下几个优势:

  1. 可以保存状态:仿函数是对象,可以有成员变量记录状态。

  2. 可以作为模板参数:这是标准库容器需要自定义排序时的关键点。

  3. 性能更好:编译器可以更容易内联仿函数的调用。

标准库中的仿函数:greater和less

在C++标准库中,已经预定义了一些常用的仿函数模板,位于<functional>头文件中。最常用的两个是:

  1. less<T>:比较两个元素是否a < b。

  2. 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?

这些标准仿函数的主要用途是:

  1. 作为算法的可配置比较方式

  2. 作为容器的模板参数来指定排序规则

  3. 使代码更通用和可配置

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;
}

代码解释:

  1. priority_queue的模板参数有三个:

    • 第一个是元素类型int

    • 第二个是底层容器类型vector<int>

    • 第三个是比较函数greater<int>

  2. 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;
}

关键点:

  1. 我们定义了一个CompareAge仿函数,重载了operator()

  2. 注意比较逻辑是a.age > b.age,这看起来是"大于",但实际上是为了实现最小堆。

    • priority_queue默认是最大堆,它会将"较小"的元素放在后面。

    • 所以如果我们想让年龄小的在前面,需要定义"年龄大的更小"。

有些同学可能会注意到:为什么greater你使用的时候用例greater<int>,但是自己定义的仿函数只需要传一个结构体名字呢?

1. 标准库仿函数(greater/less)是模板类

标准库中的greaterless类模板,使用时需要指定模板参数:

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中的自定义排序

mapset默认是按键值升序排列的,但有时我们需要降序或自定义排序规则。

默认的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;
}

代码解释:

  1. 我们定义了CompareLength仿函数,比较字符串长度。

  2. 如果长度相同,再按字典序比较。

  3. 这样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;
}

关键点: 

  1. 我们实现了多级排序:先按分数降序,然后按年龄升序,最后按名字字典序。

  2. 注意比较逻辑的层次结构,确保所有可能的比较情况都被覆盖。

  3. 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;
}

注意:

  1. lambda表达式可以方便地创建简单的仿函数。

  2. 但对于priority_queuemapset等需要在模板参数中指定比较函数的容器,不能直接使用lambda表达式(因为lambda表达式有独特的类型,不能作为模板参数)。

  3. 这种情况下,还是需要使用结构体/类定义的仿函数。

常见问题解答

Q1:为什么priority_queue的比较函数和sort的相反?

这是一个常见的困惑点。priority_queue默认是最大堆,它会将"较小"的元素放在后面。所以:

  • 如果你想让小的元素先出队(最小堆),需要定义"大的更小"(即a > b)。

  • 而在sort中,a > b表示降序排列。

可以这样理解:priority_queue的比较函数定义的是"优先级",返回true表示第一个参数的优先级低于第二个参数。

Q2:如何选择使用仿函数还是lambda表达式?

  • 如果比较逻辑简单且只在一个地方使用,lambda表达式更方便。

  • 如果比较逻辑复杂或需要在多个地方重用,定义仿函数更好。

  • 对于需要在模板参数中指定的比较函数(如priority_queuemapset),必须使用仿函数。

Q3:自定义比较函数时有哪些陷阱?

  1. 没有实现严格弱序:这会导致未定义行为。

  2. 修改了被比较的元素:比较函数应该是"纯"的,不修改被比较对象。

  3. 性能问题:如果比较函数很复杂,可能会影响容器操作的性能。

总结

  1. 仿函数是重载了operator()的类或结构体,可以像函数一样调用。

  2. priority_queuemapset等容器中,可以通过仿函数实现自定义排序。

  3. priority_queue的比较逻辑与直觉可能相反,需要特别注意。

  4. 多级排序时,确保比较函数覆盖所有情况并满足严格弱序要求。

  5. lambda表达式适合简单的一次性比较,仿函数适合复杂的或重用的比较逻辑。

深呼吸,头晕是正常的,大家初次接触这个东西肯定会有迷惑,但是仿函数在我们做一些算法题时格外有用,通过自定义的比较方法,来快速进行排序查找,大家平时可以多试着去使用仿函数,这样才能在实践中用出来!

希望这篇博客能帮助你理解C++中的仿函数和自定义排序!如果有任何问题,欢迎在评论区留言讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

渡我白衣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值