在 C++ 中,"堆"(Heap)可以指两种不同的概念:
- 堆数据结构(Heap Data Structure):一种特殊的树形数据结构,常用于实现优先队列等。
- 内存堆(Memory Heap):用于动态内存分配的区域,通过
new
和delete
操作符进行管理。
堆数据结构 及其在 C++ 标准库中的应用,同时简要提及 内存堆 的概念。
1. 堆数据结构(heap data structure)
什么是堆?
堆(Heap)是一种特殊的完全二叉树 ,它分为最大堆(Max Heap)和 最小堆(Min Heap)两种形式,分别满足不同的性质。堆广泛应用于优先队列 、排序算法(如堆排序)等场景。
- 最大堆(Max Heap):每个父节点的值都大于或等于其子节点的值。堆顶(根节点)是整个堆中最大的元素。
- 最小堆(Min Heap):每个父节点的值都小于或等于其子节点的值。堆顶(根节点)是整个堆中最小的元素。
堆的性质
- 完全二叉树:堆是一种完全二叉树,因此它的所有节点都尽可能靠左对齐。
- 父子节点关系:在最大堆中,父节点的值大于等于其子节点的值;在最小堆中,父节点的值小于等于其子节点的值。
- 常用于优先队列:堆可以快速找到最大值或最小值,这使得它非常适合实现优先队列,插入和删除操作的时间复杂度为 O(log n)。
堆的常见操作
- 插入元素:将新元素放在堆的末尾,然后向上调整("上浮"),直到满足堆的性质。
- 删除根节点:将根节点删除,并将堆中最后一个元素移到根节点,然后向下调整("下沉"),直到满足堆的性质。
- 获取最大值或最小值:对于最大堆,根节点是最大值;对于最小堆,根节点是最小值,获取的时间复杂度为 O(1)。
堆的实现
堆通常使用数组来实现,而不直接用指针和节点:
- 对于给定位置
i
的节点,其左子节点 在位置2*i+1
,右子节点 在位置2*i+2
。 - 其父节点 在位置
(i-1)/2
。
堆的应用
- 优先队列:使用最大堆或最小堆快速获取优先级最高的元素。
- 堆排序:利用堆的性质进行排序,时间复杂度为 O(n log n)。
- 图的最短路径算法:如 Dijkstra 算法中使用最小堆优化查找最短路径的过程。
- 哈夫曼编码:利用最小堆构建哈夫曼树。
堆的高效性来自于其结构和调整操作,通过上浮或下沉调整可以在对数时间内保持堆的性质。
注意事项
- 堆的存储:C++ 标准库中的堆通常存储在一个连续的存储容器中,如
std::vector
。 - 堆的性质:堆只是部分有序的(最大堆:每个父节点大于或等于子节点),不保证整个容器是有序的。
- 操作顺序:在使用堆算法时,确保在所有插入和删除操作完成后调用相应的堆算法(如
std::make_heap
)。
C++ 标准库中的堆相关算法
++ 标准库中没有直接封装堆(Heap)数据结构,但是提供了用于操作堆的相关算法,可以很方便地在序列容器(如 std::vector)上实现堆的功能
C++ 标准库提供了一组基于 随机访问迭代器 的堆操作算法,这些算法位于 <algorithm>
头文件中。主要包括:
std::make_heap
:将一个范围内的元素重新排列成堆。std::push_heap
:将新元素插入堆中,保持堆的性质。std::pop_heap
:将堆顶元素移到范围的末尾,并重新调整剩余元素以维持堆性质。std::sort_heap
:对堆进行排序,结果为升序(最大堆)或降序(最小堆)。- std::is_heap_until:找到一个位置,使得从开始到该位置的子序列满足堆的性质。
- std::is_heap:检查一个序列容器是否满足堆的性质
- std::is_heap:检查一个序列容器是否满足堆的性质。
堆算法的复杂度
std::make_heap
:线性时间复杂度 O(n)。std::push_heap
和std::pop_heap
:对数时间复杂度 O(log n)。std::sort_heap
:线性时间复杂度 O(n log n)。
堆算法的使用示例
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> heap = {20, 15, 30, 10, 5, 40};
// 1、创建最大堆
std::make_heap(heap.begin(), heap.end());
std::cout << "最大堆顶元素: " << heap.front() << std::endl; // 输出 40
// 2、插入新元素 50
heap.push_back(50);
std::push_heap(heap.begin(), heap.end());
std::cout << "插入 50 后堆顶元素: " << heap.front() << std::endl; // 输出 50
// 3、移除堆顶元素
std::pop_heap(heap.begin(), heap.end());
int removed = heap.back();
heap.pop_back();
std::cout << "移除堆顶元素: " << removed << std::endl; // 输出 50
std::cout << "当前堆顶元素: " << heap.front() << std::endl; // 输出 40
// 4、对堆进行排序
std::sort_heap(heap.begin(), heap.end());
std::cout << "排序后的元素: ";
for (int n : heap) {
std::cout << n << " ";
}
std::cout << std::endl; // 输出 5 10 15 20 30 40
return 0;
}
输出结果:最大堆顶元素: 40
插入 50 后堆顶元素: 50
移除堆顶元素: 50
当前堆顶元素: 40
排序后的元素: 5 10 15 20 30 40
std::priority_queue
(优先队列)
C++ 标准库还提供了 std::priority_queue
容器适配器,它封装了堆的操作,提供了更高层次的接口。默认情况下,std::priority_queue
实现为最大堆。
使用示例
#include <iostream>
#include <queue>
#include <vector>
int main() {
// 定义一个最大堆的优先队列
std::priority_queue<int> pq;
// 插入元素
pq.push(20);
pq.push(15);
pq.push(30);
pq.push(10);
pq.push(5);
pq.push(40);
// 输出并移除堆顶元素
while (!pq.empty()) {
std::cout << pq.top() << " "; // 输出堆顶元素
pq.pop(); // 移除堆顶元素
}
std::cout << std::endl; // 输出: 40 30 20 15 10 5
return 0;
}
输出结果:40 30 20 15 10 5
自定义优先级
你可以通过提供自定义的比较函数来创建 最小堆 或其他优先级队列。例如,创建一个最小堆:
#include <iostream>
#include <queue>
#include <vector>
#include <functional> // std::greater
int main() {
// 定义一个最小堆的优先队列
std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;
// 插入元素
min_pq.push(20);
min_pq.push(15);
min_pq.push(30);
min_pq.push(10);
min_pq.push(5);
min_pq.push(40);
// 输出并移除堆顶元素
while (!min_pq.empty()) {
std::cout << min_pq.top() << " "; // 输出堆顶元素
min_pq.pop(); // 移除堆顶元素
}
std::cout << std::endl; // 输出: 5 10 15 20 30 40
return 0;
}
输出结果:5 10 15 20 30 40
2. 内存堆(Memory Heap)
什么是内存堆?
内存堆是用于动态分配内存的区域,程序在运行时可以通过 new
和 delete
操作符(或 malloc
和 free
函数)在堆上分配和释放内存。与栈(Stack)不同,堆内存的分配和释放由程序员控制,具有更大的灵活性和更高的管理复杂性。
动态内存分配示例
#include <iostream>
int main() {
// 在堆上分配一个整数
int* ptr = new int(42);
std::cout << "动态分配的整数值: " << *ptr << std::endl;
// 释放堆内存
delete ptr;
// 在堆上分配一个整数数组
int* arr = new int[5]{1, 2, 3, 4, 5};
std::cout << "动态分配的数组: ";
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
// 释放堆内存
delete[] arr;
return 0;
}
输出结果:动态分配的整数值: 42
动态分配的数组: 1 2 3 4 5
注意事项
- 内存泄漏 :如果动态分配的内存未被释放,会导致内存泄漏。确保每一个
new
对应一个delete
,每一个new[]
对应一个delete[]
。 - 智能指针 :为了避免内存泄漏,推荐使用智能指针(如
std::unique_ptr
、std::shared_ptr
)来管理动态分配的内存。
总结
- 堆数据结构 :在 C++ 中,堆是一种用于实现优先队列等的数据结构,标准库通过一系列算法(
make_heap
、push_heap
、pop_heap
、sort_heap
)和std::priority_queue
提供了强大的支持。 - 内存堆:用于动态内存分配,推荐使用智能指针来管理堆内存,避免内存泄漏和其他内存管理问题。
理解和正确使用堆数据结构及内存堆是高效和安全编程的重要组成部分,尤其在涉及复杂数据管理和动态内存操作的应用中尤为关键。