C++优先队列详解

66 阅读5分钟

前言

不久前记录了单调队列的用法,而优先队列则是和单调队列很相似的一种数据结构。

普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。

在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出的行为特征。在优先队列里,元素被储存在“堆里面”,里面的元素是大致有序的(不是严格有序的),但是每次pop出来的元素,都是最大(或者最小)的元素,然后pop出去之后,再重新调整堆内的元素,使最大(最小)的元素处于最顶端。

C++中优先队列的实现

和队列基本操作相同

top 访问队头元素

empty 队列是否为空

size 返回队列内元素个数

push 插入元素到队尾 (并排序)

emplace 原地构造一个元素并插入队列

pop 弹出队头元素

swap 交换内容

定义:priority_queue<Type, Container, Functional>

Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式。

当需要用自定义的数据类型时才需要传入这三个参数;

//升序队列 
priority_queue <int,vector<int>,greater<int> > q; //队头元素是所有元素中的最小值
//降序队列 
priority_queue <int,vector<int>,less<int> >q; //队头元素是所有元素中的最大值
//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)

什么是大顶堆和小顶堆

一、什么是堆
堆是一种非线性结构,可以把堆看作一棵二叉树,也可以看作一个数组,即:堆就是利用完全二叉树的结构来维护的一维数组
堆可以分为大顶堆和小顶堆。
大顶堆:每个结点的值都大于或等于其左右孩子结点的值。
小顶堆:每个结点的值都小于或等于其左右孩子结点的值。
如果是排序,求升序用大顶堆,求降序用小顶堆。
一般我们说 topK 问题,就可以用大顶堆或小顶堆来实现,
最大的 K 个:小顶堆
最小的 K 个:大顶堆
二、大顶堆的构建过程
大顶堆的构建过程就是从最后一个非叶子结点开始从下往上调整。
最后一个非叶子节点怎么找?这里我们用数组表示待排序序列,则最后一个非叶子结点的位置是:数组长度/2-1。假如数组长度为9,则最后一个非叶子结点位置是 9/2-1=3。
比较当前结点的值和左子树的值,如果当前节点小于左子树的值,就交换当前节点和左子树;
交换完后要检查左子树是否满足大顶堆的性质,不满足则重新调整子树结构;
再比较当前结点的值和右子树的值,如果当前节点小于右子树的值,就交换当前节点和右子树;
交换完后要检查右子树是否满足大顶堆的性质,不满足则重新调整子树结构;
无需交换调整的时候,则大顶堆构建完成。

在C++优先队列里面,对于基础类型默认是大顶堆,比如priority_queue< int > a,等同于 priority_queue<int, vector< int >, less< int > > a; 这里一定要有空格,不然成了右移运算符

priority_queue<int, vector< int >, greater< int > > c这样就是小顶堆。

下面是用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(); 
    }
}

输出是:

image.png

下面是用自定义数据类型做优先队列元素的例子:

image.png

经典题目

一、前k个高频元素

题目链接:347. 前 K 个高频元素 - 力扣(LeetCode)

题目要求:给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

C++ 代码:

class Solution {
public:
    class myCmp{
    public://这里自己定义的仿函数一定要是public的,不能是private的
        bool operator()(const pair<int,int>& a,const pair<int,int>& b)
        {
            return a.second > b.second;//注意,这里和快排里面自己定义的cmp函数不一样,如果是 >,就是小根堆,反之就是大根堆
        }
    };
    vector<int> topKFrequent(vector<int>& nums, int k) {
        //用c++自带的stl里的优先队列来做
        priority_queue<pair<int,int>,vector<pair<int,int>>,myCmp> pri_que;//注意定义的方式
        unordered_map<int,int>record;
        for(auto i:nums)
            record[i] ++;
        for(auto it = record.begin();it != record.end();it ++)
        {
            pri_que.push(*it);
            if(pri_que.size() > k)//用的是小顶堆,只维护k个数,每次把最小的数都pop出去,最后剩下的就是最大的k个数
                pri_que.pop();
        }
        vector<int>ans;
        while(k)
        {
            ans.push_back(pri_que.top().first);
            pri_que.pop();
            k --;
        }
        return ans;
    }
};

二、丑数

题目链接:264. 丑数 II - 力扣(LeetCode)

题目要求:给你一个整数 n ,请你找出并返回第 n 个 丑数 。

丑数 就是只包含质因数 23 和/或 5 的正整数。

思路:首先,将最小的丑数1入堆,每次pop出最小的丑数x,而且2x,3x,5x也都是丑数,分别都压入堆中 。如此重复n次,第n个pop出来的元素就是第n个丑数。

class Solution {
public:
    int nthUglyNumber(int n) {
        //方法一,优先队列
        //首先,将最小的丑数1入堆
        //每次pop出最小的丑数x,而且2x,3x,5x也都是丑数,分别都压入堆中
        //如此重复n次,第n个pop出来的元素就是第n个丑数
        //为了避免同一个元素重复入队,用set来记录入队过的元素
        priority_queue<long,vector<long>,greater<long> >que;
        que.push(1);
        unordered_set<long>record;
        long ans = 0;
        while(n)
        {
            ans = que.top();
            que.pop();
            n --;
            if(record.find(2*ans) == record.end())
            {
                que.push(2*ans);
                record.insert(2*ans);
            }
            if(record.find(3*ans) == record.end())
            {
                que.push(3*ans);
                record.insert(3*ans);
            }
            if(record.find(5*ans) == record.end())
            {
                que.push(5*ans);
                record.insert(5*ans);
            }
        }
        return ans;
    }
};