文章目录
- 1. priority_queue的使用
- 1.priority_queue的介绍
- 2.priority_queue的结构
- 3. 主要接口
- 4. 使用示例
- 2. 仿函数
- 1. 仿函数的概念
- 2.尝试实现仿函数
- 3.priority_queue的模拟实现
- 1.priority_queue的结构
- 2. 接口实现
- 1.向下调整算法
- 2. 向上调整算法
- 3.构造函数
- 4.修改数据
- 5.获取数据
首先,在这里还是推荐一下我正在用的一个C++的查询文档网站,这里是关于 priority_queue的使用文档。有什么本文中没有讲清楚的东西,可以去参考这个网站的内容。
1. priority_queue的使用
1.priority_queue的介绍
priority_queue(优先级队列),是包含在<queue>头文件下的一个容器适配器。下面是cplusplus网站对priority_queue的介绍。
可以看到,priority_queue是一个容器适配器,默认的容器是vector。他和队列在使用上是类似的,只是出队列的规则不同,queue是按照入队列的顺序出队列,priority_queue是按照优先级出队列,这里的优先级是按照类模板的第三个参数决定的,这里的第三个参数是一个仿函数,关于仿函数的概念将会在下文中详细讲解。
2.priority_queue的结构
priority_queue在底层的逻辑上,是一个堆,每次pop的都是堆定的数据,关于堆的讲解,可以去看一下博主之前写的【数据结构】树与二叉树,里面对堆这个数据结构讲解的还是比较清楚的。
3. 主要接口
priority_queue是一个容器适配器,所以接口基本上都差不多,我们看一下文档:
可以看到,接口和stack、queue基本相同,这里给出几个重点的函数接口:
函数接口 | 接口说明 |
---|---|
priority_queue() | 构造一个空的优先级队列 |
priority_queue(first, last) | 按照迭代器区间构造一个优先级队列 |
empty() | 判断优先级队列是否为空,返回bool类型 |
top() | 返回堆顶元素(被cosnt修饰) |
push(x) | 插入一个数据 |
pop() | 删除堆顶数据 |
4. 使用示例
了解了上述的一些接口之后,总是要实践一下的,接下来我们使用以下这个结构,来测试一下:
//这里放一下测试代码,读者可以自行拷贝下去测试
void Test1()
{
priority_queue<int> heap;
heap.push(5);
heap.push(3);
heap.push(7);
heap.push(10);
heap.push(1);
heap.push(9);
while (!heap.empty())
{
cout << heap.top() << " ";
heap.pop();
}
cout << endl;
}
可以看到,默认情况pop出来的是当前堆内的最大值,所以可知默认的堆是大堆。
❓那么如果想让堆变成小堆需要怎么做呢?
✅这里就要注意到,在文章的开头,我们讲到的类模板的第三个参数。可以看到默认传的参数是less,这就是表示默认建立大堆,如果想建小堆的话需要传的仿函数就是greater。
接下来,就用刚学的priority_queue做一道OJ题吧:215. 数组中的第K个最大元素 - 力扣(LeetCode).
我是题解,点我
2. 仿函数
1. 仿函数的概念
在上文中,我们看到priority_queue类模板中,有三个模板参数,其中第三个就是仿函数。
仿函数到底是什么呢?
仿函数(functors),也叫函数对象(function objects),是STL六大组件中的一部分,这里我们没办法一次性讲完它,所以就基于priority_queue的实现稍微介绍一下。
实际上,就实现意义而言,函数对象这个名字更加贴切:一种具有函数特质的对象。但是,仿函数似乎能更加符合的描述他的行为。所以这里我们就采用仿函数这种叫法。
在学习STL之前我们就已经了解了泛型编程的概念,C++引入了模板让我们的编程能够随意的控制数据类型,现在引入了仿函数的概念,让我们能够控制逻辑。
那么现在让我们见见仿函数:
这两个就是我们在priority_queue的参数列表中可能用到的仿函数。
可以看到,在使用的时候,实际上类似一个函数的调用,因此被称为仿函数
2.尝试实现仿函数
仿函数的本质就是一个运算符重载operator()
,重载的是函数调用的运算符。由于仿函数本身也就是一个类模板,所以我们的实现如下
template<class T>
class less
{
public:
bool operator()(const T& x, const T& y)//less和greater需要实现的就是一个比较,所以这里的返回值是bool类型
{
return x < y;
}
};
template<class T>
class greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
这样就算是实现了less和greater的仿函数。
这里只是稍微提了一下仿函数的概念,仿函数毕竟是STL六大组件之一,其中包含的东西还有很多,我们对仿函数的学习还在路上
3.priority_queue的模拟实现
有了上述只是的铺垫,现在我们已经有了模拟实现priority_queue的能力,那么just do it。
1.priority_queue的结构
首先,对于函数模板的设计,我们和库里面对其,给了三个参数,分别表示参数存入容器的参数类型,容器类型和仿函数,其中默认的仿函数是less,建大堆。
template<class T, class Container = std::vector<T>, class Compare = less<T>>
class priority_queue
{
public:
//...
private:
Container _con;
};
2. 接口实现
按照我们之前在【数据结构】树与二叉树实现堆的经验,一定需要实现的两个接口是向上调整和向下调整,这也是整个堆的核心接口,所以我们就先着重实现这两个功能性接口,这两个接口实现完成之后,其他的结构性接口都很容易实现啦。
1.向下调整算法
这里,我们以小堆为例(在这里偷个懒:小堆的图有以前画过的),我们需要做的事情就是找到两个孩子中较小的,然后与父节点比较大小,如果父节点大于子节点就执行交换,然后原来的子节点成为新的父节点,再次进行上述步骤。直到符合堆的结构
void adjust_down(size_t parent)
{
Compare cmp;//实例化仿函数
int minchild = 2 * parent + 1;
while (minchild < _con.size())
{
if (minchild + 1 < _cin.size() && cmp(_con[minchild], con[minchild + 1]))//使用仿函数找到符合条件的子节点
{
minchild++;
}
if (cmp(_con[parent], _con[minchild]))//判断是否满足堆结构
{
std::swap(_con[parent], _con[minchild]);
//父子节点迭代
parent = minchild;
minchild = 2 * parent + 1;
}
}
}
2. 向上调整算法
这里向上调整的操作,从指定的孩子节点位置开始,和父节点比较,判断是否满足堆结构,如果不满足,就交换父子节点,然后原来的父节点编程子节点,再次进行上述操作,直到满足堆结构为止。
void adjust_up(size_t child)
{
Compare cmp;//实例化仿函数
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (cmp(_con[parent], _con[child]))
{
std::swap(_con[parent], _con[child]);
//父子迭代
child = parent;
parent = (child - 1) / 2;
}
}
}
3.构造函数
构造函数分为无参的构造和迭代器区间构造
//无参的构造函数
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);
}
}
4.修改数据
priority_queue的数据修改只有push和pop两种情况
push数据就直接尾插,然后向上调整堆,直到满足堆的结构即可,pop数据就交换堆顶数据和最后一个数据,然后容器pop_back,然后向下调整直到剩余数据满足堆结构即可
void push(const T& val)
{
_con.push_back(val);//尾插数据
adjust_up(_con.size() - 1);//向上调整堆结构
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);//交换堆顶和堆内最后一个元素
_con.pop_back;//容器尾删
adjust_down(0);//向下调整堆结构
}
5.获取数据
剩下的接口就直接复用容器提供的接口即可
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
const T& top() const
{
return _con[0];
}