• 【C++】详解priority_queue(优先级队列)与函数对象


    目录

    一、priority_queue 的介绍和使用

    1.1priority_queue 的介绍

    2.2priority_queue 的使用

    二、仿函数

    2.1什么是仿函数

    2.2仿函数的作用

    三、函数对象的特点(知识点多)

    3.1分析特点5(比较普通函数与函数对象)

    3.1.1利用普通函数传递参数

    拓展之:深度剖析函数利用模板的本质

    3.1.2利用函数对象传递参数

    3.1.3函数对象作为for_each的参数(知识点较多)

    2.第三个参数传递函数:(计算从0到100)

    3.第三个参数传递函数对象:(计算从0到100)

    4.难点:关于第三个参数是传值的易错点

    5.拓展:如果我重写for_each,加上引用,会不会得到我想要的效果?

    3.2分析特点6(一共俩句话)

    3.2.1分析保持函数的调用状态(本质是因为成员变量不会销毁)

    3.2.2参数个数可变是什么意思?代码分析过程中居然用到了匿名对象!

    1.分析函数对象的用法及匿名对象

    2.探究匿名对象的生命周期

    3.回忆匿名对象拆分开怎么写

    4.讨论多参数问题(多参数不是重载函数多,而是类中多变量)

    四、函数对象的分类

    4.1一元函数

    4.2二元函数

    4.3一元判定函数

    4.4二元判定函数 

    五、总结知识点(未完待续.....)

    一、priority_queue 的介绍和使用

    1.1priority_queue 的介绍

    priority_queue (优先级队列)是一种容器适配器,它与 queue 共用一个头文件,其底层结构是一个堆,并且默认情况下是一个大根堆,所以它的第一个元素总是它所包含的元素中最大的,并且为了不破坏堆结构,它也不支持迭代器。

    同时,由于堆需要进行下标计算,所以 priority_queue 使用 vector 作为它的默认适配容器 (支持随机访问):

     但是,priority_queue 比 queue 和 stack 多了一个模板参数 – 仿函数;关于仿函数的具体细节,我们将在后文介绍

    class Compare = less<typename Container::value_type>
    

    2.2priority_queue 的使用

    优先级队列默认使用 vector 作为其底层存储数据的容器,在 vector 上又使用了堆算法将 vector 中元素构造成堆的结构,因此 priority_queue 就是堆,所有需要用到堆的位置,都可以考虑使用 priority_queue。(注意:默认情况下priority_queue是大堆)

    priority_queue 的使用文档

    函数声明接口说明
    priority_queue()构造一个空的优先级队列
    priority_queue(first, last)迭代器区间构造优先级队列
    empty( )检测优先级队列是否为空
    top( )返回优先级队列中最大(最小元素),即堆顶元素
    push(x)在优先级队列中插入元素x
    pop()删除优先级队列中最大(最小)元素,即堆顶元素
    size()返回优先级队列中的元素个数

    注意事项:

    priority_queue 默认使用的仿函数是 less,所以默认建成的堆是大堆;如果我们想要建小堆,则需要指定仿函数为 greater,该仿函数包含在头文件 functional 中,并且由于仿函数是第三个缺省模板参数,所以如果要传递它必须先传递第二个模板参数即适配容器

    1. void test_priority_queue() {
    2. priority_queue<int> pq; //默认建大堆,仿函数为less
    3. pq.push(5);
    4. pq.push(2);
    5. pq.push(4);
    6. pq.push(1);
    7. pq.push(3);
    8. while (!pq.empty()) {
    9. cout << pq.top() << " ";
    10. pq.pop();
    11. }
    12. cout << endl;
    13. priority_queue<int, vector<int>, greater<int>> pq1; //建小堆,仿函数为greater,需要显式指定
    14. pq1.push(5);
    15. pq1.push(2);
    16. pq1.push(4);
    17. pq1.push(1);
    18. pq1.push(3);
    19. while (!pq1.empty()) {
    20. cout << pq1.top() << " ";
    21. pq1.pop();
    22. }
    23. cout << endl;
    24. }


    二、仿函数

    2.1什么是仿函数

    仿函数也叫函数对象仿函数是一个类,但是该类必须重载函数调用运算符 ( ),即 operator()(参数);由于这样的类的对象可以像函数一样去使用,所以我们将其称为仿函数/函数对象,如下:

    1. namespace lzy {
    2. class less {
    3. public:
    4. bool operator()(const int& x, const int& y) const
    5. {
    6. cout << "less类" << endl;
    7. return x < y;
    8. }
    9. };
    10. class greater
    11. {
    12. public:
    13. bool operator()(const int& x, const int& y)
    14. {
    15. cout << "greater类" << endl;
    16. return x > y;
    17. }
    18. };
    19. }
    20. void functor_test() {
    21. lzy::less lessFunc;
    22. cout << lessFunc(1, 2) << endl; //lessFunc.operator(1,2)
    23. lzy::greater greaterFunc;
    24. cout << greaterFunc(1, 2) << endl; //greaterFunc.operator(1,2)
    25. }

    可以看到,因为 less 类和 greater 类重载了 () 操作符,所以类对象可以像函数一样去使用,因此它们被称为仿函数。

    2.2仿函数的作用

    我们以最简单的冒泡排序为例来说明仿函数的作用,我们知道,排序分为排升序和排降序,那么在没有仿函数的时候,即C语言阶段,我们是如何来解决这个问题的呢 – 答案是函数指针

    将排序函数的最后一个参数定义为函数指针,然后通过用户给排序函数传递不同的比较函数来决定升序还是降序:

      

    但是!!在 C++ 中,我们不再使用函数指针解决升序降序的问题,转而使用更加方便的仿函数


    三、函数对象的特点(知识点多)

    • 函数对象是一个类的对象,而不是函数

    • 函数对象是重载了operator()的类对象

    • 函数对象的调用格式类似于函数调用,所以也称仿函数

    • 函数对象也具有参数和返回值

    • 函数对象作为一个对象,可以作为参数传递给函数或其它类函数对象独有的特点

    • 保持函数的调用状态,参数个数可变(因为函数在栈中,之后会销毁)

    3.1分析特点5(比较普通函数与函数对象)

    3.1.1利用普通函数传递参数

    代码作用:给定成绩数组,找出大于或者小于基准值的成绩分数

    1. #include
    2. using namespace std;
    3. int baseScore = 0;
    4. int arrSize = 5;
    5. typedef bool (*pf) (int,int); //函数指针,是为了代码将来的可维护性 不需要修改count函数内部
    6. int count(int grade[],pf p)//传递int int形的接口
    7. {
    8. int sum=0;
    9. for(int i=0;i
    10. {
    11. if(p(baseScore,grade[i]))//难点
    12. {
    13. ++sum;
    14. }
    15. }
    16. return sum;
    17. }
    18. bool Cmp_big(int base,int num)// 比较函数
    19. {
    20. return num>=base;// 也可以返回整数值,设置各种比较情况
    21. }
    22. bool Cmp_small(int base,int num)// 修改比较函数
    23. {
    24. return num<=base;// 也可以返回整数值,设置各种比较情况
    25. }
    26. int main()
    27. {
    28. int grade[arrSize]={1,2,3,4,5};
    29. cout << count(grade,Cmp_big) << endl;
    30. return 0;
    31. }

    这里利用到了函数指针的知识点,但在这里我不想过多探讨,所以看注释就好啦

    这里通过传递不同的函数比较方法,可以有不同的效果,p是一个函数指针,而函数名正好也是函数指针,所以这里重点正是cmp_big给了p,使得p变成了大于基准值比较函数(sort的第三个参数,实则正可以理解为函数指针(C语言中),在C++的话一定就是模板了)

    拓展之:深度剖析函数利用模板的本质

    但是在看了sort函数的底层时,我发现大部分还是用模板(谁还用函数指针啊真是)(但是C没模板,那么C一定就得用函数指针当sort函数的第三个参数了)

    这样用着蛮不错的哦,但是如果我们的函数要实例化的话,你就会惊奇的发现:

     这个东西长得不就是我们的函数指针吗?这也充分验证了我们函数名就是函数指针的观点!

    模板的好处正是用起来爽,但其实并没有提高效率

    3.1.2利用函数对象传递参数

    1. #include
    2. using namespace std;
    3. int baseScore = 0;
    4. int arrSize = 5;
    5. //方法二 利用函数对象
    6. class cmp_obj
    7. {
    8. public:
    9. int base;
    10. bool operator()(int score) const
    11. {
    12. return (score>=base);
    13. }
    14. };
    15. int count(int grade[],cmp_obj cmp)//grade[i],cmp1 cmp1(grade[i]) cmp1.oprara(grade[i]);
    16. {
    17. int sum=0;
    18. for(int i=0;i
    19. {
    20. if(cmp(grade[i])) //cmp.operator(grade[i]);
    21. {
    22. ++sum;
    23. }
    24. }
    25. return sum;
    26. }
    27. int main()
    28. {
    29. int grade[arrSize]={1,2,3,4,5};
    30. cmp_obj cmp1;
    31. cmp1.base=2;
    32. cout << count(grade,cmp1) << endl;
    33. cmp1.base=4;//修改基准值
    34. cout << count(grade,cmp1) << endl;
    35. return 0;
    36. }

    分析代码:

    这里没有比较函数,有的只是对象和重载operator( ),所以要修改比较规则,那么重载里面修改就好了

     这正是函数对象作为一个对象,可以作为参数传递给函数或其它类函数对象独有的特点

    利用函数的话,修改基准值得用全局变量,但是利用函数对象的话就不需要,这都是优势! 

    3.1.3函数对象作为for_each的参数(知识点较多)

    1.for_each函数:

    1. template<class InputIterator, class Function>
    2. Function for_each(InputIterator first, InputIterator last, Function fn)
    3. //第三个参数是函数或者是函数对象
    4. {
    5. while (first!=last) {
    6. fn (*first);//取值 传递到fn函数/函数对象
    7. ++first;
    8. }
    9. return fn; //如果是函数对象 返回是有意义的 如果是函数 反而是没有意义的
    10. }

    这里重点知识点就是:for_each的第三个参数可以传递函数对象/函数

    传递函数时:往往不需要接收返回值,因为接收函数没有意义

    传递函数对象:一定得接收返回值,因为不接受的话因为是传值,就会丢失想要的值

    2.第三个参数传递函数:(计算从0到100)
    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. int sum=0;
    6. void f(int x)
    7. {
    8. sum+=x;//这里是f函数
    9. }
    10. int main()
    11. {
    12. vector<int> v;
    13. for(int i=0;i<100;i++)
    14. {
    15. v.push_back(i);
    16. }
    17. //不需要接收返回值 接收函数没有意义
    18. for_each(v.begin(),v.end(),f);//在迭代器的范围内,每次取一个x元素,计算f(x)
    19. // for(int i=0;i
    20. // {
    21. // f(v[i]);
    22. // }
    23. cout<< sum;
    24. }

    如果我们将for_each拆分开来:

    1. for(int i=0;isize();++i)
    2. {
    3. f(v[i]);
    4. }

     很明显,用for_each是更爽的! 只可惜有一定的使用难度成本,让我们继续往下看!

    3.第三个参数传递函数对象:(计算从0到100)
    1. //print为仿函数
    2. class print
    3. {
    4. int count;
    5. public:
    6. int get_count()
    7. {
    8. return count;
    9. }
    10. print() {count = 0;} //必须写默认构造 要不然下面匿名对象会出问题
    11. void operator()(int x)
    12. {
    13. cout << x << " ";
    14. ++count;
    15. }
    16. };

    在这里仿函数类中,我们用get_count函数接收私有变量,并且写好了默认构造函数,接着重载了operator( ) ,因为这个我们是用来输出的,所以返回值写成void就好了

    1. int main()
    2. {
    3. list<int> l;//初始化
    4. for (size_t i= 1;i<10;++i)
    5. {
    6. l.push_back(i);
    7. }
    8. //遍历ilist元素并打印
    9. print p = for_each(l.begin(),l.end(),print());//打印l元素个数 这里的第三个参数好好分析一下
    10. cout <<"count="<get_count() << endl;
    11. return 0;
    12. }

    这里我们先对list进行了初始化,接着难点来了:

     利用构造函数创建匿名对象与对象调用重载()函数是很容易混淆的!!!

     接着由于返回值是函数对象,所以可以利用p来验证最后的结果~

    4.难点:关于第三个参数是传值的易错点

    但是如果我不接受返回值,直接输出p1.get_count( ) ,你会不会汗流浃背呢?

     这就说明,我们这里进去的值出来后就被销毁了,为了避免销毁,得加上返回值哦!

    5.拓展:如果我重写for_each,加上引用,会不会得到我想要的效果?

    1. Function& for_each(InputIterator first, InputIterator last, Function& fn)
    2. {
    3. while (first != last) {
    4. fn(*first); // 取值并传递到fn函数/函数对象
    5. ++first;
    6. }
    7. return fn;
    8. }

    注意:类对象引用初始化的时候,一定得加上引用的是谁,类类型也是可以用引用的 


    3.2分析特点6(一共俩句话)

    3.2.1分析保持函数的调用状态(本质是因为成员变量不会销毁)

    保留函数的调用状态是什么意思呢? 我们看一段代码!

    1. int sum = 0;
    2. void f(int x)
    3. {
    4. sum += x;
    5. }
    6. class add
    7. {
    8. int comm;
    9. public:
    10. int Getsum() { return comm; }
    11. add() { comm = 0 ;}
    12. void operator()(int x)
    13. {
    14. comm += x;
    15. }
    16. };
    17. int main()
    18. {
    19. int i = 0;
    20. for ( i = 1; i <= 100; i++)
    21. f(i);
    22. cout << sum << endl; //5050
    23. add fobi;
    24. for (i = 1;i <= 100;i++)
    25. fobi(i); //调用格式类似于函数
    26. cout << fobi.Getsum ()<< endl; //5050,保留函数状态
    27. return 0;
    28. }

    这段代码的知识点很多咧~ 让小羊博主一起带你们分析一波!

    直观上来说,一个是普通函数,一个是重载(),那么保留函数的调用状态是什么意思?

    1.本质上是因为类中的成员变量不会被销毁,但是函数中的sum与x出了作用域就会清空

    2.在这俩个函数都是void的情况,为了输出最终结果,俩方势力也是煞费脑筋:

    函数势力是利用了全局变量 int sum=0; 才使得得到想要结果(可惜这样是不安全的)

    而重载势力是利用了共有函数接口取回最终的结果值

     而且看到这个写法会不会觉得很奇怪咧,有俩个括号,意思是第一个是缺省参数吗?

    不不不不,这里是重载()函数,所以长得有些奇怪,只有第二个int x才是参数~


    3.2.2参数个数可变是什么意思?代码分析过程中居然用到了匿名对象!

    1.分析函数对象的用法及匿名对象
    1. #include
    2. using namespace std;
    3. class add_two
    4. {
    5. int comm;
    6. //int hehe;
    7. public:
    8. int Getsum() { return comm; }
    9. add_two()
    10. { comm = 0;}
    11. add_two(int v) { comm = v;}
    12. int operator()(int x) //不是说这个是特殊语法 是因为operator*(int x) 等等重载函数都是这么写的
    13. {
    14. return (comm + x );
    15. }
    16. };
    17. void test1()
    18. {
    19. int ret = add_two()(2); //利用匿名对象
    20. cout << ret << endl;// ret=2;
    21. ret = add_two(5)(2);//ret=7;
    22. cout << ret << endl;
    23. ret = add_two(6)(2);//ret=8;
    24. cout << ret << endl;
    25. }
    26. int main()
    27. {
    28. test1();
    29. return 0;
    30. }

    代码分析:

    还是老样子,在类中依然重载了operator( ) , 所以让我们看看主函数是怎么调用的吧

    类中有俩个构造函数,一个是有参,一个是无参(并且给了默认值是0)

    (吐槽:直接给缺省不就行了,还写俩个构造函数)

    add_two():虽然是俩个括号 但是第一个是构造函数 然后用了匿名对象的知识点!!!!这部分创建了一个 add_two 的匿名对象(生命周期只有一行),使用了默认构造函数,该构造函数将 comm 成员初始化为 0这个对象实际上是一个函数对象(因为我们重载了operator(),所以对象后面跟上括号,自然而然就会去调用重载函数去了),因为我们可以调用它

    接下来,像调用函数一样传递了一个整数参数 2 给这个匿名对象由于 add_two 类重载了 operator(),因此可以将它视为函数调用。

     int ret = ...:最后,将这个函数对象调用的结果(在这种情况下是 0 + 2 = 2)赋值给整数变量 ret。

    所以,int ret = add_two()(2); 的含义是创建一个 add_two 的匿名对象,然后将其当做函数调用,将参数 2 传递给它,最后将结果 2 赋给 ret。这展示了函数对象的使用,允许像调用函数一样使用类的实例。

    但如果想充分发挥函数对象的作用的话,其实我们可以直接输出:

    2.探究匿名对象的生命周期
    1. //讲解匿名对象
    2. class MyClass {
    3. public:
    4. MyClass() {
    5. std::cout << "MyClass constructor" << std::endl;
    6. }
    7. ~MyClass() {
    8. std::cout << "MyClass destructor" << std::endl;
    9. }
    10. };
    11. int main() {
    12. cout << "flag1" << endl;
    13. MyClass(); // 创建匿名对象 匿名对象的生命周期只有这一行
    14. cout << "flag2" << endl;
    15. return 0;
    16. }

    匿名对象的生命周期只有创建的那一行,走了之后就会被销毁掉! 

    3.回忆匿名对象拆分开怎么写

    好了,我们了解完匿名对象的知识点后,让我们来回忆一下上面那个例子拆分开来是什么效果 

    1. //int tmp=add_two(100);
    2. add_two addFunction(100); // 创建一个 add_two 对象
    3. ret = addFunction(2); // 调用对象的 operator(),传递参数 2
    4. cout << ret << endl;

    4.讨论多参数问题(多参数不是重载函数多,而是类中多变量)

    1. #include
    2. // 基本情况:当没有参数时,返回0
    3. int sum() {
    4. return 0;
    5. }
    6. // 使用可变参数模板来计算总和
    7. template <typename T, typename... Args>
    8. T sum(T first, Args... rest) {
    9. return first + sum(rest...);
    10. }
    11. int main() {
    12. int result = sum(1, 2, 3, 4, 5);
    13. std::cout << "Sum: " << result << std::endl;
    14. double result2 = sum(1.1, 2.2, 3.3, 4.4);
    15. std::cout << "Sum: " << result2 << std::endl;
    16. return 0;
    17. }


    四、函数对象的分类

    函数对象是重载了operator() 的类的一个实例,operator()是函数调用运算符。

    标准C++库根据 operator() 参数个数为0个,1个,2个加以划分的。要有以下3种类型:

    • 发生器:一种没有参数且返回一个任意类型值的函数对象,例如随机数发生器。

    • 一元函数:一种只有一个任意类型的参数,且返回-个可能不同类型值的函数对象。

    • 二元函数:一种有两个任意类型的参数,且返回一个任意类型值的函数对象。

    • 一元判定函数: 返回bool型值的一元函数

    • 二元判定函数: 返回bool型值的二元函数

    4.1一元函数

    1. templat<typename _A,typename _R>
    2. class unary_function
    3. {
    4. typedef _A argument_type;
    5.   typedef _R result_type;      
    6. };

    有俩个模板参数 第一个是输出参数 参数类型 第二个模板参数定义为结果类型 返回类型 动态特性非常强

    p62 即使类中从来没有用过 但是还是可以定义用 (意思是,这个基类是在上面的类继承下来的,所以说它的arg也可以当成输出参数,即使我们的基类没有写)

    P62页代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. template<typename _A, typename _R>
    7. struct unary {
    8. typedef _A argument_type;
    9. typedef _R result_type;
    10. };
    11. template<typename _inPara, typename _outPara>//邸老师说 希望这里删去
    12. class CSum : public unary<_inPara, _outPara> {
    13. public:
    14. _outPara sum;//这里可以使用 result_type;
    15. CSum() { sum = 0; }
    16. void operator()(_inPara n) { sum += n; } //这里同理也可以用 argument_type;
    17. _outPara GetSum() { return sum; }
    18. };
    19. template<typename _input>
    20. class CPrint
    21. {
    22. public://这个记得加
    23. CPrint(){};
    24. void operator()(_input var)
    25. {
    26. cout << var << " ";
    27. }
    28. };
    29. int main()
    30. {
    31. //传int
    32. vector<int> v;
    33. for (int i = 1;i <= 5;i++) { v.push_back(i); }
    34. //遍历int
    35. vector<int>::iterator it=v.begin();
    36. for(;it!=v.end();++it)
    37. {
    38. cout << *it << " ";//这样遍历 可以防止出界 比那个[]遍历更安全
    39. }cout << endl;
    40. CSum<int, int> sobj = for_each(v.begin(), v.end(), CSum<int, int>());//类的显示实例化+匿名对象
    41. cout << "sum(int)=" << sobj.GetSum() << endl;
    42. //传float
    43. vector<float> v2;
    44. float f = 1.3f;
    45. for (int i = 1;i <= 5;i++) {
    46. v2.push_back(f);
    47. f += 1.0f;
    48. }
    49. //遍历float
    50. vector<float>::iterator it1=v2.begin();
    51. for(;it1!=v2.end();++it1)
    52. {
    53. cout << *it1 << " ";//这样遍历 可以防止出界 比那个[]遍历更安全
    54. }cout << endl;
    55. CSum<float, float> sobj2 = for_each(v2.begin(), v2.end(), CSum<float, float>());
    56. cout << "sum(float)=" << sobj2.GetSum() << endl;
    57. //利用CPrint函数对象 与 for_each代替迭代器输出
    58. vector<int>v3={1,2,3,4,5};
    59. for_each(v3.begin(), v3.end(), CPrint<int>());
    60. cout << endl;
    61. //同理
    62. vector<float>v4={1.1,2.1,3.1,4.1,5.1};
    63. for_each(v4.begin(), v4.end(), CPrint<float>());
    64. //lambad表达式
    65. return 0;
    66. }

    4.2二元函数

    利用二元函数使得学生成绩升序 

    这个基类只定义了一个变量的名字,其实也没有什么东西,以下程序自定义的类得以这个为基类,派生

    很多时候都要重载与之对应的操作符 模板函数相当于框架,操作符重载相当于调用的接口 

    4.3一元判定函数

    4.4二元判定函数 

    五、总结知识点(未完待续.....)

    一个类中重载了(),那么这个类创造的对象就叫做函数对象!!!同时这个类被叫做仿函数

    再用起来的时候,函数对象就是函数名!!!!(底层不是)

  • 相关阅读:
    SBOM落地的关键一步——漏洞可利用性交流(VEX)
    SpringBoot集成kubernetes-client升级k8s后初始化失败问题
    微信小程序第三方插件申请
    牛客C++刷题记录
    OD-Model【8】:YOLOv4
    知识改变命运:Java 语言 【可变参数】
    【Docker】Docker:解析容器化技术的利器与在Linux中的关键作用
    Qt窗体设计的布局
    上周热点回顾(10.10-10.16)
    mybatis的xml中<trim>标签的用法
  • 原文地址:https://blog.csdn.net/weixin_62985813/article/details/133900648