C++ priority_queue 底层:手撕二叉堆与 Top K 问题

C++ priority_queue 底层:手撕二叉堆与 Top K 问题

摘要 :priority_queue(优先队列)是算法题和工程开发中极高频的容器。但当你被问到"它的底层是什么?为什么默认是大堆?为什么底层容器默认是 vector 而不是 deque?"时,很多人只能背出"堆"这个字。本文用一张图拆解二叉堆的上浮与下沉,并手写核心骨架代码,最后用 Top K 问题实战演练。

一、一个反直觉的默认行为

先看一段代码:

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

int main() {
    priority_queue<int> pq;
    pq.push(3);
    pq.push(1);
    pq.push(4);
    pq.push(2);
    cout << pq.top() << endl;  // 输出 4,而不是 3
}

为什么 top() 不是最先插入的 3,而是最大的 4?

因为 priority_queue 不是普通的先进先出队列,它是容器适配器 ,底层默认维护一个大顶堆(Max-Heap) 。每次 top() 返回的都是当前堆中优先级最高的元素(默认数值最大的)。

二、一张图看懂二叉堆:数组里的完全二叉树

priority_queue 的底层是一个完全二叉树 ,并且它被完美地映射到了数组(vector)中。

复制代码
大顶堆的物理存储(vector):
下标:  0    1    2    3    4    5
数值: [9,  7,   8,   2,   5,   6]

对应的完全二叉树:
        9 (0)
       /     \
    7 (1)   8 (2)
    /  \    /
 2 (3) 5(4) 6(5)

核心规则:

  • 任意节点的值 ≥ 其左右子节点的值(大顶堆)。
  • 对于下标为 i 的节点:
    • 左孩子下标:2 * i + 1
    • 右孩子下标:2 * i + 2
    • 父节点下标:(i - 1) / 2

📌 核心结论 :堆的根节点(下标 0)永远是整个数组中的最大值 。这就是 top() 的 O(1) 复杂度的来源。

三、堆的两大核心操作:上浮与下沉

3.1 插入元素:向上调整(AdjustUp)

当新元素插入到数组尾部时,它可能破坏大顶堆的性质。此时需要向上调整:不断与父节点比较,如果比父节点大,就交换,直到满足条件或到达根节点。

cpp 复制代码
// 向上调整(大顶堆)
void AdjustUp(vector<int>& heap, int child) {
    int parent = (child - 1) / 2;
    while (child > 0) {
        if (heap[parent] < heap[child]) {
            swap(heap[parent], heap[child]);
            child = parent;
            parent = (child - 1) / 2;
        } else {
            break; // 已经满足堆性质
        }
    }
}

3.2 删除堆顶:向下调整(AdjustDown)

pop() 操作会移除堆顶(最大值),然后把数组最后一个元素放到堆顶,再进行向下调整:不断与较大的子节点比较,如果比子节点小,就交换,直到满足条件或到达叶子节点。

cpp 复制代码
// 向下调整(大顶堆)
void AdjustDown(vector<int>& heap, int parent, int size) {
    int child = 2 * parent + 1;  // 左孩子
    while (child < size) {
        // 选出左右孩子中较大的一个
        if (child + 1 < size && heap[child] < heap[child + 1]) {
            ++child;
        }
        if (heap[parent] < heap[child]) {
            swap(heap[parent], heap[child]);
            parent = child;
            child = 2 * parent + 1;
        } else {
            break;
        }
    }
}

💡 面试金句 :push 是上浮 ,pop 是下沉。上浮和下沉的路径长度最多为树高 O(log n)。

四、为什么底层默认是 vector,而不是 deque?

priority_queue 的模板声明如下:

cpp 复制代码
template <
    class T,
    class Container = std::vector<T>,  // 默认底层容器
    class Compare = std::less<typename Container::value_type>  // 默认大堆
> class priority_queue;

为什么必须是 vector(或 deque),不能用 list?

因为堆的 AdjustUp 和 AdjustDown 需要随机访问 (通过下标 i 直接访问 heap[i])。list 不支持 O(1) 随机访问,因此被排除。

为什么默认选 vector 而不是 deque?

虽然 deque 也支持随机访问,但 vector 的内存连续性更好,CPU 缓存命中率更高 。堆操作需要频繁地上下跳跃访问,连续内存的局部性优势非常明显。此外,vector 的迭代器是原生指针,实现更简单。

五、实战:用 priority_queue 解决 Top K 问题

面试高频题:"从 100 万个数据中找出最大的 K 个。"

关键思路 :维护一个大小为 K 的小顶堆(注意:找最大 K 个,反而用小顶堆)。

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

vector<int> topK(vector<int>& nums, int k) {
    // 小顶堆:堆顶是当前 K 个元素中最小的
    priority_queue<int, vector<int>, greater<int>> minHeap;
    
    for (int x : nums) {
        if (minHeap.size() < k) {
            minHeap.push(x);
        } else if (x > minHeap.top()) {
            minHeap.pop();    // 淘汰最小的
            minHeap.push(x);  // 加入更大的
        }
    }
    
    // 堆中剩下的 K 个就是最大的 K 个
    vector<int> result;
    while (!minHeap.empty()) {
        result.push_back(minHeap.top());
        minHeap.pop();
    }
    return result;
}

复杂度:时间 O(n log K),空间 O(K)。如果 K 很小(比如 10),性能远超全量排序的 O(n log n)。

⚠️ 避坑 :找"最大 K 个"必须用小顶堆 ,找"最小 K 个"用大顶堆。面试时先想清楚再写代码。

六、面试高频 FAQ

Q1:priority_queue 是容器还是容器适配器?

是容器适配器 。它本身不维护数据结构,而是包装了 vector(默认),并调用 push_heap / pop_heap 等算法来维护堆性质。

Q2:如何让 priority_queue 变成小顶堆?

两种方式:

  • priority_queue<int, vector<int>, greater<int>> pq;(推荐)
  • 自定义仿函数 struct Compare { bool operator()(int a, int b) { return a > b; } };

Q3:priority_queue 的 push 和 pop 的时间复杂度是多少?

push 需要上浮,O(log n);pop 需要下沉,O(log n);top 是 O(1)。

Q4:priority_queue 可以遍历吗?

不可以 。它只暴露 top(),不提供迭代器。因为堆的内部顺序并不等同于优先级顺序,遍历没有意义。如需遍历,应使用 vector 加 make_heap 手动管理。

七、总结与挑战

核心点 结论
底层结构 完全二叉树 → 数组存储
默认行为 大顶堆,top() 返回最大值
核心操作 push 上浮,pop 下沉,均 O(log n)
底层容器 默认 vector(随机访问 + 缓存友好)
Top K 口诀 最大 K 个用小顶堆,最小 K 个用大顶堆

核心结论 :priority_queue 是"用数组存树"的经典范例。理解上浮与下沉,就理解了堆的核心;理解 Top K 的对偶关系,就掌握了这道面试题的灵魂。

评论区互动 :

你在算法题或项目中用过 priority_queue 吗?

扣1 :用过,主要用来做 Top K。

扣2 :用过,用来做任务调度/合并 K 个链表。

扣3 :还没用过,今天第一次学。

评论区见!


🔥 系列爆款推荐 :

👉 [C++ unordered_map 底层:一张图看懂哈希冲突与扩容](#C++ unordered_map 底层:一张图看懂哈希冲突与扩容)

👉 [C++ vector 迭代器失效:一张图+3句话,面试直接背](#C++ vector 迭代器失效:一张图+3句话,面试直接背)

相关推荐
qetfw1 小时前
Linux 文件清理:find、mtime 与安全传参验证
linux·shell·1024程序员节
liubinkaixin1 小时前
一炉硬质合金的温度曲线,卡在几十根补偿导线上
物联网·1024程序员节
蓝祁子yoky1 小时前
Qt学习日记 Day8:事件系统
1024程序员节
xianghongtao01161 小时前
04_ToM2Vec_解读报告
1024程序员节
夕除1 小时前
redis--IO 内核
1024程序员节
handsome_sai1 小时前
mysql索引
sql·1024程序员节
灵境(虚幻知音)1 小时前
华为超节点系统对军事体系仿真分布式计算的需求影响与技术路线变革研究
1024程序员节
Oiiouui1 小时前
针对Godot-GridGame(网格游戏)的DI注入+RefCounted数据驱动信号流架构设计的研究(全程开源)
游戏·游戏引擎·godot·1024程序员节
坤坤子吖1 小时前
C++继承:友元、静态成员、多继承与组合
开发语言·c++·笔记·学习·1024程序员节