priority_queue的介绍和使用

优先队列的基本概念与实现原理

priority_queue文档介绍

优先队列是一种容器适配器,其核心特性是始终保证队列中的第一个元素(即队首元素)为当前所有元素中优先级最高的一个。在默认情况下,该"最高优先级"表现为数值上的最大值,因此优先队列在默认配置下表现为大根堆结构。这种设计使得优先队列特别适用于需要频繁获取最大(或最小)元素的场景,如任务调度、图算法中的最短路径计算等。

优先队列的底层实现基于堆(heap)数据结构 ,而堆是一种完全二叉树结构,满足父节点的值大于等于(或小于等于)其子节点值的性质。通过维护这一性质,可以确保每次访问顶部元素时都能快速获得全局最大(或最小)值,时间复杂度为常数级别 O ( 1 ) O(1) O(1)。

底层容器的选择与要求

优先队列作为容器适配器,将特定的标准容器类封装为其底层存储结构。这意味着它并不直接管理数据,而是依赖于所选容器来完成实际的数据存储和操作。常见的底层容器包括 std::vector 和 std::deque,二者均满足优先队列对容器的基本要求:

  • 支持随机访问迭代器;
  • 提供 empty() 接口以判断容器是否为空;
  • 提供 size() 接口返回有效元素数量;
  • 提供 front() 接口访问首个元素;
  • 支持 push_back() 向尾部插入元素;
  • 支持 pop_back() 删除尾部元素。

这些接口共同保障了堆结构的动态构建与维护。由于堆操作依赖于随机访问能力(例如在调整堆结构时需快速定位父节点或子节点),因此必须使用支持随机访问迭代器的容器。

默认容器与堆算法的自动维护机制

当未显式指定底层容器时,优先队列默认使用 std::vector 作为其存储容器。std::vector 具有良好的内存连续性、高效的插入与访问性能,且支持随机访问,非常适合用于堆的实现。

为了维持堆的有序性,优先队列内部通过调用标准库中的三个关键算法函数实现自动维护:

  • make_heap(first, last):将一段范围内的元素构造成一个合法的堆;
  • push_heap(first, last):在已构成堆的基础上,向末尾添加新元素并重新调整堆结构;
  • pop_heap(first, last):将堆顶元素移至末尾,并重新调整剩余元素形成新的堆。

这些操作由优先队列的成员函数(如 push()、pop())在内部自动触发,用户无需手动干预。这使得优先队列既具备高效性,又具有良好的封装性。

优先队列的核心接口详解

函数声明 功能说明
priority_queue()/priority_queue(first,last) 构造一个空的优先队列;也可从指定范围内的元素初始化队列,此时会自动调用 make_heap 构建初始堆结构。
empty() 判断优先队列是否为空。若无元素则返回 true,否则返回 false。时间复杂度为 O ( 1 ) O(1) O(1)。
top() 返回当前优先队列中优先级最高的元素(即堆顶元素)。该操作不改变队列内容,仅读取,时间复杂度为 O ( 1 ) O(1) O(1)。注意:若队列为空,调用此函数可能导致未定义行为。
push(const T& x) 将元素 x 插入优先队列。插入后自动调用 push_heap 保持堆结构,时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)。
pop() 移除堆顶元素。内部先执行 pop_heap 将最大元素移到末尾,再调用 pop_back 实际删除,时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)。

优先队列的本质:堆的封装

尽管优先队列提供了类似队列的操作语义(先进后出的逻辑顺序),但其实质是堆的高级封装。它利用堆的特性实现了"按优先级出队"的行为,而非传统的"先进先出"。因此,在任何需要动态维护最大值或最小值的场合,都可以考虑使用优先队列替代手动维护堆结构。

例如:

  • 在 Dijkstra 算法中,需要不断取出距离最小的节点;
  • 在合并多个有序链表时,需选择当前最小头节点;
  • 在实时系统中,优先处理高优先级任务。

上述场景均可通过优先队列高效解决。

自定义比较规则:小根堆的实现

默认情况下,优先队列是大根堆(即堆顶为最大值)。若需实现小根堆(堆顶为最小值),可通过自定义比较函数进行配置。例如:

cpp 复制代码
priority_queue<int, vector<int>, greater<int>> pq;

其中 greater<int> 是一个模板仿函数,表示"较小者优先",从而使得队列顶部始终为最小元素。

更复杂的自定义比较逻辑也可通过定义结构体或函数对象实现,灵活适应不同应用场景。

总结重点

  • 优先队列是基于堆结构的容器适配器,提供高效的最大/最小元素访问;
  • 默认使用 std::vector 作为底层容器,支持随机访问与动态扩展;
  • 所有堆操作均由内部算法自动完成,用户无需关心具体实现细节;
  • 核心操作如 push()、pop()、top() 均具有良好的时间复杂度保障;
  • 可通过自定义比较器切换为小根堆,增强适用性;
  • 优先队列本质上就是堆的封装,适用于所有需要动态维护极值的算法场景。

该设计兼顾了效率、灵活性与易用性,是现代 C++ 编程中不可或缺的重要工具之一。


priority_queue的模拟实现

priority_queue模拟实现

仿函数(Functor)的设计与实现原理

一个类若重载了 operator(),其对象便具备了像函数一样被调用的能力。这种对象称为仿函数 (Function Object),也叫函数对象 或函数式对象。

cpp 复制代码
template<class T>
class Less {
public:
    bool operator()(const T& x, const T& y) const {
        return x < y;
    }
};
  • operator() 的重载使得 Less<int> lessFunc; 创建的对象可以像函数一样使用:
    lessFunc(a, b) 等价于 lessFunc.operator()(a, b)。
  • 返回类型为 bool 比 int 更加语义清晰,符合比较操作的逻辑预期。
  • 由于是模板类,Less<int>、Less<double> 等可实例化为不同类型的比较器,只要元素支持 < 操作符即可。
为什么使用仿函数而非普通函数指针?
特性 仿函数 函数指针
可内联优化 ✅ 编译期确定具体类型,可直接展开 ❌ 通常无法内联,运行时跳转开销
支持状态 ✅ 可包含成员变量(如计数器、缓存等) ❌ 无状态,仅函数地址
类型即策略 ✅ 比较方式作为模板参数传递,编译期配置 ❌ 运行时传入函数地址,灵活性差
性能 ⭐ 零开销(空类可被完全优化) ⚠️ 调用开销不可忽略

示例:

cpp 复制代码
priority_queue<int, vector<int>, Greater<int>> pq;
// Compare = Greater<int> 是一个类型,它在编译时决定比较行为

这正是 STL 的核心设计思想:将策略抽象为类型,通过模板实现"类型即配置"。


priority_queue 模板类结构解析

cpp 复制代码
template<class T, class Container = vector<T>, class Compare = Greater<T>>
class priority_queue {
private:
    Container _con;           // 底层容器
    Compare _com;             // 比较器对象(仿函数)
    
    // 私有辅助函数
    void adjust_up(int child);
    void adjust_down(int parent);
public:
    // 构造函数
    priority_queue() = default;

    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);
    }

    // 插入元素
    void push(const T& x);

    // 删除堆顶
    void pop();

    // 获取堆顶元素
    const T& top() const;

    // 判空与大小
    bool empty() const { return _con.empty(); }
    size_t size() const { return _con.size(); }
};
模板参数详解
参数 含义 默认值
T 元素类型 ------
Container 底层容器类型(必须支持随机访问和 push_back) vector<T>
Compare 比较器类型(仿函数) Greater<T>

关键点 :默认使用 Greater<T>,意味着该优先队列是一个大顶堆(最大元素在顶部)。


建堆过程:heapify 算法详解

构造函数中:

cpp 复制代码
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);
}
1. 为何从 (n-1-1)/2 开始?
  • 完全二叉树中,下标从 0 开始。
  • 最后一个节点索引为 n-1。
  • 其父节点索引为 (n-1 - 1) / 2 = (n-2)/2。
  • 所以最后一个非叶子节点的下标是 (n-2)/2,即 (_con.size() - 1 - 1) / 2。
2. 时间复杂度分析
  • 若逐个插入并向上调整:O(n log n)
  • 使用 adjust_down 从底向上建堆:O(n)
  • 证明基于每层节点的下沉高度总和,数学上可证为线性。

举例:对数组 {4, 1, 3, 2, 16, 9, 10, 14, 8, 7} 建堆,只需从第 4 个节点(索引 4)开始调整。


插入操作:push 与 adjust_up

cpp 复制代码
void push(const T& x) {
    _con.push_back(x);              // 放到末尾(完全二叉树最后位置)
    adjust_up(_con.size() - 1);     // 向上调整至合适位置
}
向上调整逻辑(adjust_up)
cpp 复制代码
void adjust_up(int child) {
    Compare com;                    // 每次构造比较器对象
    size_t parent = (child - 1) / 2;

    while (child > 0) {
        if (com(_con[parent], _con[child])) {
            swap(_con[child], _con[parent]);
            child = parent;
            parent = (child - 1) / 2;
        } else {
            break;
        }
    }
}
核心解读:
  • 父节点公式:parent = (child - 1) / 2
  • 循环条件:child > 0,直到到达根节点为止。
  • com(parent, child) 返回 true 表示:父节点"不如"子节点 → 需要交换,让子节点上浮。
  • 交换后继续向上检查。
重要结论:
  • 当 Compare = Greater<T> 时,com(a,b) 即 a > b。
    • com(parent, child) 为真 ⇒ parent > child ⇒ 父比子大?但还要交换?
    • 实际上,这里 com(a,b) 的语义是:"a 是否应该让位给 b?"
    • 因此,当 parent > child 为真时,说明父更大,应保留;但代码却执行了交换!

矛盾吗?不!

我们来澄清这个关键点:

cpp 复制代码
if (com(_con[parent], _con[child]))
  • com(a, b) 为真 ⇒ a 应该让位给 b ⇒ b 更优 ⇒ b 应上浮。
  • 所以:如果 parent > child 且 Compare = Greater,则 com(parent, child) == true ⇒ 交换 ⇒ child 上浮。

这意味着:只有当子节点更"小"时才让它上浮 ,最终形成的是小堆(最小元素在顶部)。

但这与标准 priority_queue 的行为相反!

🚩 重大发现:本实现与标准库行为相反
比较器 堆性质 输出顺序
Greater<T> 小堆(最小值在顶) 升序出队
Less<T> 大堆(最大值在顶) 降序出队

而标准库 std::priority_queue 默认使用 std::less<T>,即大堆。

所以:当前实现中,Greater<T> 对应小堆,Less<T> 对应大堆 ,与标准库互补。

这是由 com(a,b) 的语义决定的:

"如果 a 不如 b,就交换" → 即"让 b 上浮",所以 com(a,b) 为真表示 b 更优。

因此,堆的形状取决于比较器如何定义"谁更优"。


删除操作:pop 与 adjust_down

cpp 复制代码
void pop() {
    swap(_con[0], _con[_con.size() - 1]);  // 1. 交换堆顶与末尾
    _con.pop_back();                        // 2. 移除末尾(原堆顶)
    adjust_down(0);                         // 3. 新堆顶向下调整
}
为什么不能直接删除 index=0?

index=0 意味着你正在访问一个数据序列中的第一个元素。它不是"第零个"元素,而是"第一个",只是编号方式从 0 开始。这种设计广泛应用于编程语言、数据分析工具和算法实现中。

  • 会破坏完全二叉树结构。
  • 必须保证树的连续性和层级完整性。
  • 正确做法:将堆顶替换为最后一个元素,再从根开始向下调整。
向下调整逻辑(adjust_down)
cpp 复制代码
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;
        }
    }
}
详细解释:
  • child = parent * 2 + 1:左孩子下标。
  • child + 1 < _con.size():判断右孩子是否存在。
  • com(_con[child], _con[child + 1]):若为真,说明左孩子"不如"右孩子 → 选右孩子。
  • 然后判断 com(_con[parent], _con[child]):若为真,说明父节点"不如"孩子 → 交换。
举个例子:

假设 Compare = Greater<T>,即 com(a,b) 为真当且仅当 a > b。

  • com(left, right) 为真 ⇒ left > right ⇒ right 更小 ⇒ 更优 ⇒ 选右孩子。
  • com(parent, child) 为真 ⇒ parent > child ⇒ 父更大 ⇒ 不应留在上面 ⇒ 交换。

结果:较小的元素不断上浮,形成小堆。


接口封装与性能保障

cpp 复制代码
const T& top() const {
    return _con[0];
}

bool empty() const {
    return _con.empty();
}

size_t size() const {
    return _con.size();
}
  • top():常量引用返回,避免拷贝,时间复杂度 O(1)。
  • empty()/size():转发到底层容器,无额外开销。

总结:核心机制与设计哲学

机制 说明
仿函数驱动 比较逻辑由 Compare 控制,支持自定义策略
类型即配置 比较方式作为模板参数,编译期决定行为
堆性质由比较器定义 com(a,b) 为真 ⇒ b 应上浮 ⇒ 决定堆的形态
大堆/小堆切换 Less<T> → 大堆(降序输出) Greater<T> → 小堆(升序输出)
建堆效率 O(n) 而非 O(n log n),利用 heapify 算法
内存布局 底层容器为 vector<T>,支持随机访问,便于堆操作

完整测试示例(验证行为)

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>

// 假设已有 Less / Greater / priority_queue 定义

int main() {
    // 测试:使用 Greater<T> → 小堆 → 升序出队
    priority_queue<int, vector<int>, Greater<int>> pq1;
    pq1.push(5);
    pq1.push(3);
    pq1.push(7);
    pq1.push(1);

    std::cout << "Greater<int> 堆(小堆): ";
    while (!pq1.empty()) {
        std::cout << pq1.top() << " ";
        pq1.pop();
    }
    std::cout << "\n"; // 输出: 1 3 5 7

    // 测试:使用 Less<T> → 大堆 → 降序出队
    priority_queue<int, vector<int>, Less<int>> pq2;
    pq2.push(5);
    pq2.push(3);
    pq2.push(7);
    pq2.push(1);

    std::cout << "Less<int> 堆(大堆): ";
    while (!pq2.empty()) {
        std::cout << pq2.top() << " ";
        pq2.pop();
    }
    std::cout << "\n"; // 输出: 7 5 3 1

    return 0;
}

结论

  • 本实现展示了 priority_queue 的底层机制。
  • 重点在于:比较器决定了堆的性质。
  • Greater<T> 在此处构建的是小堆 ,与标准库 std::priority_queue 的默认行为相反。
  • 设计精妙之处在于:用仿函数实现策略解耦,类型即配置,零开销,高内联性。
  • 理解 com(a,b) 的语义是掌握堆逻辑的关键:"a 是否应让位给 b?"

✅ 正确理解:com(a,b) 为真 ⇒ b 更优 ⇒ b 应上浮 ⇒ 堆中"更优"的元素靠近根。

相关推荐
驭渊的小故事1 小时前
MyBatis 入门:从 JDBC 到优雅操作数据库(一)
笔记·sql·spring
量子-Alex1 小时前
【大模型强化学习后训练】强化学习后训练算力应投向何处?模型规模、搜索、学习与反馈
人工智能·学习
茉莉玫瑰花茶1 小时前
OpenGL [ 纹理 ]
开发语言·c++·opengl
余额瞒着我当琳2 小时前
C++ map 底层与AVL 树
开发语言·c++·算法
All for pursuit.2 小时前
【贪心-2】253.会议室 II
数据结构·c++·算法·leetcode
重生之小比特3 小时前
【C++进阶】异常
开发语言·c++
阳光九叶草LXGZXJ3 小时前
达梦数据库-学习-68-dmasm0X_XXXXXX.log日志激增
linux·运维·数据库·sql·学习
朝朝辞暮i3 小时前
C++ 第 15 课:指针 * 和地址 &
开发语言·c++
奶茶树3 小时前
【C++】14. C++11新特性【下】
开发语言·c++