de风——【从零开始学C++】(十三):优先级队列 priority_queue 全解析 & 仿函数入门

📌 专栏说明:本专栏「从零开始学C++」面向完全零基础的同学,每篇文章聚焦一个核心概念,用最通俗的语言 + 完整可运行的代码帮你把知识点吃透。

C++相关专题链接🔗:

https://blog.csdn.net/xiao_running/category_13158015.html?fromshare=blogcolumn&sharetype=blogcolumn&sharerId=13158015&sharerefer=PC&sharesource=Xiao_running&sharefrom=from_link

本文主题priority_queue(优先级队列)的基本使用与模拟实现 + 仿函数(函数对象)

作者:小小de风呀


🧭 前言

在上一篇中,我们已经学习了 stack(栈)和 queue(队列)。今天来认识一个功能更强大的"特殊队列"------ priority_queue(优先级队列)

普通队列是"先进先出",而优先级队列则是"按优先级出队"------每次出队的永远是当前优先级最高(或最低)的那个元素。

此外,为了能灵活地自定义"谁的优先级更高",我们还会介绍一个非常实用的 C++ 特性------仿函数(Functor)

学会这两个知识点,你处理排序、调度、算法竞赛题目的能力将大幅提升!


一、priority_queue 基本使用

简介与作用

priority_queue 是 C++ STL 中的优先级队列 容器适配器,定义在头文件 <queue> 中。

它的底层是一个堆(Heap)结构,默认是大堆(最大堆) ,即堆顶永远是所有元素中最大的那个

常用操作一览:

操作 说明
push(val) 插入元素
pop() 删除堆顶元素(最大值)
top() 访问堆顶元素(只读)
empty() 判断是否为空
size() 返回元素个数

模板参数(三个):

复制代码
priority_queue<Type, Container, Compare>
  • Type:元素类型
  • Container:底层容器,默认 vector<Type>
  • Compare:比较方式,默认 less<Type>(大堆)

💡 大堆 vs 小堆

  • 默认 less<T>大堆,堆顶是最大值(最大优先)
  • 改用 greater<T>小堆,堆顶是最小值(最小优先)

代码示例

示例一:默认大堆的基本用法

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

int main() {
    priority_queue<int> pq;  // 默认大堆

    pq.push(3);
    pq.push(1);
    pq.push(7);
    pq.push(5);
    pq.push(2);

    cout << "依次弹出(从大到小):";
    while (!pq.empty()) {
        cout << pq.top() << " ";  // 查看堆顶
        pq.pop();                  // 弹出堆顶
    }
    cout << endl;

    return 0;
}

输出结果:

复制代码
依次弹出(从大到小):7 5 3 2 1

✅ 无论插入顺序如何,每次弹出的都是当前最大值。


示例二:使用 greater 构建小堆

cpp 复制代码
#include <iostream>
#include <queue>
#include <functional>   // greater 在此头文件中
using namespace std;

int main() {
    // 小堆:堆顶是最小值
    priority_queue<int, vector<int>, greater<int>> minPQ;

    minPQ.push(3);
    minPQ.push(1);
    minPQ.push(7);
    minPQ.push(5);
    minPQ.push(2);

    cout << "依次弹出(从小到大):";
    while (!minPQ.empty()) {
        cout << minPQ.top() << " ";
        minPQ.pop();
    }
    cout << endl;

    return 0;
}

输出结果:

复制代码
依次弹出(从小到大):1 2 3 5 7

示例三:存放自定义结构体(按分数排序)

当元素是自定义类型时,需要告诉 priority_queue 怎么比较大小。

cpp 复制代码
#include <iostream>
#include <queue>
#include <string>
using namespace std;

struct Student {
    string name;
    int score;

    // 重载 < 运算符,让 priority_queue 知道如何比较
    // 分数高的优先级更高(大堆逻辑)
    bool operator<(const Student& other) const {
        return score < other.score;  // 分数小的"小",所以分数大的排前面
    }
};

int main() {
    priority_queue<Student> pq;

    pq.push({"小明", 88});
    pq.push({"小红", 95});
    pq.push({"小刚", 76});
    pq.push({"小美", 100});

    cout << "按分数从高到低出队:" << endl;
    while (!pq.empty()) {
        Student s = pq.top();
        pq.pop();
        cout << s.name << "  " << s.score << " 分" << endl;
    }

    return 0;
}

输出结果:

复制代码
按分数从高到低出队:
小美  100 分
小红  95 分
小明  88 分
小刚  76 分

⚠️ 经典 Bug:误用 top() 后未 pop(),陷入死循环

cpp 复制代码
// ❌ 错误写法
while (!pq.empty()) {
    cout << pq.top() << endl;
    // 忘记写 pq.pop(),堆顶永远不变 → 死循环!
}

// ✅ 正确写法
while (!pq.empty()) {
    cout << pq.top() << endl;
    pq.pop();  // 必须弹出,才能访问下一个元素
}

二、priority_queue 的底层原理:堆

简介与作用

要真正理解 priority_queue,就必须了解它的底层数据结构------堆(Heap)

堆是一棵满足特定规则的完全二叉树

  • 大堆(最大堆):每个节点的值 ≥ 其所有子节点的值。根节点是最大值。
  • 小堆(最小堆):每个节点的值 ≤ 其所有子节点的值。根节点是最小值。

堆通常用数组 来存储,对于下标为 i 的节点:

  • 左子节点:2*i + 1
  • 右子节点:2*i + 2
  • 父节点:(i - 1) / 2

核心操作:

  • 向上调整(AdjustUp):插入元素后,将新元素与父节点比较,若违反堆性质则交换,直到满足为止。
  • 向下调整(AdjustDown):删除堆顶后,将最后一个元素移到堆顶,再向下比较交换,直到满足为止。

💡 这就是为什么 pushpop 的时间复杂度都是 O(log n)------每次调整最多走树的高度步。


代码示例

示例四:可视化大堆的插入过程

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

// 向上调整(大堆)
void adjustUp(vector<int>& heap, int child) {
    while (child > 0) {
        int parent = (child - 1) / 2;
        if (heap[parent] < heap[child]) {   // 父节点比子节点小,违反大堆性质
            swap(heap[parent], heap[child]);
            child = parent;
        } else {
            break;
        }
    }
}

// 插入元素并打印堆当前状态
void pushAndPrint(vector<int>& heap, int val) {
    heap.push_back(val);
    adjustUp(heap, heap.size() - 1);

    cout << "插入 " << val << " 后,堆数组:[";
    for (int i = 0; i < (int)heap.size(); i++) {
        cout << heap[i];
        if (i != (int)heap.size() - 1) cout << ", ";
    }
    cout << "]  →  堆顶(最大值)= " << heap[0] << endl;
}

int main() {
    vector<int> heap;   // 用数组模拟堆

    cout << "====== 大堆插入过程演示 ======" << endl;
    pushAndPrint(heap, 3);
    pushAndPrint(heap, 1);
    pushAndPrint(heap, 7);
    pushAndPrint(heap, 5);
    pushAndPrint(heap, 2);

    cout << endl;
    cout << "最终堆数组:[";
    for (int i = 0; i < (int)heap.size(); i++) {
        cout << heap[i];
        if (i != (int)heap.size() - 1) cout << ", ";
    }
    cout << "]" << endl;

    // 验证堆的父子关系
    cout << endl;
    cout << "====== 验证父子节点关系 ======" << endl;
    for (int i = 1; i < (int)heap.size(); i++) {
        int parent = (i - 1) / 2;
        cout << "节点[" << i << "]=" << heap[i]
             << "  父节点[" << parent << "]=" << heap[parent]
             << (heap[parent] >= heap[i] ? "  ✓ 满足大堆" : "  ✗ 违反大堆!")
             << endl;
    }

    return 0;
}

输出结果:

复制代码
====== 大堆插入过程演示 ======
插入 3 后,堆数组:[3]  →  堆顶(最大值)= 3
插入 1 后,堆数组:[3, 1]  →  堆顶(最大值)= 3
插入 7 后,堆数组:[7, 1, 3]  →  堆顶(最大值)= 7
插入 5 后,堆数组:[7, 5, 3, 1]  →  堆顶(最大值)= 7
插入 2 后,堆数组:[7, 5, 3, 1, 2]  →  堆顶(最大值)= 7

最终堆数组:[7, 5, 3, 1, 2]

====== 验证父子节点关系 ======
节点[1]=5  父节点[0]=7  ✓ 满足大堆
节点[2]=3  父节点[0]=7  ✓ 满足大堆
节点[3]=1  父节点[1]=5  ✓ 满足大堆
节点[4]=2  父节点[1]=5  ✓ 满足大堆

✅ 结合图示和代码输出可以清晰地看到:每次插入后,adjustUp 都会自动把元素"冒泡"到正确的位置,始终保证堆顶是最大值。最终数组 [7, 5, 3, 1, 2] 与图示完全一致!


三、手动模拟实现 priority_queue

简介与作用

在学会使用 priority_queue 之后,我们来"造轮子"------自己动手实现一个简版的优先级队列

这样做的好处:

  1. 深刻理解堆的工作原理,不再是黑箱。
  2. 掌握向上调整、向下调整等核心算法。
  3. 为后续学习算法竞赛、操作系统调度等打基础。

我们实现的版本支持:pushpoptopemptysize,并且通过仿函数模板参数支持自定义比较规则(大堆/小堆均可),与 STL 的设计思路一致。


代码示例

示例五:完整手写 priority_queue(支持大堆/小堆切换)

cpp 复制代码
#include <iostream>
#include <vector>
#include <functional>   // less, greater
using namespace std;

template<class T, class Container = vector<T>, class Compare = less<T>>
class MyPriorityQueue {
private:
    Container _con;    // 底层容器,用数组(vector)模拟堆
    Compare   _comp;   // 比较器(仿函数)

    // 向上调整:插入新元素后维护堆性质
    void adjustUp(int child) {
        int parent = (child - 1) / 2;
        while (child > 0) {
            // _comp(a, b):若 a 的优先级"低于"b,则为 true
            // 大堆:less,当 parent < child 时,parent 优先级更低,需要交换
            if (_comp(_con[parent], _con[child])) {
                swap(_con[parent], _con[child]);
                child = parent;
                parent = (child - 1) / 2;
            } else {
                break;
            }
        }
    }

    // 向下调整:删除堆顶后维护堆性质
    void adjustDown(int parent) {
        int size = _con.size();
        int child = 2 * parent + 1;  // 左子节点

        while (child < size) {
            // 找左右子节点中优先级更高的那个
            if (child + 1 < size && _comp(_con[child], _con[child + 1])) {
                child++;  // 右子节点优先级更高
            }
            // 如果子节点优先级高于父节点,则交换
            if (_comp(_con[parent], _con[child])) {
                swap(_con[parent], _con[child]);
                parent = child;
                child = 2 * parent + 1;
            } else {
                break;
            }
        }
    }

public:
    // 插入元素
    void push(const T& val) {
        _con.push_back(val);
        adjustUp(_con.size() - 1);   // 新元素在末尾,向上调整
    }

    // 删除堆顶元素
    void pop() {
        if (empty()) return;
        swap(_con[0], _con[_con.size() - 1]);  // 堆顶与末尾交换
        _con.pop_back();                        // 删除末尾(原堆顶)
        adjustDown(0);                          // 新堆顶向下调整
    }

    // 访问堆顶
    const T& top() const {
        return _con[0];
    }

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

    size_t size() const {
        return _con.size();
    }
};

int main() {
    cout << "====== 大堆测试 ======" << endl;
    MyPriorityQueue<int> maxPQ;  // 默认 less<int>,大堆
    maxPQ.push(3);
    maxPQ.push(1);
    maxPQ.push(7);
    maxPQ.push(5);
    maxPQ.push(2);

    cout << "依次弹出:";
    while (!maxPQ.empty()) {
        cout << maxPQ.top() << " ";
        maxPQ.pop();
    }
    cout << endl;

    cout << "====== 小堆测试 ======" << endl;
    MyPriorityQueue<int, vector<int>, greater<int>> minPQ;  // greater<int>,小堆
    minPQ.push(3);
    minPQ.push(1);
    minPQ.push(7);
    minPQ.push(5);
    minPQ.push(2);

    cout << "依次弹出:";
    while (!minPQ.empty()) {
        cout << minPQ.top() << " ";
        minPQ.pop();
    }
    cout << endl;

    return 0;
}

输出结果:

复制代码
====== 大堆测试 ======
依次弹出:7 5 3 2 1
====== 小堆测试 ======
依次弹出:1 2 3 5 7

✅ 自己实现的 MyPriorityQueue 与 STL 的 priority_queue 行为完全一致!


⚠️ 经典 Bug:向下调整时忘记检查右子节点是否存在

cpp 复制代码
// ❌ 错误写法:直接比较 child 和 child+1,没有判断 child+1 是否越界
if (_comp(_con[child], _con[child + 1])) { // 当 child 是最后一个节点时,child+1 越界!
    child++;
}

// ✅ 正确写法:先判断 child+1 < size
if (child + 1 < size && _comp(_con[child], _con[child + 1])) {
    child++;
}

四、仿函数(Functor)

简介与作用

仿函数(Functor) ,也叫函数对象 ,是一种重载了 operator() 的类。重载之后,这个类的对象就可以像函数一样被调用。

听起来有点绕,看一个例子就清楚了:

cpp 复制代码
MyFunctor f;
f(3, 5);  // 这里 f 不是函数,而是一个对象,但它"长得像函数调用"

为什么需要仿函数?

  1. 可以携带状态:普通函数没有"记忆",而仿函数是对象,可以有成员变量记录状态。
  2. 灵活传递比较规则 :STL 容器(如 priority_queuesort)通过模板参数接收仿函数,实现自定义排序。
  3. 比函数指针更高效:仿函数调用可以被编译器内联优化,性能更好。

💡 你已经见过仿函数了!前面用到的 less<int>greater<int> 就是 STL 提供的标准仿函数。


代码示例

示例六:手写一个最简单的仿函数

cpp 复制代码
#include <iostream>
using namespace std;

// 定义一个仿函数类
class Add {
public:
    int operator()(int a, int b) const {
        return a + b;
    }
};

int main() {
    Add add;             // 创建仿函数对象
    cout << add(3, 5) << endl;   // 像函数一样调用,输出 8
    cout << add(10, 20) << endl; // 输出 30

    // 也可以直接用匿名对象调用
    cout << Add()(100, 200) << endl;  // 输出 300

    return 0;
}

输出结果:

复制代码
8
30
300

示例七:带状态的仿函数(记录调用次数)

cpp 复制代码
#include <iostream>
using namespace std;

class Counter {
private:
    int count = 0;  // 记录状态(调用次数)

public:
    void operator()() {
        count++;
        cout << "第 " << count << " 次被调用" << endl;
    }

    int getCount() const {
        return count;
    }
};

int main() {
    Counter c;
    c();   // 第 1 次
    c();   // 第 2 次
    c();   // 第 3 次

    cout << "总共调用了 " << c.getCount() << " 次" << endl;
    return 0;
}

输出结果:

复制代码
第 1 次被调用
第 2 次被调用
第 3 次被调用
总共调用了 3 次

✅ 普通函数做不到"记录自己被调用了多少次",但仿函数可以!


示例八:自定义仿函数用于 sort 排序

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 自定义比较仿函数:按绝对值从小到大排序
class AbsCmp {
public:
    bool operator()(int a, int b) const {
        return abs(a) < abs(b);
    }
};

int main() {
    vector<int> v = {-5, 3, -1, 8, -2, 6};

    // 使用自定义仿函数排序
    sort(v.begin(), v.end(), AbsCmp());

    cout << "按绝对值从小到大排序:";
    for (int x : v) {
        cout << x << " ";
    }
    cout << endl;

    return 0;
}

输出结果:

复制代码
按绝对值从小到大排序:-1 -2 3 -5 6 8

示例九:自定义仿函数用于 priority_queue

cpp 复制代码
#include <iostream>
#include <queue>
#include <string>
using namespace std;

struct Task {
    string name;
    int priority;   // 数字越大,优先级越高
};

// 自定义比较仿函数
struct TaskCmp {
    bool operator()(const Task& a, const Task& b) const {
        // 返回 true 表示 a 的优先级"低于"b(a 排在后面)
        return a.priority < b.priority;
    }
};

int main() {
    priority_queue<Task, vector<Task>, TaskCmp> taskQueue;

    taskQueue.push({"写作业", 2});
    taskQueue.push({"打游戏", 1});
    taskQueue.push({"吃饭",   5});
    taskQueue.push({"睡觉",   4});
    taskQueue.push({"写代码", 3});

    cout << "任务按优先级执行顺序:" << endl;
    while (!taskQueue.empty()) {
        Task t = taskQueue.top();
        taskQueue.pop();
        cout << "优先级 " << t.priority << " → " << t.name << endl;
    }

    return 0;
}

输出结果:

复制代码
任务按优先级执行顺序:
优先级 5 → 吃饭
优先级 4 → 睡觉
优先级 3 → 写代码
优先级 2 → 写作业
优先级 1 → 打游戏

示例十:STL 内置仿函数一览

C++ 在 <functional> 中提供了很多开箱即用的仿函数:

cpp 复制代码
#include <iostream>
#include <functional>
using namespace std;

int main() {
    // 算术仿函数
    cout << plus<int>()(3, 5) << endl;       // 3+5 = 8
    cout << minus<int>()(10, 3) << endl;     // 10-3 = 7
    cout << multiplies<int>()(4, 5) << endl; // 4*5 = 20
    cout << divides<int>()(10, 2) << endl;   // 10/2 = 5
    cout << modulus<int>()(10, 3) << endl;   // 10%3 = 1
    cout << negate<int>()(5) << endl;        // -5

    cout << "---" << endl;

    // 比较仿函数
    cout << less<int>()(3, 5) << endl;           // 3 < 5  → 1 (true)
    cout << greater<int>()(3, 5) << endl;        // 3 > 5  → 0 (false)
    cout << less_equal<int>()(5, 5) << endl;     // 5 <= 5 → 1 (true)
    cout << greater_equal<int>()(3, 5) << endl;  // 3 >= 5 → 0 (false)
    cout << equal_to<int>()(5, 5) << endl;       // 5 == 5 → 1 (true)
    cout << not_equal_to<int>()(3, 5) << endl;   // 3 != 5 → 1 (true)

    return 0;
}

输出结果:

cpp 复制代码
8
7
20
5
1
-5
---
1
0
1
0
1
1

五、综合练习:用 priority_queue 求 TopK 问题

简介与作用

TopK 问题 是一道经典的算法题:从 N 个数中找出最大(或最小)的 K 个数

priority_queue 解决 TopK 非常优雅:

  • 求前 K 大:维护一个大小为 K 的小堆,遍历完所有数后,堆中剩下的就是最大的 K 个。
  • 求前 K 小:维护一个大小为 K 的大堆,遍历完所有数后,堆中剩下的就是最小的 K 个。

代码示例

示例十一:求数组中前 K 个最大的数

cpp 复制代码
#include <iostream>
#include <queue>
#include <vector>
using namespace std;

// 返回数组中最大的 k 个数
vector<int> topK(vector<int>& nums, int k) {
    // 使用小堆,维护一个大小为 k 的窗口
    priority_queue<int, vector<int>, greater<int>> minHeap;

    for (int num : nums) {
        minHeap.push(num);
        if ((int)minHeap.size() > k) {
            minHeap.pop();  // 堆顶是最小值,超出 k 个时弹出最小值
        }
    }

    // 收集结果
    vector<int> result;
    while (!minHeap.empty()) {
        result.push_back(minHeap.top());
        minHeap.pop();
    }
    return result;
}

int main() {
    vector<int> nums = {3, 1, 7, 5, 2, 9, 4, 8, 6};
    int k = 4;

    vector<int> res = topK(nums, k);
    cout << "前 " << k << " 个最大的数:";
    for (int x : res) {
        cout << x << " ";
    }
    cout << endl;

    return 0;
}

输出结果:

复制代码
前 4 个最大的数:6 7 8 9

💡 用小堆(size = K)解决"最大 K 个",是非常经典的技巧,时间复杂度 O(N·log K),空间复杂度 O(K),效率远优于先排序再取前 K 个。


📝 总结

知识点 核心要点
priority_queue 默认行为 大堆,堆顶是最大值,头文件 <queue>
切换为小堆 第三个模板参数改为 greater<T>
三个模板参数 <元素类型, 底层容器, 比较器>
底层结构 完全二叉树(用数组存储),大/小堆
push 时间复杂度 O(log n),执行向上调整
pop 时间复杂度 O(log n),执行向下调整
仿函数定义 重载了 operator() 的类
仿函数优势 可携带状态、灵活传参、编译器可内联优化
STL 内置仿函数 lessgreaterplusminus 等,头文件 <functional>
TopK 经典用法 最大 K 个用小堆,最小 K 个用大堆

🚀 下期预告

掌握了 priority_queue 和仿函数之后,下一篇,我们将重点讲解 C++ 中至关重要的面向对象特性------继承,深入拆解继承的核心概念、语法规则、分类特性以及实际开发中的应用场景,助力大家进一步掌握 C++ 面向对象的编程精髓。


de风小提醒: 堆调整算法是数据结构中的重中之重,建议大家亲手写 3 遍以上,直到能闭着眼写出来为止!面试时这是最常考的手写算法之一。

如果本篇博客对你有帮助,欢迎点赞👍、收藏⭐、评论💬,你的支持是我创作的最大动力!

相关推荐
糖果店的幽灵1 小时前
时间序列处理
开发语言·python·pandas
王老师青少年编程1 小时前
信奥赛C++提高组csp-s之搜索进阶(记忆化搜索案例实践1)
c++·记忆化搜索·搜索·信奥赛·csp-s·提高组·滑雪
light blue bird1 小时前
3C 数码电子BOM 协同工作台组件
java·开发语言·jvm·windows·.net·桌面端
落羽的落羽1 小时前
【项目】JsonRpc框架——功能测试、项目总结
linux·服务器·开发语言·c++·qt·算法·机器学习
ZC跨境爬虫1 小时前
跟着 MDN 学JavaScript day_6:JavaScript 中的基础数学——数字与运算符
开发语言·前端·javascript·学习·ecmascript
Lucis__1 小时前
图的高阶算法:从构造最小生成树到求解最短路径问题
数据结构·c++·算法·图论
小小测试开发8 小时前
安装 Python 3.10+
开发语言·人工智能·python
好评1249 小时前
【C++】智能指针全解
c++·智能指针
AAA大运重卡何师傅(专跑国道)10 小时前
【无标题】
开发语言·c#