priority_queue 优先级队列
介绍
priority_queue,优先级队列,它的底层是个vector,在vector的基础上封装堆的算法,于是它摇身一变,成了一个存储在一块连续空间中的堆。
《什么是堆?》 堆是一棵完全二叉树,特点是:所有子节点小于或等于父节点 / 所有子节点大于或等于父节点。
而所谓优先级,就是大堆和小堆的区分。
如此看来,优先级队列似乎一点也不神秘。
但它还是有一些值得我们学习的地方的,不只是源码的实现细节。
思索
1)仿函数
顾名思义,模仿函数。那么谁来模仿呢,当然是C++最常用的东西——类。
其实就是写一个类,在类中重载运算符operator()
,使得这个类对象可以像函数一样调用。
表面上是调用了一个函数,其实是调用重载运算符operator()
,然后运行其下的代码。
举个小例子:
template<class T>
struct com_less // 这是一个仿函数类,用来比较大小(左参数小于右参数)
{
bool operator()(const T& left, const T& right)
{
return left < right;
}
};
void Test()
{
com_less<int> com;
cout << com(1, 2) << endl;
}
// 输出结果为1
很简单吧。
在C语言中,函数指针也可也达到这样的效果,但是不同的是,C++的仿函数更加的“泛型编程”,满足STL对抽象性的要求,可以很好的和STL的其他组件进行搭配。比如和配接器搭配,应用于各个容器之中。
关于仿函数的更多知识,博主尚未掌握,诸如仿函数的种类等等,日后有机会再详谈。
2)刚才说了,priority_queue底层是vector + 堆算法实现的,在查看STL中priority_queue的源码时候,我发现库中将priority_queue关于堆的操作封装了(make_heap/push_heap/pop_heap)。
最坑的是,当时的我并不知道堆算法在一个名为"stl_heap.h"的头文件中,于是找了半天。同样出于好奇,我想知道库里用的是向下调整还是向上调整,最终,我探到了它的底牌:
库中使用向下调整建堆。
为什么不用向上调整建堆?
这与两种建堆方法的时间复杂度有关。还是这篇文章
我们以满二叉树为例,通过计算,求出向上调整建堆和向下调整建堆的时间复杂度:
如果你信得过我的话(当然,我是挺自信的),一个是O(N*logN)
,一个是O(N)
,自然,向下调整建堆的效率更高,猜想得证。
模拟
参考STL库源码,我对priority_queue模拟了一番:
#pragma once
#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;
namespace Myspace
{
// 实现仿函数
template<class T>
struct less // less是建大堆
{
bool operator()(const T& val1, const T& val2)
{
return val1 < val2;
}
};
template<class T>
struct greater // greater是建小堆
{
bool operator()(const T& val1, const T& val2)
{
return val1 > val2;
}
};
// 优先级队列类
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
// 向下调整
void AdjustDown(int parent)
{
Compare com; // 仿函数
int child = parent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.size() && com(_con[child], _con[child + 1])) // 先假设建大堆,则需要找到左右孩子中大的那个
{
child += 1;
}
if (child < _con.size() && com( _con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
// 向上调整
void AdjustUp(int child)
{
Compare com; // 仿函数
int parent = (child - 1) / 2;
while ( child > 0)
{
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
void make_heap()
{
// 建堆 - 使用向下调整
for (int i = (_con.size() - 2) / 2; i >= 0; i--)
{
AdjustDown(i);
}
}
public:
priority_queue()
{ }
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
{
while (first != last)
{
_con.push_back(*first);
++first;
}
make_heap();
}
void push(const T& val)
{
_con.push_back(val);
AdjustUp(_con.size() - 1);
}
void pop()
{
assert(!empty());
// 删除的是头元素
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
const T& top() const
{
return _con.front();
}
private:
Container _con;
};
}
需要注意的是:
- 每次push时,都是在vector的最后插入一个元素,也就是二叉树的最下、最右的位置,因此只需一次向上调整就可以把新元素正确地插入堆中。
- pop删除的是根节点,也就是vector第一个元素,此时将头、尾两个元素交换,将新的尾删除,再将新的头向下调整一次即可。
- 相对于插入、删除一个元素,建堆操作是在vector已有一些元素之后,对整个vector进行排序建堆。