作者:几冬雪来
时间:2023年10月10日
内容: C++——优先级队列与仿函数讲解
目录
前言:
在上一篇博客中我们讲解了stack和queue,以及deque的用法说明。而今天我们将借由stack和queue来学习C++的又一重要学习板块——优先级队列。
优先级队列:
要学习优先级队列就要了解什么是优先级队列。
在上一篇博客中我们学习了deque(双端队列),而deque和优先级队列有异曲同工之处,那就是二者虽然都有队列的名字,但是实际上它们都不是队列,优先级队列写作——priority_queue。
和stack的后进先出与queue的先进先出类似,优先级队列顾名思义就是要先出优先级高的。
在优先级队列中它默认为大的优先级高。
像上图一样,如果是大的优先级高那就类似二叉树中的大堆,如果是小的优先级高那么这个地方就类似二叉树的小堆。
而且优先级队列并没有一个单独的头文件,我们只要书写了“#include<queue>”就可以使用优先级队列了。
在上图我们就使用了优先级队列进行了数据的插入。
与此同时,因为优先级队列也是适配器,因此它也没提供迭代器不支持遍历,我们只能栈和队列一样,边取边走才能取到全部的数据。
再然后对pq里面的数据进行top和pop以后就能依次取到我们想要的值。
在上面有说过,优先级队列可以分为大的优先级高的大堆,它也可以变成小的优先级高的小堆。那么小的优先级高的代码又应该怎么写呢?
要实现小堆,这里就要用到之前学习的模板参数。第一条代码只有一个模板参数的是大堆的实现,下面的三个模板参数的则是小堆的实现。
小堆的实现需要传第三个模板参数,这也是我们后面要讲解到的——仿函数控制实现小堆的操作。
那么接下来就来看看优先级队列的代码实现。
这里就是我们的代码。
第一步是借由函数模板来输入我们想要的数据。
第二步则是建堆,找到最后叶子的父结点的位置。然后对其进行向下调整,i = (_con.size()-1-1)/2是找父结点的条件,第一个-1是找到下标,第二个是找到子结点,最后/2找到它的父结点。
最后就是调整,这里要注意循环条件子结点必须在区间里面,还有就是对比和调换。
这里就完成了优先级队列代码的书写。
同样的代码进行修改,我们也可以实现向上调整建立大堆。
这里就将原先建小堆的parent和child计算方式改变一下,原先代码的parent和child的书写位置也进行调换。
同时要注意向上调整的AdjustUp中循环的条件并不是单纯的将原先的child改为parent,因为parent的值是不会小于0的,因此条件要进行更改。
接口:
接下来就是优先级队列一些常见的接口书写。
在这里我们主要关注的就是pop和push接口。
pop的话要取最上面的数据我们不能直接pop,这里我们就要将堆顶元素和最后一个子结点交换。然后再进行pop最后一个子结点的数据,最后进行一次向下调整。
然后就是push的插入数据,这个地方也是在最后的位置插入一个数据,接下来进行向上调整即可完成。
仿函数:
接下来我们来学习C++一个重要的知识点——仿函数。
那么仿函数是什么呢?在这里我们书写一个最简单的仿函数。
这就是我们最简单的仿函数代码的书写,也可以叫它为函数对象。
但是严格意义上来讲仿函数和函数对象并不是完全相同的,仿函数更多的是指这个类,函数对象则是指这个类所定义的对象。
接下来我们再写一个代码。
在这里如果单看lessfunc而不去看上面的类,我们大概率会将它理解为是一个函数名或者函数指针。
但是实际上并不是这样的,结合上面的类来看。在类中重载了运算符,这个重载的运算符就是operator()。
这个地方本质等价于——lessfunc.operator()。
而在这里我们就可以看出来仿函数到底是什么作用了,它的作用就是让类的对象可以像使用函数那样被使用。
那么在这个地方有人就要问了,既然仿函数的作用是让类的对象可以像函数那样使用的话,为什么不直接写一个函数呢?
这是因为直接写一个函数的话,它是被我们写死的。要让其可以修改有两种方法,一种是指针,但是指针在有些地方的可读性差。而另外一种就是仿函数,这也就是仿函数出现的理由。
在优先级队列中,因为运算符重载的原因,我们在类模板处增加模板参数。
而在这里Comaper就是我们的仿函数。
用我们优先级队列的代码进行修改。
在原代码中我们比较都是直接大于号或者小于号进行比较,但是这种方式则是将代码写死了。
而用另一种方法则是定义一个类型是一个仿函数,它重载的是operator(),是它的对象。因此在比较的时候就可以对它进行调用。
按照同样的方式,我们也可以对向上调整的代码进行一个仿函数的书写。
同时,仿函数operator()中定义的类型也并不是一定是整形,类似double等类型也是可以进行比较。
特殊用法(指针):
在上文我们说过,仿函数operator()中定义的类型很多。
在有些特定的时候,它会有不同的书写方法,有的时候可能还需要我们自己去写它的仿函数。就拿我们的当初判断日期的函数来讲解。
类似上图,如果我们在priority_queue中书写的是指针,也就是在优先级队列中存放的是结点的指针。
如果是这样子书写的话可能会导致我们比较大小的结果出现差错。
就像上图一样,两次判断大小的结果不一样。
这也就是类型是指针带来的一系列的问题,在这个地方它默认是类型进行比较,而我们的类似是指针。
而我们new出来的地址的位置和大小是不确定的,new出来的地址一直在发生变化。
这里就需要我们自己去写它的仿函数去控制实现它。
像这里就控制仿函数去实现比较。
在这里我们就能对其进行修改,根据下面你的不同对上面的代码进行修改,因为下面的类型是指针所以在operator中的类型我们也修改为Date*,这样下去二者比较的就不是指针而是它们的对象。
就不会发生地址改变影响结果的情况了。
与此同时下面的类的参数也要进行修改。
结尾:
到这里我们的优先级队列和仿函数就了解了不少了,仿函数方面的内容可以不是讲得那么的全面,最后希望这篇博客能带来些许帮助吧。