前言
我们前面学习了queue队列和deque双端队列,今天要学习的priority_queue是
优先级队列
,和queue在同一个头文件中
事不宜迟,那我们就开始今天的学习
文章目录
一. 结构
普通的队列是一种
先进先出的数据结构
,元素在队列尾追加,而从队列头删除。
在优先级队列中,元素被赋予优先级
。当访问元素时,具有最高优先级的元素最先删除
。优先级队列具有最高级先出的行为特征。通常采用堆数据结构
来实现。
1. 堆
在数据结构中,我们学到一个特殊的二叉树——堆
其实堆的本质是一个数组,只是通过下标之间的关系,可以被转换成完全二叉树
2. priority_queue
而priority_queue就是一种堆,不过堆并不是完全有序,就像上图的大顶堆。
而priority_queue是完全有序的堆
3. 实例化
priority_queue的类模板分别有三个参数
参数名称 | 参数说明 |
---|---|
T | 元素类型 |
Container | 底层容器(默认是vector) |
Compare | 比较方式(默认是less) |
前两个参数我们在stack和queue中已经见过,这里就不再赘述了。
主要是第三个参数Compare
在稍后的模拟实现中,我们会再次谈及这个模板参数的强大
二. 函数接口
priority_queue的函数接口如下所示
函数名称 | 函数功能 |
---|---|
(constructor) | 构造函数 |
empty | 判空 |
size | 获取优先级队列的元素个数 |
top | 取堆顶元素 |
push | 插入 |
emplace | 插入(和push有所不同) |
pop | 删除堆顶数据 |
swap | 交换两个堆的元素 |
函数接口和stack,queue基本一致,这里就不再过多赘述
三. 模拟实现
那么接下来,我们就来模拟实现priority_queue吧
我们先模拟实现一个优先级高的队列,也就是大堆
1. 基础框架
第一次的实现,我们类模板里不写class Compare
template<class T,class Container=vector<T>>
class priority_queue
{
private:
Container _con;
};
基本框架和stack和queue一样
2. 函数接口实现
因为我们套用了底层容器,所以很多函数接口都可以复用叠层容器的函数接口来实现
empty判空
//判空
bool empty()
{
return _con.empty();
}
获取优先级队列的元素个数
//返回优先级队列的元素个数
size_t size()
{
return _con.size();
}
取堆顶元素
//取堆顶元素
const T& top()
{
return _con[0];
}
插入
这里涉及到堆的算法,不懂的读者可以参看【算法基础】八大排序中的堆排
或者看其他大佬的文章,这里仅以注释作简单说明
//向上调整法
void adjust_up(size_t child)
{
size_t parent = (child - 1) / 2;
while (child > 0)
{
//建大堆——降序
//双亲节点比孩子节点小才交换
if (_con[parent] < _con[child])
{
swap(_con[parent] , _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
//因为插入前堆本身就是有序的
//所以一旦遇到双亲比孩子大的,就代表再上面的节点都比该孩子节点大
//所以排序完毕,直接跳出循环
break;
}
}
}
//插入
void push(const T&val)
{
//尾插元素
_con.push_back(val);
//向上调整法
adjust_up(_con.size()-1);
}
删除
//向下调整法
void adjust_down(size_t parent)
{
//先找左孩子
size_t child = parent * 2 + 1;
while (child < _con.size())
{
//先判断右孩子是否比左孩子还大
if (child + 1 < _con.size() && _con[child] < _con[child + 1])
{
++child;
}
if (_con[parent] < _con[child])
{
swap(_con[parent],_con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//删除堆顶元素
void pop()
{
assert(!empty());
//将堆顶的元素和最后的元素交换
swap(_con[0], _con[_con.size() - 1]);
//删除最后一个元素
_con.pop_back();
//向下调整法重建堆
adjust_down(0);
}
3.简单测试
void test1()
{
priority_queue<int>pq;
pq.push(1);
pq.push(3);
pq.push(4);
pq.push(5);
pq.push(3);
pq.push(7);
pq.push(10);
pq.push(15);
pq.push(2);
while(!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
}
4. 完整代码
#pragma once
#include<vector>
#include<assert.h>
#include<algorithm>
#include<iostream>
using namespace std;
template<class T,class Container=vector<T>>
class priority_queue
{
public:
//判空
bool empty()
{
return _con.empty();
}
//返回优先级队列的元素个数
size_t size()
{
return _con.size();
}
//取堆顶元素
const T& top()
{
return _con[0];
}
//向上调整法
void adjust_up(size_t child)
{
size_t parent = (child - 1) / 2;
while (child > 0)
{
//建大堆——降序
//双亲节点比孩子节点小才交换
if (_con[parent] < _con[child])
{
swap(_con[parent] , _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
//因为插入前堆本身就是有序的
//所以一旦遇到双亲比孩子大的,就代表再上面的节点都比该孩子节点大
//所以排序完毕,直接跳出循环
break;
}
}
}
//插入
void push(const T&val)
{
//尾插元素
_con.push_back(val);
//向上调整法
adjust_up(_con.size()-1);
}
//向下调整法
void adjust_down(size_t parent)
{
//先找左孩子
size_t child = parent * 2 + 1;
while (child < _con.size())
{
//先判断右孩子是否比左孩子还大
if (child + 1 < _con.size() && _con[child] < _con[child + 1])
{
++child;
}
if (_con[parent] < _con[child])
{
swap(_con[parent],_con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//删除堆顶元素
void pop()
{
assert(!empty());
//将堆顶的元素和最后的元素交换
swap(_con[0], _con[_con.size() - 1]);
//删除最后一个元素
_con.pop_back();
//向下调整法重建堆
adjust_down(0);
}
private:
Container _con;
};
void test1()
{
priority_queue<int>pq;
pq.push(1);
pq.push(3);
pq.push(4);
pq.push(5);
pq.push(3);
pq.push(7);
pq.push(10);
pq.push(15);
pq.push(2);
while(!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
}
四. Compare—仿函数
1. 介绍
首先我们介绍一下仿函数
在之前的学习,我们学习了很多的运算符重载,也说了哪几个运算符没有重载。
有5个运算符不能重载,“ .* ” , “ ::” , “sizeof” , “ ?:” , “ . ”
那在我们平常使用的,有一个运算符我们还没有重载过 — ()
以下我们给个简单的样例
struct Less
{
bool operator()(int x, int y)
{
return x < y;
}
};
int main()
{
int x = 10;
int y = 10;
Less less;
cout << less(x, y) << endl;
return 0;
}
诶,仅看代码,是不是感觉这个Less类,被拿来当作函数一样使用了呢?
这就是仿函数——重载()
2. 思考
再返回来,我们不是将priority_queue的实现,怎么突然将仿函数了呢?
这里我们要先抛出一个问题:
上面的priority_queue的实现,我们是不是设置成了优先级高的队列,或者叫大堆
那小堆怎么写了?
有人可能就在想,简单,改一下向上调整法和向下调整法的比较部分的代码就好了
那难道我们每次切换大小优先级的时候,都需要改代码吗?
答案肯定是不
那如何控制这个比较部分呢?
这就要应用到我们刚学到的仿函数了。
3. 仿函数应用
参看文献,比较方式有两个类——less和greater
二者的头文件都是functional
我们模仿标准库的写法
less是建大堆,greater是建小堆
template<class T>
struct less
{
bool operator()(const T&num1, const T&num2)
{
return num1 < num2;
}
};
template<class T>
struct greater
{
bool operator()(const T&num1, const T&num2)
{
return num1 > num2;
}
};
在类模板的参数列表中添加一个参数,并默认为less
这样在比较部分我们就可以套用仿函数,实现内部的自动推导,这就是封装的魅力
注意,这里仿函数传参的位置不可以交换,要仔细思考位置
其他部分代码均未改变
4. 测试
void test1()
{
priority_queue<int>pq;
pq.push(1);
pq.push(3);
pq.push(4);
pq.push(5);
pq.push(3);
pq.push(7);
pq.push(10);
pq.push(15);
pq.push(2);
while(!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
priority_queue<int, vector<int>, greater<int>>pq1;
pq1.push(1);
pq1.push(3);
pq1.push(4);
pq1.push(5);
pq1.push(3);
pq1.push(7);
pq1.push(10);
pq1.push(15);
pq1.push(2);
while (!pq1.empty())
{
cout << pq1.top() << " ";
pq1.pop();
}
cout << endl;
}
5. 完整代码
#pragma once
#include<vector>
#include<assert.h>
#include<algorithm>
#include<iostream>
using namespace std;
template<class T>
struct less
{
bool operator()(const T&num1, const T&num2)
{
return num1 < num2;
}
};
template<class T>
struct greater
{
bool operator()(const T&num1, const T&num2)
{
return num1 > num2;
}
};
template<class T,class Container=vector<T>,class Compare=less<T>>
class priority_queue
{
public:
//判空
bool empty()
{
return _con.empty();
}
//返回优先级队列的元素个数
size_t size()
{
return _con.size();
}
//取堆顶元素
const T& top()
{
return _con[0];
}
//向上调整法
void adjust_up(size_t child)
{
Compare Com;
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (Com(_con[parent],_con[child]))
{
swap(_con[parent] , _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//插入
void push(const T&val)
{
//尾插元素
_con.push_back(val);
//向上调整法
adjust_up(_con.size()-1);
}
//向下调整法
void adjust_down(size_t parent)
{
Compare Com;
//先找左孩子
size_t child = parent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.size() && Com(_con[child],_con[child + 1]))
{
++child;
}
if (Com(_con[parent],_con[child]))
{
swap(_con[parent],_con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//删除堆顶元素
void pop()
{
assert(!empty());
//将堆顶的元素和最后的元素交换
swap(_con[0], _con[_con.size() - 1]);
//删除最后一个元素
_con.pop_back();
//向下调整法重建堆
adjust_down(0);
}
private:
Container _con;
};
void test1()
{
priority_queue<int>pq;
pq.push(1);
pq.push(3);
pq.push(4);
pq.push(5);
pq.push(3);
pq.push(7);
pq.push(10);
pq.push(15);
pq.push(2);
while(!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
priority_queue<int, vector<int>, greater<int>>pq1;
pq1.push(1);
pq1.push(3);
pq1.push(4);
pq1.push(5);
pq1.push(3);
pq1.push(7);
pq1.push(10);
pq1.push(15);
pq1.push(2);
while (!pq1.empty())
{
cout << pq1.top() << " ";
pq1.pop();
}
cout << endl;
}
结束语
本章对priority_queue的学习就到这了,感谢阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要