【C++】priority_queue以及 仿函数 的学习

在 C++ 的 STL 容器中,priority_queue(优先级队列)是基于堆(Heap)结构实现的自适应容器适配器,它能保证队列队首元素始终是当前队列中优先级最高 / 最低的元素,无需手动维护堆结构,是处理「优先级调度、TOP-K 问题、贪心算法」等场景的核心工具。


深度剖析 C++ priority_queue

一、priority_queue 核心认知

1. 定义与本质

priority_queue 是容器适配器(而非底层容器),STL 提供的「容器适配器」是基于底层容器(如 vectordeque)封装的接口,屏蔽底层实现细节,只暴露符合需求的上层接口。

它的核心特性:

  • 有序性:队首元素永远是当前优先级极值(最大或最小);
  • 底层结构:默认使用大顶堆(max-heap),底层依赖 vector 存储数据;
  • 访问限制:只能访问队首元素(top()),无法随机访问或遍历中间元素;
  • 操作效率:插入、删除堆顶元素的时间复杂度为 O(log n),访问堆顶为 O (1)。

2. 与普通队列(queue)的区别

普通队列遵循 FIFO(先进先出) 规则,而 priority_queue 完全打破顺序限制,只按优先级排序:

特性 queue(普通队列) priority_queue(优先级队列)
出队规则 先进先出 优先级最高 / 最低先出
元素顺序 插入顺序 动态维护优先级堆序
访问方式 队首(front)、队尾(back) 仅队首(top)
底层实现 deque/list 堆(heap)+ 底层容器(默认 vector)

3. 底层堆原理

priority_queue 底层是完全二叉堆(Complete Binary Heap),分为两种:

  1. 大顶堆:父节点值 ≥ 子节点值,堆顶是最大值(默认);
  2. 小顶堆:父节点值 ≤ 子节点值,堆顶是最小值。

堆的存储:用连续数组(vector) 模拟完全二叉树,索引规则:

  • 父节点索引:i
  • 左子节点索引:2*i + 1
  • 右子节点索引:2*i + 2

STL 提供 make_heappush_heappop_heap 算法,priority_queue 直接封装这些算法,无需手动调用。


二、priority_queue 基础语法

1. 头文件

priority_queue 定义在 <queue> 头文件中,使用前必须包含:

复制代码
#include <queue>

2. 模板原型

C++ 中 priority_queue 的完整模板声明:

复制代码
template <class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue;

3. 核心成员函数

函数 功能说明
push(val) 插入元素 val,自动调整堆结构
pop() 删除堆顶元素(优先级最高 / 最低),自动调整堆
top() 返回堆顶元素的引用(只读,不删除)
empty() 判断队列是否为空,空返回 true
size() 返回队列中元素的个数
emplace(args) 原地构造元素,比 push 更高效(C++11)
swap(pq) 交换两个优先级队列的内容

代码实现

1. 比较器设计

复制代码
template<class T>
class Less
{
public:
    bool operator()(const T& x, const T& y) const
    {
        return x < y;  // 用于构建最大堆
    }
};

template<class T>
class Greater
{
public:
	bool operator()(const T& x, const T& y) const
	{
		return x > y;
	}
}

两种策略:

  • Less:创建最大堆(默认)

  • Greater:创建最小堆


2. 优先队列类模板

复制代码
template<class T, class Container=std::vector<T>, class Compare=Less<T>>
class priority_queue
{
    // 实现细节...
};

模板参数说明:

  • T:元素类型

  • Container:底层容器,默认std::vector<T>

  • Compare:比较器类型,默认Less<T>


三、核心算法详解

二叉堆使用数组(或向量)存储,具有以下父子节点关系:

  • 父节点索引:parent = (child - 1) / 2

  • 左子节点:left_child = parent * 2 + 1

  • 右子节点:right_child = parent * 2 + 2

1. 向上调整(adjust_up)

复制代码
void adjust_up(int child)
{
    Compare com;
    int 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;  // 堆性质已满足
        }
    }
}

算法流程:

  1. 比较子节点与其父节点的优先级

  2. 如果子节点优先级更高,交换它们

  3. 继续向上比较,直到根节点或满足堆性质

  4. 时间复杂度:O(log n)

2. 向下调整(adjust_down)

复制代码
void adjust_down(int 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[child], _con[parent]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            break;  // 堆性质已满足
        }
    }
}

算法流程:

  1. 找到父节点的两个子节点

  2. 选择优先级更高的子节点

  3. 比较父节点与该子节点的优先级

  4. 如果子节点优先级更高,交换它们

  5. 继续向下调整,直到叶子节点或满足堆性质

  6. 时间复杂度:O(log n)


四、主要操作实现

1. 插入操作(push)

复制代码
void push(const T& x)
{
    _con.push_back(x);            // 步骤1:放入末尾
    adjust_up(_con.size() - 1);   // 步骤2:向上调整
}

2. 删除操作(pop)

复制代码
void pop()
{
    if (empty()) return;                      // 边界检查
    swap(_con[0], _con[_con.size() - 1]);    // 步骤1:交换首尾
    _con.pop_back();                          // 步骤2:删除尾部
    if (!empty())
        adjust_down(0);                       // 步骤3:向下调整
}

3. 构建堆(Heapify)

复制代码
priority_queue(InputIterator first, InputIterator last)
    :_con(first, last)
{
    // 从最后一个非叶子节点开始向下调整
    for (int i = (_con.size() - 2) / 2; i >= 0; i--)
    {
        adjust_down(i);
    }
}

算法分析:

  • 最后一个非叶子节点索引:(size - 2) / 2

  • 自底向上构建堆

  • 时间复杂度:O(n),比逐个插入的O(n log n)更高效


五、设计模式与优化

1. 策略模式的应用

通过模板参数Compare实现策略模式,用户可自定义比较逻辑

复制代码
// 最大堆(默认)
wxx::priority_queue<int> max_heap;

// 最小堆
wxx::priority_queue<int, std::vector<int>, Greater<int>> min_heap;

// 自定义比较器
struct CompareByLength {
    bool operator()(const string& a, const string& b) const {
        return a.length() < b.length();  // 按长度构建最大堆
    }
};
wxx::priority_queue<string, vector<string>, CompareByLength> str_heap;

2. 容器适配器模式

优先队列是典型的容器适配器:

  • 不直接管理内存

  • 依赖于底层容器(默认std::vector

  • 提供统一的接口抽象

3. 迭代器支持

通过迭代器构造函数,支持从各种容器初始化:

复制代码
std::vector<int> vec = {3, 1, 4, 1, 5};
wxx::priority_queue<int> pq(vec.begin(), vec.end());

六、实际应用场景

1. 任务调度系统

复制代码
struct Task {
    int priority;
    string name;
    // 重载<运算符,优先级数值小的先执行
    bool operator<(const Task& other) const {
        return priority > other.priority;  // 最小堆
    }
};

wxx::priority_queue<Task> task_queue;

2. 求Top K问题

复制代码
// 求数组中最大的k个元素
vector<int> findTopK(const vector<int>& nums, int k) {
    wxx::priority_queue<int, vector<int>, Greater<int>> min_heap;
    
    for (int num : nums) {
        if (min_heap.size() < k) {
            min_heap.push(num);
        } else if (num > min_heap.top()) {
            min_heap.pop();
            min_heap.push(num);
        }
    }
    
    vector<int> result;
    while (!min_heap.empty()) {
        result.push_back(min_heap.top());
        min_heap.pop();
    }
    return result;
}

3. 合并K个有序链表

复制代码
struct ListNode {
    int val;
    ListNode* next;
    bool operator<(const ListNode* other) const {
        return val > other->val;  // 最小堆
    }
};

ListNode* mergeKLists(vector<ListNode*>& lists) {
    wxx::priority_queue<ListNode*, vector<ListNode*>, 
                       function<bool(ListNode*, ListNode*)>> 
        pq([](ListNode* a, ListNode* b) { return a->val > b->val; });
    
    // 合并逻辑...
}

仿函数

1. 仿函数是什么?

是一个对象,里面可以有成员变量 + 重载 operator ()。

复制代码
struct Cmp {
    bool operator()(int a, int b) {
        return a > b;
    }
};

特点:

  • 是对象
  • 可以存数据(状态)
  • 可以被模板识别
  • 编译器能直接内联,0 开销

2. 函数指针是什么?

就是一个变量,存的是函数的内存地址。

复制代码
bool cmp(int a, int b) { return a > b; }

// 函数指针
bool (*fp)(int, int) = cmp;

特点:

  • 只是个地址
  • 不携带任何数据
  • 无法保存状态
  • 编译器很难优化
功能 函数指针 仿函数
本质 函数地址 对象
能否做模板参数 ❌ 不能 ✅ 可以
能否保存状态(成员变量) ❌ 不能 ✅ 可以
效率 低(函数调用) 极高(可内联)
多态能力
STL 支持 完美支持

相关推荐
风味蘑菇干43 分钟前
斗地主案例
java·数据结构·算法
码农学院1 小时前
itextsharp .net中如何设置两个表格的间距设为0,取网站的域名,协议、端口、当前站点目录的地址
开发语言·c#·.net
宠..1 小时前
VS Code 修改 C++ 标准同时修改错误检测标准
java·linux·开发语言·javascript·c++·python·qt
小+不通文墨1 小时前
树莓派4b-wiringpi库的安装和使用
驱动开发·经验分享·笔记·嵌入式硬件·学习
WL_Aurora1 小时前
Java Scanner输入陷阱深度解析
java·开发语言
Han_han9191 小时前
斗地主案例:
java·开发语言
赏金术士1 小时前
Kotlin Flow 完全指南
android·开发语言·kotlin
石榴树下的七彩鱼1 小时前
AI抠图效果实测:基于Python的3种背景移除模型对比
开发语言·人工智能·python·ai抠图·石榴智能·背景移除·rmbg
洛水水1 小时前
【力扣100题】39.二叉树的最近公共祖先
算法·leetcode·职场和发展