

目录
正文
1. 堆的基本概念
1.1 定义
堆是一种特殊的完全二叉树数据结构,它分为两种类型:最大堆和最小堆。在最大堆中,每个节点的值都大于或等于它的子节点的值;而在最小堆中,每个节点的值都小于或等于它的子节点的值。例如,对于一个最大堆,根节点存储的是整棵树中的最大值,对于最小堆来说,根节点存储的则是整棵树中的最小值。堆这种数据结构常用于实现优先队列,能够高效地进行插入和删除最值的操作。
1.2 相关术语
1.2.1 节点
和普通树结构一样,堆中的元素就是节点,节点中可以存储具体的数据信息。比如在一个存储学生成绩的堆中,每个节点可以存放一个学生的成绩以及对应的学号等相关信息。
1.2.2 父节点与子节点
在堆(作为完全二叉树)里,除了根节点外,每个节点都有父节点,并且每个非叶节点都有子节点。对于节点在数组中的下标为 i(数组下标从 0 开始),其左子节点的下标为 2 * i + 1,右子节点的下标为 2 * i + 2;反过来,父节点的下标为 (i - 1) / 2(这里的除法为向下取整)。例如,若节点在下标为 3 的位置,它的左子节点下标就是 2 * 3 + 1 = 7,右子节点下标就是 2 * 3 + 2 = 8,它的父节点下标就是 (3 - 1) / 2 = 1。
1.2.3 叶节点
叶节点就是没有子节点的节点,在堆对应的完全二叉树结构中,最后一层的节点都是叶节点。例如,一个深度为 4 的堆,第 4 层的所有节点就是叶节点,它们不会再有子节点往下延伸了。
2. 堆的存储结构
堆通常采用顺序存储结构,也就是用数组来存储堆中的节点。这是因为堆是完全二叉树,其节点在数组中的存储可以方便地通过下标来计算父子节点的位置关系,从而实现高效的操作。例如,有如下一个最大堆用数组表示:[9, 7, 8, 5, 6, 4, 3],数组下标为 0 的位置存储的就是根节点的值 9,按照父子节点的下标计算公式,就能快速定位其他节点以及它们之间的关联。
以下是定义一个简单的堆结构体示例(以最大堆为例,暂不考虑实际完整功能实现细节):
#include <iostream>
#include <vector>
using namespace std;
class MaxHeap {
public:
MaxHeap(); // 构造函数
void insert(int value); // 插入元素函数
int extractMax(); // 取出最大值(删除堆顶元素并返回其值)函数
private:
vector<int> heap; // 使用vector来存储堆元素,方便动态扩容
};
3. 堆的基本操作
3.1 插入操作
3.1.1 操作原理
当向堆中插入一个新元素时,首先将该元素添加到数组(也就是堆)的末尾,然后通过不断比较该元素与其父节点的值,按照堆的性质(最大堆就是新元素大于父节点时交换位置)进行上浮操作(也叫上滤操作),直到满足堆的性质为止。例如,在一个最大堆中插入元素 10,插入到末尾后,可能需要和它的父节点比较,如果父节点值小于 10,就交换它们的位置,持续这个过程,直到新元素到达合适的位置,使得整个堆依然保持最大堆的性质。
3.1.2 C++代码实现
// 插入元素函数
void MaxHeap::insert(int value) {
heap.push_back(value); // 将新元素添加到堆数组末尾
int index = heap.size() - 1; // 获取新元素所在的下标位置
// 上浮操作,让新元素找到合适位置以满足堆的性质
while (index > 0) {
int parentIndex = (index - 1) / 2; // 计算父节点下标
if (heap[parentIndex] < heap[index]) {
// 如果父节点值小于新元素值,交换它们的位置
swap(heap[parentIndex], heap[index]);
index = parentIndex; // 更新新元素的下标,继续向上比较
} else {
break; // 满足堆性质了,退出循环
}
}
}
3.2 删除操作(以最大堆删除堆顶元素为例)
3.2.1 操作原理
删除堆顶元素(在最大堆中就是最大值)时,先将堆顶元素和堆的最后一个元素进行交换,然后删除最后一个元素(也就是原来的堆顶元素),接着对新的堆顶元素进行下沉操作(也叫下滤操作),让它通过不断与子节点比较(在最大堆中与较大的子节点比较,如果小于较大子节点就交换位置),直到满足堆的性质为止。例如,在最大堆 [9, 7, 8, 5, 6, 4, 3] 中删除堆顶元素 9,先将 9 和最后一个元素 3 交换,变为 [3, 7, 8, 5, 6, 4, 9],然后删除原来的 9(现在数组末尾的 9),接着对新堆顶元素 3 进行下沉操作,使其找到合适位置保持堆的性质。
3.2.2 C++代码实现
// 取出最大值(删除堆顶元素并返回其值)函数
int MaxHeap::extractMax() {
if (heap.empty()) {
cerr << "堆为空,无法提取最大值" << endl;
return -1; // 返回一个表示错误的值,实际中可根据需求调整
}
int maxValue = heap[0]; // 记录堆顶元素(最大值)
int lastIndex = heap.size() - 1;
heap[0] = heap[lastIndex]; // 将最后一个元素移到堆顶
heap.pop_back(); // 删除原来的最后一个元素
int index = 0;
// 下沉操作,让新堆顶元素找到合适位置以满足堆的性质
while (true) {
int leftChildIndex = 2 * index + 1;
int rightChildIndex = 2 * index + 2;
int largerChildIndex = index;
// 先比较左子节点是否存在且比当前节点大
if (leftChildIndex < heap.size() && heap[leftChildIndex] > heap[largerChildIndex]) {
largerChildIndex = leftChildIndex;
}
// 再比较右子节点是否存在且比当前较大子节点还大
if (rightChildIndex < heap.size() && heap[rightChildIndex] > heap[largerChildIndex]) {
largerChildIndex = rightChildIndex;
}
if (largerChildIndex!= index) {
// 如果有更大的子节点,交换位置
swap(heap[index], heap[largerChildIndex]);
index = largerChildIndex;
} else {
break; // 满足堆性质了,退出循环
}
}
return maxValue;
}
3.3 构建堆操作
3.3.1 操作原理
构建堆有两种常见的方法,一种是通过不断插入元素来逐个构建,另一种是利用已有的数组数据直接调整构建堆,后者更为常用,其思路是从最后一个非叶节点开始,依次对每个非叶节点进行下沉操作,使得整个数组呈现出堆的结构。例如,有数组 [4, 10, 3, 5, 1, 8],从最后一个非叶节点(这里是下标为 2 的节点 3)开始进行下沉操作,然后依次往前对其他非叶节点操作,最终将整个数组调整为一个堆结构。
3.3.2 C++代码实现
// 构造函数,用于初始化堆,可以传入一个数组来构建堆
MaxHeap::MaxHeap(vector<int> arr) {
heap = arr;
int lastNonLeafIndex = (heap.size() - 2) / 2; // 计算最后一个非叶节点下标
// 从最后一个非叶节点开始,依次对非叶节点进行下沉操作
for (int i = lastNonLeafIndex; i >= 0; i--) {
int index = i;
while (true) {
int leftChildIndex = 2 * index + 1;
int rightChildIndex = 2 * index + 2;
int largerChildIndex = index;
// 先比较左子节点是否存在且比当前节点大
if (leftChildIndex < heap.size() && heap[leftChildIndex] > heap[largerChildIndex]) {
largerChildIndex = leftChildIndex;
}
// 再比较右子节点是否存在且比当前较大子节点还大
if (rightChildIndex < heap.size() && heap[rightChildIndex] > heap[largerChildIndex]) {
largerChildIndex = rightChildIndex;
}
if (largerChildIndex!= index) {
// 如果有更大的子节点,交换位置
swap(heap[index], heap[largerChildIndex]);
index = largerChildIndex;
} else {
break; // 满足堆性质了,退出循环
}
}
}
}
4. 堆的应用
4.1 优先队列
堆常被用于实现优先队列。优先队列是一种数据结构,其中的元素具有优先级,每次出队操作取出的是优先级最高的元素(在最大堆实现的优先队列中就是值最大的元素,最小堆实现的就是值最小的元素)。例如,在操作系统中,任务调度可以使用优先队列,每个任务有不同的优先级,优先级高的任务先被执行,通过堆来实现优先队列就可以高效地获取和处理这些有优先级区分的任务。
4.2 排序算法(堆排序)
堆排序是一种基于堆结构的排序算法。它的基本步骤是先将待排序的数组构建成一个堆(一般构建成最大堆),然后依次取出堆顶元素(最大值)并放在数组末尾,再对剩余的堆元素进行调整保持堆的性质,重复这个过程,直到整个数组有序。例如,有数组 [5, 3, 8, 4, 9],先构建成最大堆 [9, 8, 5, 4, 3],然后取出堆顶 9 放在数组末尾变为 [8, 4, 5, 3, 9],接着调整剩余元素为堆,继续取堆顶元素放置,最终实现数组的排序。堆排序的时间复杂度为
O
(
n
log
n
)
O(n \log n)
O(nlogn),空间复杂度为
O
(
1
)
O(1)
O(1),是一种比较高效的排序算法。
4.3 解决 Top-K 问题
Top-K 问题是指在一组数据中找出最大(或最小)的 K 个元素。可以利用堆来解决这个问题,比如要找最大的 K 个元素,就构建一个大小为 K 的最小堆,然后遍历数据,当遍历到的元素大于堆顶元素时,将堆顶元素替换掉并调整堆,最后堆中存储的就是最大的 K 个元素。例如,有一组海量数据,要找出其中最大的 100 个元素,通过构建一个 100 个元素的最小堆,依次处理数据,就能高效地获取这最大的 100 个元素,避免了对所有数据进行完全排序的高开销操作。
结语
感谢您的阅读!期待您的一键三连!欢迎指正!

4424

被折叠的 条评论
为什么被折叠?



