【数据结构】非线性数据结构——堆
1. 堆的介绍
堆(Heap) 是一种 完全二叉树,满足某种特定的顺序性质:
-
大根堆(Max Heap):任意节点的值 ≥ 子节点。
-
小根堆(Min Heap):任意节点的值 ≤ 子节点。
堆的根节点永远是堆中的最大值(大根堆)或最小值(小根堆),堆通常用 数组 来存储,本质是 “用数组存储的二叉树”,完全二叉树结构保证了堆可以用数组紧凑存储(无冗余空间),同时通过数组索引的数学关系可以模拟二叉树的父子节点关联,避免了链式存储的指针开销。
2. 堆的存储方式
假设数组下标从 0 开始(最常用),对于任意节点 i
:
父节点索引:parent(i) = (i - 1) / 2(整数除法,向下取整)
左子节点索引:left(i) = 2 * i + 1
右子节点索引:right(i) = 2 * i + 2
3. 堆的基本操作
3.1 插入(Insert)
把新元素插到数组末尾(保持完全二叉树形态)。
执行 上浮(sift up / heapify-up):不断和父节点比较,若不满足堆性质就交换。
时间复杂度:O(log n)。
3.2 删除堆顶(Extract)
取出根节点(最大或最小值)。
把最后一个元素移到根节点。
执行 下沉(sift down / heapify-down):不断和子节点比较并交换,直到满足堆性质。
时间复杂度:O(log n)。
3.3 建堆(Heapify)
自底向上调整:从最后一个非叶子节点开始,逐步下沉。
时间复杂度:O(n)。
4、堆的应用场景
堆的核心优势是 “快速获取极值”,主要应用包括:
-
堆排序:利用堆的特性实现排序,时间复杂度 O (n log n),空间复杂度 O (1)(原地排序)。
-
优先队列(Priority Queue):堆是优先队列的标准实现方式。优先队列的核心需求是 “每次取出优先级最高的元素”,堆的 “堆顶是极值” 特性完美匹配这一需求。
-
Top K 问题:从海量数据中快速找到前 K 个最大 / 最小元素(如 “前 10 名高分学生”)。
-
中位数查找:用两个堆维护数据(大顶堆存左半部分数据,小顶堆存右半部分数据),堆顶分别为左半最大值和右半最小值,可 O (1) 获取中位数。
5、堆排序
如果把一个无序数组通过桶排序算法变成有序数组?
堆排序分为 构建堆 和 排序(提取最大值并调整堆) 两大阶段,具体步骤如下:
1. 构建大顶堆
将无序数组转换为大顶堆,确保整个二叉树满足大顶堆的性质。
起点: 从最后一个非叶子节点开始(索引为 n/2 - 1,n 为数组长度),因为叶子节点本身就是合法的堆。
解释:最后一个节点的索引是 n-1 ,最后一个非叶子节点的索引是 (n-1-1)/2(n 是数组长度,即最后一个节点的父节点)。选择从这里开始,是因为叶子节点本身已满足 “大顶堆属性”,无需堆化;从后向前遍历非叶子节点,可确保每个节点的子树先满足堆属性,进而让整个数组成为大顶堆。
过程: 从后往前依次对每个非叶子节点执行 堆调整(heapify) 操作,确保以该节点为根的子树是大顶堆。
堆调整(heapify):对每个非叶子节点,执行 “向下堆化”:
- 比较当前节点与其左、右子节点,找到三者中的最大值;
- 若最大值不是当前节点,则交换当前节点与最大值节点(确保父节点≥子节点);
- 交换后,需递归(或迭代)检查被交换的子节点,直到该节点及其子树满足大顶堆属性。
2. 排序(提取最大值并调整堆)
1) 提取堆顶最大值,放到已排序部分
- 将 “堆顶(数组第一个元素,未排序部分的最大值)” 与 “当前堆的最后一个元素(未排序部分的最后一个元素)” 交换
- 此时最大值被“移动到数组的末尾”,成为 “已排序部分的第一个元素”(随着循环,已排序部分会从后向前逐步扩大);
- 原堆的最后一个元素被移到堆顶,破坏了堆的属性(新堆顶可能小于子节点)。
2)缩小堆的范围,恢复堆结构
- 由于最大值已放入已排序部分,后续无需再处理它,因此 “堆的大小” 缩小 1(即忽略数组末尾的已排序元素);
- 对新堆顶(原末尾元素)执行 堆调整(heapify),修复堆的属性,确保新堆的堆顶仍是 “当前未排序部分的最大值”。
#include <iostream>
#include <vector>
using namespace std;// 向下堆化(维护大顶堆)
// arr: 待堆化的数组
// n: 堆的大小
// i: 开始堆化的节点索引
void heapify(vector<int>& arr, int n, int i) {int largest = i; // 初始化最大值为当前节点int left = 2 * i + 1; // 左子节点索引int right = 2 * i + 2; // 右子节点索引// 如果左子节点大于当前最大值,则更新最大值if (left < n && arr[left] > arr[largest]) {largest = left;}// 如果右子节点大于当前最大值,则更新最大值if (right < n && arr[right] > arr[largest]) {largest = right;}// 如果最大值不是当前节点,则交换并递归堆化if (largest != i) {swap(arr[i], arr[largest]);heapify(arr, n, largest);}
}// 堆排序函数
void heapSort(vector<int>& arr) {int n = arr.size();// 1. 构建大顶堆// 从最后一个非叶子节点开始向上堆化for (int i = n / 2 - 1; i >= 0; i--) {heapify(arr, n, i);}// 2. 提取最大值并维护堆// 每次将堆顶(最大值)与当前堆的最后一个元素交换// 然后减小堆的大小并重新堆化for (int i = n - 1; i > 0; i--) {swap(arr[0], arr[i]); // 交换堆顶和当前堆的最后一个元素heapify(arr, i, 0); // 对剩余的元素重新堆化}
}// 打印数组
void printArray(const vector<int>& arr) {for (int num : arr) {cout << num << " ";}cout << endl;
}int main() {vector<int> arr = {12, 11, 13, 5, 6, 7};cout << "排序前的数组: ";printArray(arr);heapSort(arr);cout << "排序后的数组: ";printArray(arr);return 0;
}