个人主页:@Sherry的成长之路
学习社区:Sherry的成长之路(个人社区)
专栏链接:C++学习
长路漫漫浩浩,万事皆有期待
上一篇博客:【C++】STL详解(七)—— stack和queue的使用及模拟实现
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中的元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的地方,都可以考虑使用priority_queue。
注意
: 默认情况下priority_queue是大堆。
方式一: 使用vector作为底层容器,内部构造大堆结构。
priority_queue<int, vector<int>, less<int>> q1;
方式二: 使用vector作为底层容器,内部构造小堆结构。
priority_queue<int, vector<int>, greater<int>> q2;
方式三: 不指定底层容器和内部需要构造的堆结构。
priority_queue<int> q;
注意
: 此时默认使用vector作为底层容器,内部默认构造大堆结构。
priority_queue的各个成员函数及其功能如下:
成员函数 | 功能 |
---|---|
push | 插入元素到队尾(并排序) |
pop | 弹出队头元素(堆顶元素) |
top | 访问队头元素(堆顶元素) |
size | 获取队列中有效元素个数 |
empty | 判断队列是否为空 |
swap | 交换两个队列的内容 |
示例:
#include
#include
#include
using namespace std;
int main()
{
priority_queue<int> q;
q.push(3);
q.push(6);
q.push(0);
q.push(2);
q.push(9);
q.push(8);
q.push(1);
while (!q.empty())
{
cout << q.top() << " ";
q.pop();
}
cout << endl; //9 8 6 3 2 1 0
return 0;
}
仿函数/函数对象也是类,是一个类对象。类对象可以像函数一样使用。仿函数要重载operator(),我们通过代码来看一看仿函数:
namespace sherry
{
template <class T>
class less
{
public:
bool operator()(const T& x, const T& y)const
{
return x < y;
}
};
template <class T>
class greater
{
public:
bool operator()(const T& x, const T& y)const
{
return x > y;
}
};
}
int main()
{
sherry::less<int> lessFunc;
lessFunc(1, 2);
lessFunc.operator()(1, 2);
return 0;
}
//lessFunc是一个对象,仿函数对象,可以像函数一样使用
仿函数的作用在于:在C语言中我们通过传入函数指针解决升序降序问题,虽然C++兼容了C,但是C++并没有继续利用函数指针,而是通过仿函数来控制升序和降序,我们以之前写过的排序为例子,通过利用仿函数来实现升序和降序:
template<class T,class Compare>
void BubbleSort(T* a, int n, Compare com)
{
for (int j = 0; j < n; j++)
{
int exchange = 0;
for (int i = 1; i < n - j; i++)
{
//if (a[i]
if (com(a[i], a[i-1]))
{
swap(a[i - 1], a[i]);
exchange = 1;
}
}
if (exchange == 0)
{
break;
}
}
}
int main()
{
sherry::less<int> lessFunc;
sherry::greater<int> greaterFunc;
lessFunc(1, 2);
int a[] = { 2,3,4,5,6,1,2,8 };
BubbleSort(a, sizeof(a) / sizeof(int),lessFunc);
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
BubbleSort(a, sizeof(a) / sizeof(int), greaterFunc);
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
return 0;
}
传值传参问题:这里是直接传值传参Compare com,当然也可以传引用传参const Compare& com,不过要记得加上const进行修饰,因为一般的仿函数比较小,问题不是很大。
priority_queue的底层实际上就是堆结构,实现priority_queue之前,我们先认识两个重要的堆算法。关于堆算法的具体讲解可以看这篇博客:【树与二叉树】二叉树顺序结构实现以及堆的概念及结构
以大堆为例,堆的向上调整算法就是在大堆的末尾插入一个数据后,经过一系列的调整,使其仍然是一个大堆。
调整的基本思想如下:
1、将目标结点与其父结点进行比较。
2、若目标结点的值比父结点的值大,则交换目标结点与其父结点的位置,并将原目标结点的父结点当作新的目标结点继续进行向上调整;若目标结点的值比其父结点的值小,则停止向上调整,此时该树已经是大堆了。
堆的向上调整算法代码:
//堆的向上调整(大堆)
void AdjustUp(vector<int>& v, int child)
{
int parent = (child - 1) / 2; //通过child计算parent的下标
while (child > 0)//调整到根结点的位置截止
{
if (v[parent] < v[child])//孩子结点的值大于父结点的值
{
//将父结点与孩子结点交换
swap(v[child], v[parent]);
//继续向上进行调整
child = parent;
parent = (child - 1) / 2;
}
else//已成堆
{
break;
}
}
}
以大堆为例,使用堆的向下调整算法有一个前提,就是待向下调整的结点的左子树和右子树必须都为大堆。
调整的基本思想如下:
1、将目标结点与其较大的子结点进行比较。
2、若目标结点的值比其较大的子结点的值小,则交换目标结点与其较大的子结点的位置,并将原目标结点的较大子结点当作新的目标结点继续进行向下调整;若目标结点的值比其较大子结点的值大,则停止向下调整,此时该树已经是大堆了。
堆的向下调整算法代码:
//堆的向下调整(大堆)
void AdjustDown(vector<int>& v, int n, int parent)
{
//child记录左右孩子中值较大的孩子的下标
int child = 2 * parent + 1;//先默认其左孩子的值较大
while (child < n)
{
if (child + 1 < n&&v[child] < v[child + 1])//右孩子存在并且右孩子比左孩子还大
{
child++;//较大的孩子改为右孩子
}
if (v[parent] < v[child])//左右孩子中较大孩子的值比父结点还大
{
//将父结点与较小的子结点交换
swap(v[child], v[parent]);
//继续向下进行调整
parent = child;
child = 2 * parent + 1;
}
else//已成堆
{
break;
}
}
}
只要知道了堆的向上调整算法和堆的向下调整算法,priority_queue的模拟实现就没什么困难了。
成员函数 | 功能 |
---|---|
push | 在容器尾部插入元素后进行一次向上调整算法 |
pop | 将容器头部和尾部元素交换,再将尾部元素删除,最后从根结点开始进行一次向下调整算法 |
top | 返回容器的第0个元素 |
size | 返回容器的当前大小 |
empty | 判断队列是否为空 |
priority_queue的模拟实现代码:
1、基础接口
const T& top() const
{
return _con[0];
}
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
2、push与向上调整
优先级队列就是堆结构,插入一个数之后,我们还需要去进行向上调整
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);
}
向上调整算法:
void adjust_up(size_t child)
{
//仿函数
Compare com;
size_t parent = (child - 1) / 2;
while (child > 0)
{
//if ( _con[parent]<_con[child])
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
3、pop与向下调整
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(0);
}
向下调整:
void adjust_down(size_t parent)
{
Compare com;//仿函数
size_t child = parent * 2 + 1;
while (child < _con.size())
{
//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
{
child++;
}
//if (_con[parent]<_con[child])
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
4、构造函数
这里主要说一下迭代器区间的构造函数:自定义类型会调用自己的迭代器区间构造,所以我们并不需要一个一个push,走个初始化列表即可,同时,数据进去之后我们还要建堆,利用向下调整算法:从倒数第一个非叶子节点,既最后一个节点的父节点开始进行向下调整:
priority_queue()
{}
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
:_con(first, last)
{
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
{
adjust_down(i);
}
}
5、总体代码
namespace sherry //防止命名冲突
{
//比较方式(使内部结构为大堆)
template<class T>
struct less
{
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
//比较方式(使内部结构为小堆)
template<class T>
struct greater
{
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
//优先级队列的模拟实现
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
public:
priority_queue()
{}
//迭代器
template <class InputIterator>
priority_queue(InputIterator first,InputIterator last)
{
while(first!=last)
{
_con.push_back(*first);
++first;
}
for(int i=0(_con.size()-1-1)/2;i>=0;i++)
{
adjust_down(i);
}
}
//堆的向上调整
void adjust_up(int child)
{ //logN
int parent = (child - 1) / 2; //通过child计算parent的下标
while (child > 0)//调整到根结点的位置截止
{
if (_comp(_con[parent], _con[child]))//通过所给比较方式确定是否需要交换结点位置
{
//将父结点与孩子结点交换
std::swap(_con[child], _con[parent]);
//继续向上进行调整
child = parent;
parent = (child - 1) / 2;
}
else//已成堆
{
break;
}
}
}
//插入元素到队尾(并排序)
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1); //将最后一个元素进行一次向上调整
}
//堆的向下调整
void adjust_down(int n, int parent)
{
int child = 2 * parent + 1;
//logN
while (child < n)
{
if (child + 1 < n&&_comp(_con[child], _con[child + 1]))
{
child++;
}
if (_comp(_con[parent], _con[child]))//通过所给比较方式确定是否需要交换结点位置
{
//将父结点与孩子结点交换
swap(_con[child], _con[parent]);
//继续向下进行调整
parent = child;
child = 2 * parent + 1;
}
else//已成堆
{
break;
}
}
}
//弹出队头元素(堆顶元素)
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(_con.size(), 0); //将第0个元素进行一次向下调整
}
//访问队头元素(堆顶元素)
T& top()
{
return _con[0];
}
const T& top() const
{
return _con[0];
}
//获取队列中有效元素个数
size_t size() const
{
return _con.size();
}
//判断队列是否为空
bool empty() const
{
return _con.empty();
}
private:
Container _con; //底层容器
Compare _comp; //比较方式
};
}
今天我们比较详细地完成了priority_queue的使用及模拟实现,了解了一些有关的底层原理。接下来,我们将进行C++模板进阶操作的的学习。希望我的文章和讲解能对大家的学习提供一些帮助。
当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~