priority_queue是什么?
priority_queue即优先级队列,它是一个STL库中的容器适配器,底层是用堆实现的。它常被用于解决topK问题。
priority_queue的使用
由于是容器适配器,所以它并不支持迭代器去遍历容器。使用的接口与stack、queue类似。
通过演示不难发现,优先级队列默认是一个大根堆,即top()就是堆中的最大值。这又有一个奇怪的点了,上面在看文档的时候,我们发现priority_queue的第三个模板参数是一个less<…>。那么意味着建立小根堆就要第三个模板参数要传greater。
对于内置类型可以通过传less<内置类型>和greater<内置类型>来控制建堆。对于自定义类型呢?需要类重载operator<或operator>,或是使用仿函数。仿函数等会儿会有介绍。
对于容器的选择方面,由于建堆、插入数据、删除数据都需要大幅度的使用operator[],容器为vector最佳。
下面通过一道oj来感受一下堆的实景应用场景
其实这里建大堆或建小堆都可以解决问题,下面我们依次看。首先,我演示一个最暴力的方法,即直接调用排序来解决该问题。
下面使用建大堆来解决问题。通过vector迭代器区间构造一个大根堆,然后再pop k-1次即可。这样的时间复杂度是O(N),但是空间复杂度也需要O(N),因此在大数据的解决topK问题下,可能并不合适。
最后我们通过建一个k个数的小堆来解决问题。思路如下,先用vector前k个元素构造一个小根堆。然后从k个位置开始一次遍历vector与堆顶元素比较,比堆顶元素大就pop(),然后在插入这个数即可。遍历完剩余的vector元素后,堆顶元素就是第k大的数。相较于建大堆,空间复杂度方面更为优秀,在大数据场景下的内存压力较小。
priority_queue模拟实现
核心接口
priority_queue其中最核心的就是向上调整接口和向下调整接口。向下调整接口和向上调整接口都用于是容器适配器维持堆的属性。向下调整通常用于建堆、pop()时维持容器适配器堆的结构。向上调整用于插入数据时调用以维持priority_queue堆的结构
先实现一个向下调整接口,实现思路如下,由于是向下调整需要注意通过父亲位置获取左孩子的位置的公式是child = (parent - 1) / 2。向下调整的结束标识就是当child 比 最后一个元素的位置大时就应该结束比较了。每一次比较的逻辑是,让父亲位置的元素与左孩子和右孩子较大的那个位置根据大小堆性质进行比较。若交换条件成立交换父子位置的元素,并迭代父子位置。反之则退出比较逻辑。
有一个向下调整接口还是不能满足priority_queue的应用场景。通常priority_queue的容器都是vector,而vector不支持头插头删数据,因此需要从尾部插入数据,并从这个位置向上调整以维持堆的结构。
具体实现思路如下,首先,我们需要从child节点的下标推算出parent节点的下标,parent = (child - 1) / 2。向上调整的整体逻辑是让父子节点进行比较,符合条件就让父子节点交换,然后继续迭代进行下一次的判断。直到父子节点不符合交换条件或者子节点已经在堆顶了那么结束循环。
仿函数
这样实现向上调整和向下调整还不够泛型,如何让我们这一份代码既可以生成大堆,又可以生成小堆呢?答案是借助仿函数(函数对象)来实现比较类型的泛型。下面简单来看一看。
下面在priority_queue的实现引入一个模板参数Compare用于传递仿函数来对比较的状态进行控制,使得通过不同的模板参数,让priority_queue 既可以生成大堆,又可以生成小堆。
看了Compare模板对于内置类型的处理,再看看对于自定义类型的处理。库里的less、greater要求自定义类型实现operator>和operator<。
下面再给大家看一个比较特殊的场景,以加深对仿函数的理解。
由于这里push的new的空间地址,此时greater是按照地址大小进行比较,显然这不符合小根堆的需求。那么我们可以写一个仿函数当作模板参数传过去给priority_queue。
从样例中可以感受到仿函数对c++编程带来了更多的灵活性和表达力。
基本接口
接下来就实现一下其他的基本接口,首先实现一下迭代器区间初始化建堆的构造函数。依次尾插数据到容器中,然后从第一个非叶子节点位置开始从后向前遍历容器依次向下调整建堆即可。
push()接口的实现思路就是先调用容器的push_back()接口,然后将插入的元素进行一次向上调整即可。
pop()接口实现思路如下,现将堆顶元素与最后一个元素交换一下,然后调用容器的pop_back()接口,最后对堆顶元素进行一次向下调整。
其余接口就不多赘述了,这里直接给出参考代码。
反向迭代器
反向迭代器也是一种适配器模式。所以,反向迭代器可以用正向迭代器进行封装适配。具体可以怎么实现呢?首先看一看STL库里是怎么做的。
通过观察库里的时限可以发现,其实反向迭代器的实现就像模板一样,你传什么容器的迭代器给我,我就实例化一份对应的反向迭代器。下面我就通过一份简单的反向迭代器的代码来一探究竟。
下面简单测试一下
总结
本篇文章介绍了priority_queue的使用和模拟实现,又通过priority_queue第三个模板参数Compare介绍了什么是仿函数,以及仿函数的具体使用场景。感受了仿函数带来的一种对于类的状态的一种泛型,使得在实现priority_queue是不用针对大堆或小堆写独立的代码,大幅度减少了工程代码的冗余。最后,介绍了反向迭代器并实现了一个简易版本的反向迭代器,了解了反向迭代器的实际思路和底层实现。