深入解析 STL 优先级队列:从原理到实战

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》

《C++入门到进阶&自我学习过程记录》

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

[一、priority_queue 核心逻辑](#一、priority_queue 核心逻辑)

1、本质与优先级规则

2、底层容器的适配要求

3、常用接口与场景示例

[二、priority_queue 底层实现解析](#二、priority_queue 底层实现解析)

[1、默认 "大堆" 规则排序模拟实现](#1、默认 “大堆” 规则排序模拟实现)

2、仿函数

[2.1 仿函数的结构](#2.1 仿函数的结构)

[2.2 仿函数测试代码](#2.2 仿函数测试代码)

[2.3 利用仿函数进行 priority_queue 底层实现](#2.3 利用仿函数进行 priority_queue 底层实现)

[2.4 测试代码演示](#2.4 测试代码演示)

三、OJ实战练习:数组中第k个最大的元素

结束语


在 STL 中,priority_queue(优先队列)是处理优先级任务的实用工具 ------ 以堆结构为核心,封装复杂排序逻辑,能高效获取优先级最高的元素(默认是大的值优先级高),广泛用于 Top K、任务调度等场景。本文将拆解其特性、接口与原理,帮你从 "会用" 到 "懂底层原理"。

一、priority_queue 核心逻辑

1、本质与优先级规则

  • 容器适配器属性 :不直接存储数据,而是封装底层容器 (如 vector、deque),并通过堆算法(make_heap、push_heap、pop_heap)来维护堆结构。
  • 优先级规则 :默认是大堆 (堆顶为最大元素),可通过仿函数指定比较规则改为小堆;优先级由比较函数决定,支持内置类型和自定义类型。

参考文档: priority_queue - C++ Reference

2、底层容器的适配要求

底层容器 需支持 "随机访问迭代器" 和以下操作:

  • empty():检测是否为空
  • size():返回元素个数
  • front():访问堆顶元素(即容器首元素)
  • push_back():在容器尾部插入元素(用于后续堆调整算法)
  • pop_back():删除容器尾部元素(配合堆调整算法移除堆顶数据)

STL 中默认使用 vector (空间连续,堆算法效率更高),也可指定 deque

3、常用接口与场景示例

|---------------------------------|-----------------------------------------------------------------------------------------|
| 接口声明 | 功能说明 |
| priority_queue() | 构造一个空的优先队列,初始化后无任何元素,需通过 push(x) 插入数据。 |
| priority_queue(first, last) | 利用迭代器区间 [ first, last ] 中的所有元素初始化优先队列,初始化后会自动调整为堆结构。 |
| empty() | 检测优先队列是否为空,若队列中无元素则返回 true,存在元素则返回 false,常用于判断是否可执行 top() 或 pop() 操作。 |
| size() | 返回优先队列中有效元素的个数,可用于了解队列数据量,或配合循环控制 pop() 操作次数(如 Top K 问题中弹出前 K - 1 个元素)。 |
| top() | 返回堆顶元素的引用,大堆场景下返回队列中的最大值,小堆场景下返回最小值;需注意,调用前需用 empty() 确认队列非空,否则会触发未定义行为。 |
| push(x) | 将元素 x 插入优先队列的尾部,插入后会自动调用堆算法 (push_heap) 调整堆结构,确保队列仍满足大堆或小堆的排序规则。 |
| pop() | 删除优先队列的堆顶元素,删除前会先通过堆算法 (pop_heap)将堆顶元素交换到队列尾部 ,再执行删除;操作后需确保队列非空,且删除后仍保持堆结构。 |

接口测试代码:

cpp 复制代码
//Test.cpp
#include<iostream>
#include<queue>
using namespace std;

int main()
{
	//priority_queue接口使用
	priority_queue<int> pq1;//默认底层基于vector容器,且为大堆
	pq1.push(4);
	pq1.push(7);
	pq1.push(2);
	pq1.push(5);
	pq1.push(10);
	pq1.push(8);
	while (!pq1.empty())
	{
		cout << pq1.top() << " ";
		pq1.pop();
	}
	cout << endl;

	priority_queue<int, vector<int>, greater<int>> pq2;//调整成默认是小的优先级高(小堆)
	pq2.push(4);
	pq2.push(7);
	pq2.push(2);
	pq2.push(5);
	pq2.push(10);
	pq2.push(8);
	while (!pq2.empty())
	{
		cout << pq2.top() << " ";
		pq2.pop();
	}
	cout << endl;
	return 0;
}

二、priority_queue 底层实现解析

由于优先级队列的底层结构是堆,所以就会用到在之前所学数据结构堆中的相关功能的实现(如向上调整算法、向下调整算法等),如果不清楚的或者想要回顾之前的知识可以看下面我的往期文章:

数据结构之二叉树-堆

1、默认 "大堆" 规则排序模拟实现

cpp 复制代码
//Priority_Queue.h
#include<iostream>
#include<vector>
using namespace std;

namespace MyPriorityQueue
{
	template<class T, class Container = vector<T>>
	class priority_queue
	{
	public:
		//向上调整算法(已知孩子位置推算父亲位置)
		void AdjustUp(size_t child)//访问下标
		{
			size_t father = (child - 1) / 2;
			while (child > 0) //当child等于0说明已经调整到头节点
			{
				//默认调整为大堆
				if (_con[child] > _con[father])
				{
					swap(_con[child], _con[father]);
					child = father;
					father = (child - 1) / 2;
				}
				else
				{
					break;//如果_con[child] <= _con[father]则说明调整完成
				}
			}
		}

		//向下调整算法(已知父亲位置推算孩子位置)
		void AdjustDown(size_t father)//访问下标
		{
			//假设父亲的左孩子为较大数
			size_t child = father * 2 + 1;

			//默认调整为大堆
			while (child < _con.size())
			{
				//如果父亲存在右孩子且右孩子比左孩子大则进行修改
				if ((child + 1) < _con.size() && _con[child] < _con[child + 1])
				{
					child += 1;
				}

				if (_con[father] < _con[child])
				{
					swap(_con[father], _con[child]);
					father = child;
					child = father * 2 + 1;
				}
				else
				{
					break;//如果父亲比较大孩子的值还大则说明已经调整完成了
				}
			}
		}

		//插入数据(入优先级队列)
		void push(const T& x)
		{
			//首先尾插数据
			_con.push_back(x);

			//再将该数据通过向上调整算法使得数组保持堆的结构
			AdjustUp(_con.size() - 1);
		}

		//获取数据(队头数据)
		const T& top()
		{
			return _con[0];
		}

		//删除数据(出优先级队列)
		void pop()
		{
			//首先首尾数据进行交换,将堆顶数据进行尾删
			swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();

			//再将首位置数据通过向下调整算法使得数组保持堆的结构
			AdjustDown(0);
		}

		//获取队列数据个数
		size_t size()
		{
			return _con.size();
		}

		//判空
		bool empty()
		{
			return _con.size() == 0;
		}

	private:
		Container _con;
	};
}

测试代码:

cpp 复制代码
//Test.cpp
#include"Priority_Queue.h"

//测试priority_queue模拟实现
void test_priority_queue()
{
	MyPriorityQueue::priority_queue<int> pq;
	pq.push(4);
	pq.push(7);
	pq.push(2);
	pq.push(5);
	pq.push(10);
	pq.push(8);
	while (!pq.empty())
	{
		cout << pq.top() << " ";
		pq.pop();
	}
	cout << endl;
}

int main()
{
	test_priority_queue();
	return 0;
}

2、仿函数

我们会发现标准库里面实现的优先级队列有第三个模板参数,而且上面我们讲了当优先级队列不传第三个参数默认是建立"大堆",那我们如果想要优先级队列建立成"小堆"怎么实现呢?

就需要用到接下来讲解的------仿函数

仿函数 顾名思义就是像一个函数 ,所以其实仿函数并不是一个函数而是一个类 ,其是通过重载 operator() 让一个类实例化以后所生成的对象能够像函数一样使用

2.1 仿函数的结构

cpp 复制代码
//仿函数
//通过重载 operator() 让一个类实例化以后所生成的对象能够像函数一样使用
//比较器:用于大堆(父节点优先级高于子节点时,返回true触发交换)
template<class T>
struct Less
{
	bool operator()(const T& x, const T& y)
	{
		return x < y;
	}
};

2.2 仿函数测试代码

cpp 复制代码
//Test.cpp
//仿函数的测试
void test_Functor()
{
	//函数对象
	Less<int> LessFunc;
	cout << LessFunc(1, 2) << endl;
	//我们会发现 LessFunc(1, 2) 在结构上是非常类似函数的使用
	//但实际上就是一个对象调用了重载 operator() 的结果
	//也可以写出下面更加清晰的结构:
	cout << LessFunc.operator()(1, 2) << endl;
}

int main()
{
	test_Functor();
	return 0;
}

2.3 利用仿函数进行 priority_queue 底层实现

上面我们只是简单介绍一下仿函数,仿函数的使用远没有这么简单,只是这里我们讲解仿函数是为了实现模板参数比较器 Compare,让 priority_queue 不仅能实现大堆也能实现小堆:

cpp 复制代码
//Priority_Queue.h
//仿函数
	//通过重载 operator() 让一个类实例化以后所生成的对象能够像函数一样使用
	//比较器:用于大堆(父节点优先级高于子节点时,返回true触发交换)
template<class T>
struct Less
{
	bool operator()(const T& x, const T& y)
	{
		return x < y;
	}
};

//比较器:用于小堆(子节点优先级高于父节点时,返回true触发交换)
template<class T>
struct Greater
{
	bool operator()(const T& x, const T& y)
	{
		return x > y;
	}
};

namespace MyPriorityQueue
{
	template<class T, class Container = vector<T>, class Compare = Less<T>>
    // T:存储的元素类型;Container:底层容器(默认vector,需支持随机访问和尾部操作)
    // Compare:比较器(默认Less,即大堆)
	class priority_queue
	{
	public:
		//向上调整算法(已知孩子位置推算父亲位置)
		void AdjustUp(size_t child)//访问下标
		{
			Compare com;// 实例化比较器,用于判断优先级

			size_t father = (child - 1) / 2;
			while (child > 0) //当child等于0说明已经调整到头节点
			{
				//if (_con[child] > _con[father])
				if(com(_con[father], _con[child]))
                //需要注意Less是建立大堆,但是比较大小是 < ,父亲和孩子的顺序不要错
				{
					swap(_con[child], _con[father]);
					child = father;
					father = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}

		//向下调整算法(已知父亲位置推算孩子位置)
		void AdjustDown(size_t father)//访问下标
		{
			Compare com;// 实例化比较器,用于判断优先级

			size_t child = father * 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 += 1;
				}

				//if (_con[father] < _con[child])
				if (com(_con[father], _con[child]))
				{
					swap(_con[father], _con[child]);
					father = child;
					child = father * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}

		//插入数据(入优先级队列)
		void push(const T& x)
		{
			//首先尾插数据
			_con.push_back(x);

			//再将该数据通过向上调整算法使得数组保持堆的结构
			AdjustUp(_con.size() - 1);
		}

		//获取数据(队头数据)
		const T& top()
		{
			return _con[0];
		}

		//删除数据(出优先级队列)
		void pop()
		{
			//首先首尾数据进行交换,将堆顶数据进行尾删
			swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();

			//再将首位置数据通过向下调整算法使得数组保持堆的结构
			AdjustDown(0);
		}

		//获取队列数据个数
		size_t size()
		{
			return _con.size();
		}

		//判空
		bool empty()
		{
			return _con.size() == 0;
		}

	private:
		Container _con;
	};
}

2.4 测试代码演示

cpp 复制代码
//Test.cpp
//测试priority_queue模拟实现
void test_priority_queue()
{
	MyPriorityQueue::priority_queue<int> pq1;
	pq1.push(4);
	pq1.push(7);
	pq1.push(2);
	pq1.push(5);
	pq1.push(10);
	pq1.push(8);
	while (!pq1.empty())
	{
		cout << pq1.top() << " ";
		pq1.pop();
	}
	cout << endl;

	MyPriorityQueue::priority_queue<int, vector<int>, Greater<int>> pq2;
	pq2.push(4);
	pq2.push(7);
	pq2.push(2);
	pq2.push(5);
	pq2.push(10);
	pq2.push(8);
	while (!pq2.empty())
	{
		cout << pq2.top() << " ";
		pq2.pop();
	}
	cout << endl;
}

int main()
{
	test_priority_queue();
	return 0;
}

三、OJ实战练习:数组中第k个最大的元素

题目链接:

215. 数组中的第K个最大元素 - 力扣(LeetCode)

题目描述:

C++算法代码:

cpp 复制代码
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) 
    {
        //默认构造 + push
        // priority_queue<int> pq;
        // for(auto e : nums)
        // {
        //     pq.push(e);
        // }
        
        //迭代器区间构造(更简洁)
        priority_queue<int> pq(nums.begin(), nums.end());
        
        while(--k)
        {
            pq.pop();
        }
        return pq.top();
    }
};

结束语

到此,优先级队列也就讲解完了。作为 STL 中基于堆结构实现的容器适配器,priority_queue 用简洁的接口封装了复杂的堆调整逻辑,无论是默认的大堆排序,还是自定义类型的优先级适配,都能轻松应对 "按优先级取元素" 的需求。优先级队列功能的模拟实现本身是没有难度的,因为底层结构是基于之前所学的数据结构堆,本篇文章主要是对仿函数这个以前没有接触过在使用上像函数的类进行讲解。希望这篇文章对大家学习C++能有所帮助!

C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/

相关推荐
蜕变的小白2 小时前
数据结构:排序算法与哈希表
数据结构·算法·哈希算法
承渊政道2 小时前
C++学习之旅【C++继承概念指南与核心内容介绍】
c语言·开发语言·c++·笔记·学习·visual studio
_OP_CHEN2 小时前
【算法基础篇】(六十一)SG 函数通关指南:博弈论通用解法,从原理到实战秒杀各类 ICG 游戏
算法·蓝桥杯·c/c++·博弈论·acm/icpc·sg函数·有向图游戏
We་ct2 小时前
LeetCode 2. 两数相加:链表经典应用题详解
前端·算法·leetcode·链表·typescript
If using 10 days2 小时前
multiprocessing:创建并管理多个进程
python·算法
会周易的程序员2 小时前
openplc runtime v4 安全
网络·c++·物联网·websocket·安全·https·ssl
wu_asia2 小时前
每日一练壹
算法
程序员酥皮蛋2 小时前
hot 100 第二十二题 22.相交链表
数据结构·算法·leetcode·链表
maplewen.2 小时前
C++ 内存对齐
开发语言·c++