数据结构算法刷题笔记——二、2.3 priority_queue
1 priority_queue简介
优先队列(priority_queue):本质是heap(堆),内部实现是一个二叉堆
- 是一个队列,“优先”意指取队首元素时,有一定的选择性,即根据元素的属性选择某一项值最优的出队
- 优先队列其实就是把堆模板化,将所有入队的元素排成具有单调性的一队,方便我们调用
- 优先级队列是一个拥有权值观念的queue。它允许在底端添加元素、在顶端去除元素、删除元素。 缺省情况下,优先级队列利用一个大顶堆完成
- 普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除
- 优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除
- 优先队列具有最高级先出 (first in, largest out)的行为特征
1.1 数据结构——堆
堆是以二叉树为基础的数据结构,分为大堆与小堆。
1.1.1 堆定义
- 完全二叉树的任意结点的关键字小于或等于其左孩子和右孩子的关键字,则称之为小根堆(右图)
- 反之则为大根堆(左图)
堆在的存储形式:
- 不是使用树形结构存储的,而是使用数组的形式
- 堆在逻辑上表现为一棵完全二叉树,在物理上表现为一个数组
树和数组之间有一个关联:
- 树中双亲(parent)、左孩子(left)、右孩子(right)在数组中的下标满足关系:
- (1)已知双亲在数组中的下标 parent,则左孩子和右孩子在数组中对应的下标:
left = 2 * parent + 1;
right = 2 * parent + 2; - (2)已知孩子的下标 child(无论左孩子还是右孩子) 则:
parent = (child - 1)/ 2;
- (1)已知双亲在数组中的下标 parent,则左孩子和右孩子在数组中对应的下标:
- 数组也可以看做是完全二叉树的层序遍历
1.1.2 堆的操作
1.1.2.1 重建堆
问题:当堆顶记录改变时,如何重建堆?我们以小堆为例:
算法思想:
- 首先将与堆相应的完全二叉树根结点中的记录移出,该记录称为待调整记录
- 原来那个关键字较大的子结点相当于空结点,从空结点的左、右子树中选出一个关键字较小的记录,如果该记录的关键字仍小于待调整记录的关键字,则将该记录上移至空结点中
- 重复上述移动过程,直到空结点左、右子树的关键字均小于待调整记录的关键字。此时,将待调整记录放入空结点即可
- 上述调整方法相当于把待调整记录逐步向下“筛”的过程,所以一般称其为“筛选”法或“向下调整”
public static void shiftDown(long[] array, int size, int index) {
// index代表要调整的位置。size为堆的大小。
//
while (true) {
// 1. 判断 index 所在位置是不是叶子
// 逻辑上,没有左孩子一定就是叶子了(因为完全二叉树这个前提)
int left = 2 * index + 1;
if (left >= size) {
// 越界 -> 没有左孩子 -> 是叶子 -> 调整结束
return; // 循环的出口一:走到的叶子的位置
}
// 2. 找到两个孩子中的最值【最小值 via 小堆】
// 先判断有没有右孩子
int right = left + 1; // right = 2 * index + 2
int min = left; // 假设最小值就是左孩子,所以 min 保存的最小值孩子所在的下标
if (right < size && array[right] < array[left]) {
// right < size 必须在 array[right] < array[left] 之前,不能交换顺序
// 因为先得确定有右孩子,才有比较左右孩子的意义
// 有右孩子为前提的情况下,然后右孩子的值 < 左孩子的值
min = right; // min 应该是右孩子所在的下标
}
// 3. 将最值和当前要调整的位置进行比较,判断是否满足堆的性质
if (array[index] <= array[min]) {
// 当前要调整的结点的值 <= 最小的孩子值;说明这里也满足堆的性质了,所以,调整结束
return; // 循环的出口一:循环期间,已经满足堆的性质了
}
// 4. 交换两个值,物理上对应的就是数组的元素交换 min 下标的值、index 下标的值
long t = array[index];
array[index] = array[min];
array[min] = t;
// 5. 再对 min 位置重新进行同样的操作(对 min 位置进行向下调整操作)
index = min;
}
}
1.1.2.2 建初堆
问题:如何由一个任意序列建初堆?
算法思想:
- 将一个任意序列看成是对应的完全二叉树,由于叶结点可以视为单元素的堆,因而可以反复利用上述调整堆算法(“筛选”法),自底向上逐层把所有子树调整为堆,直到将整个完全二叉树调整为堆
可以证明: - 完全二叉树中,最后一个非叶结点位于第Ln/2J个位置,n为二叉树结点数目
- “筛选”需从第Ln/2J个结点开始,逐层向上倒退,直到根结点
public static void buildHeap (int[] array) {
//我们假定传入的数组是经过处理的,即数组内的元素个数就是堆的元素个数。
//通过二叉树可以观察到只需要从最后一个节点的双亲结点开始从底向上进行向下调整
for (int i = (array.length-2)/2; i >=0 ; i--) {
shiftDown(array,array.length,i);
}
}
private static void shiftDown(int[] array, int size, int index) {
//index 为当前需要调整的位置
while (index * 2 + 1 < size){
int left = index * 2 + 1;
int right = left + 1;
//找出最小孩子的下标
int min = left;
if (right < size && array[min] > array[right]){
min = right;
}
//如果当前结点满足堆的性质则结束。
if (array[index] < array[min]){
return;
}
//交换当前结点与最小孩子的值
swap(array,min,index);
//继续向下调整
index = min;
}
}
private static void swap(int[] array, int min, int index) {
int t = array[index];
array[index] = array[min];
array[min]= t;
}
1.2 仿函数(函数对象)
- 什么是仿函数(函数对象)?
- 仿函数就是假函数,它是把对象当作函数使用,所以也称为函数对象。因为普通函数在某些特殊场景下使用比较麻烦,所以就诞生了仿函数
- 如何实现仿函数?
- 重载()运算符即可
1.2.1 实现仿函数
代码中定义Less类,重载(),函数中定义了a、b两个参数,当a小于b就返回true,否则返回false。
- 在main函数中创建了Less类的对象,如果想要调用重载(),常规的调用方法应该是对象名.函数名(参数列表)。但因为重载()函数是可以省略.operator()的,所以我们可以使用简化的方式调用,这样是不是看起来跟使用普通函数一模一样了。这就是仿函数的使用。
#include "iostream"
using namespace std;
class Less {
public:
bool operator()(const int a,const int b) const{
return a < b;
}
};
int main() {
//创建对象
Less lessCompare;
//常规的调用方式
//bool result = lessCompare.operator()(2, 8);
//简化后的调用方式
bool result = lessCompare(2, 8);
cout << result << endl;
}
2 priority_queue 操作
优先队列具有队列的所有特性,包括队列的基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的
2.1 头文件
#include <queue>
#include <functional> //greater<>
using namespcae std;
2.2 默认优先输出大数据——大顶堆
1. priority_queue<Type, Container, Functional>
- Type 为数据类型. Container 为保存数据的容器. Functional 为元素比较的方式.
- 若不写后面两个参数.
- 容器:默认使用 vector
- 比较方式: 默认使用 operator < 即优先队列是大顶堆. 队头元素最大
- 大顶堆声明方式:
- 大顶堆就是把大的元素放在堆顶的堆
- 优先队列默认实现的就是大根堆,直接按C++STL的声明规则声明即可
- C++中的int,string等类型可以直接比较大小,优先队列会帮我们实现
- 但是如果是我们自己定义的结构体,就需要进行重载运算符
#include<queue>
priority_queue<int> q;
priority_queue<string> q;
priority_queue<pair<int,int> > q;
- 案例
srand(time(NULL));
priority_queue<int> pq1; // 默认是最大堆...
std::cout << "start..." << endl;
for (int i = 0; i < 10; i++) {
int t = rand() % 100;
cout << t << ends;
pq1.push(t);
}
std::cout << endl;
while (!pq1.empty())
{
cout << pq1.top() << ends;
pq1.pop();
}
cout << endl;
2.3 优先输出小数据——小顶堆
1. priority_queue<Type, Container, Functional> //大顶堆
2. priority_queue<int, vector<int>, greater<int> > p; //小顶堆
- Type 为数据类型. Container 为保存数据的容器. Functional 为元素比较的方式.
- 使用 greater . 即改用 operator >
- 大根堆是把大的元素放堆顶,小根堆就是把小的元素放到堆顶。
- 实现小根堆有两种方式:
- 第一种:是比较巧妙的,因为优先队列默认实现的是大根堆,所以我们可以把元素取反放进去,因为负数的绝对值越小越大,那么绝对值较小的元素就会被放在前面,我们在取出的时候再取个反,就瞒天过海地用大根堆实现了小根堆。
- 第二种:小根堆有自己的声明方式
priority_queue<int,vector<int>,greater<int> >q;
注意,当我们声明的时候碰到两个"<“或者”>"放在一起的时候,一定要记得在中间加一个空格。
这样编译器才不会把两个连在一起的符号判断成位运算的左移/右移。
- 案例:
priority_queue<int, vector<int>, greater<int>> pq2; // 最小堆
std::cout << "start..." << endl;
for (int i = 0; i < 10; i++) {
int t = rand() % 100;
std::cout << t << ends;
pq2.push(t);
}
std::cout << endl;
while (!pq2.empty())
{
cout << pq2.top() << ends;
pq2.pop();
}
cout << endl;
2.4 自定义优先级——重载默认的 < 符号
- 使用function
// 定义比较函数
// 后面一个表示栈顶元素? 所以这个是 "最小堆"
bool myCom(int a, int b) {
return a % 10 > b % 10; //个位数排序
}
// 使用
priority_queue<int, vector<int>, function<bool(int,int)>> pq3(myCom);
std::cout << "start..." << endl;
for (int i = 0; i < 10; i++) {
int t = rand() % 100;
std::cout << t << ends;
pq3.push(t);
}
std::cout << endl;
while (!pq3.empty())
{
cout << pq3.top() << ends;
pq3.pop();
}
cout << endl;
- 自定义数据类型
#include<iostream>
#include<queue>
#include<cstdlib>
using namespace std;
struct Node{
int x,y;
Node(int a=0, int b=0):
x(a), y(b) {}
};
struct cmp{
bool operator()(Node a, Node b){
if(a.x == b.x) return a.y>b.y;
return a.x>b.x;
}
};
int main(){
priority_queue<Node, vector<Node>, cmp>p;
for(int i=0; i<10; ++i)
p.push(Node(rand(), rand()));
while(!p.empty()){
cout<<p.top().x<<' '<<p.top().y<<endl;
p.pop();
}//while
//getchar();
return 0;
3 基本函数实现
3.1 头文件
首先函数在头文件中,归属于命名空间std,使用的时候需要注意。
#include <queue>
#include <functional> //greater<>
using namespcae std;
3.2 构造函数
priority_queue<int> q1;
priority_queue< pair<int, int> > q2; // 注意在两个尖括号之间 一定要留空格。
priority_queue<int, vector<int>, greater<int> > q3; // 定义小的先出队
- 普通方法
priority_queue<int> q;
//通过操作,按照元素从大到小的顺序出队
priority_queue<int,vector<int>, greater<int> > q;
//通过操作,按照元素从小到大的顺序出队
- 自定义优先级
struct cmp {
operator bool ()(int x, int y)
{
return x > y; // x小的优先级高
//也可以写成其他方式,
//如: return p[x] > p[y];表示p[i]小的优先级高
}
};
priority_queue<int, vector, cmp> q; //定义方法
//其中,第二个参数为容器类型。第三个参数为比较函数。
- 自定义数据类型
struct node {
int x, y;
friend bool operator < (node a, node b)
{
return a.x > b.x; //结构体中,x小的优先级高
}
};
priority_queue<node>q; //定义方法
//在该结构中,y为值, x为优先级。
//通过自定义 operator < 操作符来比较元素中的优先级。
//在重载”<”时,最好不要重载”>”,可能会发生编译错误
3.3 添加函数
- 插入元素到队尾 (并排序)
q.push();
- 原地构造一个元素并插入队列
q.emplace();
3.4 删除函数
- 弹出队头元素
q.pop();
3.5 判断函数
- 队列是否为空
q.empty();
3.6 大小函数
- 返回队列内元素个数
q.size();
3.7 访问队头元素
- 访问队头元素,返回队列首元素,不改变队列
q.top();
3.8 交换内容
- 交换内容
swap
#include <queue>
int main() {
std::priority_queue<int> pq1;
std::priority_queue<int> pq2;
// 进行一些操作来填充 pq1 和 pq2
// 交换 pq1 和 pq2
pq1.swap(pq2);
return 0;
}
4 代码案例
4.1 基本类型优先队列案例:
#include<iostream>
#include <queue>
using namespace std;
int main()
{
//对于基础类型 默认是大顶堆
priority_queue<int> a;
//等同于 priority_queue<int, vector<int>, less<int> > a;
// 这里一定要有空格,不然成了右移运算符↓↓
priority_queue<int, vector<int>, greater<int> > c; //这样就是小顶堆
priority_queue<string> b;
for (int i = 0; i < 5; i++)
{
a.push(i);
c.push(i);
}
while (!a.empty())
{
cout << a.top() << ' ';
a.pop();
}
cout << endl;
while (!c.empty())
{
cout << c.top() << ' ';
c.pop();
}
cout << endl;
b.push("abc");
b.push("abcd");
b.push("cbd");
while (!b.empty())
{
cout << b.top() << ' ';
b.pop();
}
cout << endl;
return 0;
}
运行结果:
4 3 2 1 0
0 1 2 3 4
cbd abcd abc
4 .2 用pair做优先队列元素的例子:
规则:pair的比较,先比较第一个元素,第一个相等比较第二个
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
int main()
{
priority_queue<pair<int, int> > a;
pair<int, int> b(1, 2);
pair<int, int> c(1, 3);
pair<int, int> d(2, 5);
a.push(d);
a.push(c);
a.push(b);
while (!a.empty())
{
cout << a.top().first << ' ' << a.top().second << '\n';
a.pop();
}
}
运行结果:
2 5
1 3
1 2
4.3 用自定义类型做优先队列元素的例子
#include <iostream>
#include <queue>
using namespace std;
//方法1
struct tmp1 //运算符重载<
{
int x;
tmp1(int a) {x = a;}
bool operator<(const tmp1& a) const
{
return x < a.x; //大顶堆
}
};
//方法2
struct tmp2 //重写仿函数
{
bool operator() (tmp1 a, tmp1 b)
{
return a.x < b.x; //大顶堆
}
};
int main()
{
tmp1 a(1);
tmp1 b(2);
tmp1 c(3);
priority_queue<tmp1> d;
d.push(b);
d.push(c);
d.push(a);
while (!d.empty())
{
cout << d.top().x << '\n';
d.pop();
}
cout << endl;
priority_queue<tmp1, vector<tmp1>, tmp2> f;
f.push(b);
f.push(c);
f.push(a);
while (!f.empty())
{
cout << f.top().x << '\n';
f.pop();
}
}
运行结果:
3
2
1
3
2
1