1.前言
本章重点
本节着重讲解优先级队列的底层原理以及相关接口的模拟实现
2.什么是priority_queue
优先级队列默认的情况下是大堆。
优先级队列有三个函数模版,第一个是队列存储的类型,第二个是优先级队列底层使用的容器,第三个是仿函数,主要用来决定优先级队列里面谁的优先级更大,大的就排堆顶。
总结一下到底什么是优先级队列呢?
优先级队列简单来说就是一棵二叉树,但是这棵二叉树又被一定的条件约束,这个约束就是仿函数,也就是说二叉树的堆顶是由谁排。并且这个优先级队列可以理解成为尾进,头出的队列,即使要删除堆顶元素,也要把堆顶元素和尾部元素互换,再进行删除。
3.priority_queue的使用
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。
3.1 priority_queue的构造
1.空构造
#include<iostream>
#include<queue>//队列
#include<vector>//数组
#include<functional>//比较符号,less 或 greater
using namespace std;
int main()
{
priority_queue<int> pq1;//构造一个空的优先级队列
}
2.迭代器区间进行构造
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
priority_queue<int> pq2(v.begin(), v.end()) ;//用v的迭代器区间构造pq2
默认构造的是大堆,如果想构造小堆的话,则可以修改优先级队列的第三个参数。
3.2 push函数
push函数其实比较简单,只需要调用底层容器vector的push_back就可以了。
实际上push函数的底层实现是:先在直接调用vector的push_back函数插入,然后再调用向上调整建堆的函数,向上调整这个数,插入完成之后他才能是一个合法的堆。
priority_queue<int> pq3;
pq3.push(6);//向优先级队列中插入元素
pq3.push(3);
pq3.push(9);
pq3.push(8);
3.3 pop函数
pop函数比push函数稍微复杂一点,主要就是要先交换第一个值和最后一个值(第一个值即堆顶,最后一个值即堆尾)。然后删除堆尾这个值,再使用向下调整建堆的函数,来使他删除一个值之后还是堆。
pop函数调用:
pq3.pop();//删除优先级队列第一个元素
向size empty 以及front函数使用起来就比较简单了,这里就不过多的介绍了。
4.priority_queue的模拟实现
priority_queue的模拟实现主要是对函数的三个模版进行使用和封装。
4.1 仿函数
priority_queue默认是大堆,那么该如何实现小堆呢?需要先了解仿函数。
仿函数其实简单理解就是一个重载了operator() 的类对象,通过重载了operator,可以向调用函数一样的调用这个类对象,所以就把他称为仿函数。仿函数的具体细节参考上面链接。
(1) 仿函数的优点
① 仿函数比函数指针的执行速度快,函数指针通过地址调用,而仿函数是对运算符operator进行自定义来提高调用的效率。
② 仿函数比一般函数灵活,可以同时拥有两个不同的状态实体,一般函数不具备此种功能。
③ 仿函数可以作为模板参数使用,因为每个仿函数都拥有自己的类型。
(2)缺点
① 需要单独实现一个类。
② 定义形式比较复杂。
(3)仿函数的实现
template<class T>
struct Less
{
bool operator()(const T& l, const T& s)
{
return l < s;
}
};
template<class T>
struct greater
{
bool operator()(const T& l, const T& s)
{
return l > s;
}
};
仿函数的使用
int main()
{
less<int> lessInt;//定义一个仿函数类对象,参数类型指定为int
std::cout << lessInt(1, 3) << std::endl;//对仿函数的调用等价于std::cout << lessInt.operator()(1, 3) << std::endl;
}
4.2 堆的向上调整和向下调整
1.堆的向上调整
2.堆的向下调整
代码如下:
void AdjustDown(size_t parent)
{
size_t child = parent * 2 + 1;
while (child < size())
{
if (child + 1 < _con.size() && _con[child] <_con[child + 1] )
child++;
if (_con[child] <= _con[parent])
break;
else
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
}
}
void AdjustUp(size_t child)
{
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (_con[parent] >= _con[child])
break;
else
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child-1)/2;
}
}
}
4.3 push和pop函数
void push(const T& val)
{
_con.push_back(val);
if(_con.size()>1)
AdjustUp(_con.size() - 1);
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0); //将第0个元素进行一次向下调整
}
4.4 总体代码
.h文件
#include <iostream>
#include <vector>
#include <deque>
using namespace std;
namespace njust
{
template<class T>
struct Less
{
bool operator()(const T& l, const T& s)
{
return l < s;
}
};
template<class T>
struct greater
{
bool operator()(const T& l, const T& s)
{
return l > s;
}
};
template<class T,class Container=vector<T>,class Cmp=Less<T>>
class Priority_Queue
{
public:
Priority_Queue()
{
}
template<class InputIterator>
Priority_Queue(InputIterator first, InputIterator end)
{
while (first != end)
{
_con.push_back(*first);
first++;
}
//建堆----时间复杂度O(n)
//默认情况下priority_queue是大堆,故我们这里排升序
for (size_t i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
{
//从第一个非叶子节点开始计算,因为叶子节点天然的就是合法的堆。
AdjustDown(i);//向下调整算法,建大堆
}
}
void push(const T& val)
{
_con.push_back(val);
if(_con.size()>1)
AdjustUp(_con.size() - 1);
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0); //将第0个元素进行一次向下调整
}
bool empty()
{
return _con.empty();
}
T& top()
{
return _con[0];
}
size_t size()
{
return _con.size();
}
private:
//大堆
void AdjustDown(size_t parent)
{
size_t child = parent * 2 + 1;
while (child < size())
{
if (child + 1 < _con.size() && _con[child] <_con[child + 1] )
child++;
if (_con[child] <= _con[parent])
break;
else
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
}
}
void AdjustUp(size_t child)
{
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (_con[parent] >= _con[child])
break;
else
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child-1)/2;
}
}
}
Container _con;//可以把_con直接理解成为vector
};
}
.cpp文件
int main()
{
njust::Priority_Queue<int> pq;
pq.push(1);
pq.push(2);
pq.push(3);
pq.push(4);
pq.push(5);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
return 0;
}
5.本章总结
主要了阐述了优先级队列相关的函数接口如何实现,以及如何自己实现仿函数。笔者认为这一章最重要的就是向上向下调整建堆的过程,以及当使用一个迭代器区间进行初始化的时候,如何进行建堆,从哪个地方开始建。