C++ 堆 的基础与 二叉堆(Binary Heap)
"堆" 在 C++ 里既是 动态内存分配池 ,也是一种 数据结构。下面先简要说明两者的区别,然后从头到尾讲解二叉堆的实现与操作,最后给出完整可运行的 C++ 代码示例。
1. 先说"堆"到底是什么?
| 语境 | 说明 | 关键点 |
|---|---|---|
| C++ 语义 | new/delete、malloc/free 所使用的 动态内存池 |
也叫 heap memory ,和栈(stack)相对;由操作系统管理 |
| 数据结构 | 一种 堆(Heap) ,比如 堆排序 、优先队列 等 | 与内存池无关;采用数组或链表实现 |
本文聚焦 数据结构的堆 ------ 二叉堆(Binary Heap)。
2. 二叉堆(Binary Heap)概念
2.1 什么是二叉堆?
二叉堆 是 堆 (Heap)的最常见实现形式,是一种 满足完全二叉树 + 堆序性质 的数据结构。
- 完全二叉树:除最后一层外,每层节点数都达到最大,且最后一层节点左对齐。
- 堆序性质 :
- 最小堆(Min-Heap):父节点 ≤ 所有子节点。
- 最大堆(Max-Heap):父节点 ≥ 所有子节点。
于是,二叉堆既是树 ,又是数组(后面会讲解为什么用数组实现)。
2.2 二叉堆的基本属性
| 属性 | 说明 |
|---|---|
| 节点数 | n |
| 高度 | ⌈log₂n⌉(复杂度分析时的常用量) |
| 父子关系(数组实现) | 取 0‑based: parent(i) = (i-1)/2, left(i) = 2i+1, right(i) = 2i+2 |
3. 用数组实现二叉堆
3.1 为什么用数组?
| 场景 | 说明 |
|---|---|
| 空间占用 | 只需要一段连续内存,节省指针开销(树指针占 8 bytes × 节点) |
| 指针运算 | 计算父子索引简单、常数时间 |
| Cache友好 | 连续内存访问使 CPU 缓存命中率高 |
注意:数组实现不需要额外维护节点对象,只存值即可。
3.2 ASCII 图表示(最简 7 节点最小堆)
4 4
/ \ / \
10 12 10 12
/ \ /
30 11 20 30 11 20
支持快速划分:
父节点 | 右子节点 | 左子节点
parent = (idx-1)/2
left = 2*idx+1
right = 2*idx+2
4. 主要操作
| 操作 | 完成步骤 | 复杂度 |
|---|---|---|
| Insert | ① 放到数组末尾 → ② 上滤(SiftUp)直到堆序 | O(log n) |
| Pop(取/删除根) | ① 把根元素保存 → ② 用数组最后一个元素填根 → ③ 下滤(SiftDown) | O(log n) |
| Peek | 直接返回 heap[0] |
O(1) |
| Build (building from scratch) | ① 直接复制数据到数组 → ② 从最后一个非叶子节点往上调用 SiftDown | O(n) |
| HeapSort | ① 用 Build 生成堆 → ② 每次弹出根(交换头尾元素) | O(n log n) |
下面展开 SiftUp 与 SiftDown 的细节。
4.1 上滤(Sift-Up,插入)
使用最小堆为例:
Insert(MinHeap, value):
1. Append value at the end of array (heap).
2. idx = size-1
3. while idx > 0:
parent = (idx-1)/2
if heap[parent] <= heap[idx]:
break // 维持堆序
swap(heap[parent], heap[idx])
idx = parent
上滤的核心是 不断跟父节点比较、必要时交换,直到找到正确的位置。
4.2 下滤(Sift-Down,取/删除根)
SiftDown(MinHeap, idx):
n = heap.size()
while true:
left = 2*idx + 1
right = 2*idx + 2
smallest = idx
if left < n && heap[left] < heap[smallest]:
smallest = left
if right < n && heap[right] < heap[smallest]:
smallest = right
if smallest == idx:
break
swap(heap[smallest], heap[idx])
idx = smallest
下滤从根向下选择最小子节点与当前节点交换,保持堆序。
4.3 建堆(Build-Heap)
BuildHeap(array A):
n = A.size()
heap = A // 直接用传入数组,省去复制
for i = (n-2)/2 downto 0: // 从最后一个非叶子节点开始
SiftDown(heap, i)
时间复杂度是 O(n),细节见这里。
5. C++ 代码实现
下面给出完整可编译 的最小堆 实现,支持 push, top, pop, size, empty, 与 STL std::priority_queue 对比。
cpp
#include <iostream>
#include <vector>
#include <stdexcept>
/* ----------- MinHeap ---------------- */
template<typename T>
class MinHeap {
public:
// 默认构造
MinHeap() = default;
// 用已有数据构建堆
explicit MinHeap(const std::vector<T>& data) : heap_(data) {
buildHeap();
}
// 获取堆顶(最小值)
const T& top() const {
if (heap_.empty()) throw std::out_of_range("Heap is empty");
return heap_[0];
}
// 插入
void push(const T& value) {
heap_.push_back(value);
siftUp(heap_.size() - 1);
}
// 弹出堆顶
void pop() {
if (heap_.empty()) throw std::out_of_range("Heap is empty");
if (heap_.size() == 1) {
heap_.pop_back();
return;
}
std::swap(heap_[0], heap_.back());
heap_.pop_back();
siftDown(0);
}
// 取堆大小
std::size_t size() const noexcept { return heap_.size(); }
bool empty() const noexcept { return heap_.empty(); }
private:
std::vector<T> heap_;
// 上滤
void siftUp(std::size_t idx) {
while (idx > 0) {
std::size_t parent = (idx - 1) / 2;
if (heap_[parent] <= heap_[idx]) break;
std::swap(heap_[parent], heap_[idx]);
idx = parent;
}
}
// 下滤
void siftDown(std::size_t idx) {
std::size_t n = heap_.size();
while (true) {
std::size_t left = 2 * idx + 1;
std::size_t right = 2 * idx + 2;
std::size_t smallest = idx;
if (left < n && heap_[left] < heap_[smallest])
smallest = left;
if (right < n && heap_[right] < heap_[smallest])
smallest = right;
if (smallest == idx) break;
std::swap(heap_[smallest], heap_[idx]);
idx = smallest;
}
}
// 建堆
void buildHeap() {
if (heap_.empty()) return;
for (std::size_t i = (heap_.size()-2)/2; i != static_cast<std::size_t>(-1); --i) {
siftDown(i);
}
}
};
/* --------- 例子:堆排序 ------------- */
template<typename T>
void heapSort(std::vector<T>& v) {
MinHeap<T> h(v); // 先构造堆
for (std::size_t i = 0; i < v.size(); ++i) {
v[i] = h.top(); // 取堆顶,每次弹出
h.pop();
}
}
/* --------- 主程序展示 ---------- */
int main() {
std::vector<int> data = {30, 10, 12, 4, 20, 11};
std::cout << "原始数组: ";
for (int x : data) std::cout << x << ' ';
std::cout << "\n";
MinHeap<int> heap(data); // 构建堆
std::cout << "堆顶: " << heap.top() << "\n"; // 最小值
std::cout << "插入 2\n";
heap.push(2);
std::cout << "现在堆顶: " << heap.top() << "\n";
std::cout << "弹出堆顶\n";
heap.pop();
std::cout << "现在堆顶: " << heap.top() << "\n";
std::cout << "堆排序后结果: ";
heapSort(data);
for (int x : data) std::cout << x << ' ';
std::cout << "\n";
return 0;
}
说明
MinHeap<int> heap(data);直接把已有数组用于构造堆(构造函数内部调用buildHeap())。heapSort先用MinHeap构造堆,再一次pop()取出并覆盖原数组 ------ 就是 堆排序。- 代码兼容 大多数C++标准(C++11 之后)。
6. 典型使用场景
| 需求 | 推荐实现 | 说明 |
|---|---|---|
| 实时优先级调度 | std::priority_queue 或自定义 MinHeap |
需要 O(log n) 插入/弹出 |
| 批量排序 | HeapSort(MinHeap 或 MaxHeap) |
O(n log n),无需额外空间(除数组) |
| 求 k 大/小 | 用 k 大/小堆维护 |
只保留 k 个元素,插入/弹出 O(log k) |
| 优化性能 | 用自定义小数组实现堆 | 减少堆顶访问开销 |
7. 与 std::priority_queue 对比
std::priority_queue |
自定义 MinHeap |
|
|---|---|---|
| 底层容器 | 默认是 std::vector |
std::vector |
| 比较器 | 默认最大堆(std::less<T>) |
自定义,支持最小堆 |
| API | push, top, pop, size, empty |
与标准一致 |
| 可扩展性 | 通过模板参数可自定义 comparator | 可以直接修改 siftUp/siftDown 逻辑 |
由于
std::priority_queue默认是 最大堆 ,若想做 最小堆,需要写一个比较器:
std::priority_queue<int, std::vector<int>, std::greater<int>> minPQ;
8. 小结
- 堆(Heap) 是一种 满足堆序的完全二叉树。
- 二叉堆 用 数组 直接实现,节省空间和时间,可通过
parent,left,right快速计算关系。 - 主要操作有 Insert(Sift‑Up) 、Pop(Sift‑Down) 、Build‑Heap,时间复杂度 O(logn)O(logn) 或 O(n)O(n)。
- C++ 里既有 STL
priority_queue(包装好的二叉堆),也可以自己实现自定义MinHeap或MaxHeap。 - 二叉堆适用于 优先级调度 、堆排序 、top‑k 等多种场景。
通过上述代码与示例,你可以在生产环境中直接使用自定义二叉堆,也可以轻松理解 STL 提供的
priority_queue。祝编码愉快 🚀!