#好了,作者刚刚学习有所心得,分享一下#
堆的基本概念
堆(Heap)是一种特殊的树形数据结构,它满足以下两个基本特性:
-
完全二叉树:堆必须是一种完全二叉树,即允许最后一行不满,其他行都要满, 最后一行必须从左往右排序,不可留有间隔
-
堆性质:堆分为两种类型,大根堆和小跟堆。
-
大根堆:堆中任意节点的值总是大于或等于其子节点的值。大根堆的根节点是整个堆中最大的元素。
-
小根堆:堆中任意节点的值总是小于或等于其子节点的值。小跟堆的根节点是整个堆中最小的元素。
-
大根堆
小根堆
堆的存储
以大根堆为例
首先,我们按照层次遍历的顺序给大根堆的结点进行编号。
从上到下,从左到右,把这些编号对应到这些数组的下标,然后把树的元素存入相应的下标里
因为堆是完全二叉树,所以每个下标和树的每个位置是一一对应的,这样,一个堆就可以用一个一维数组来描述。
从图中可以看出结点下标的规律,若父节点为i,
则左子节点为2*i + 1
右子节点为 2*i + 2
堆的基本操作
-
插入(Heapify Up):向堆中添加一个新元素,并保持堆的性质。这通常通过将新元素添加到数组的末尾,然后通过与父节点比较并可能交换位置来实现,直到它不再违反堆的性质。
-
删除最大(或最小)元素(Heapify Down):移除堆顶元素(最大或最小),并保持堆的性质。这通常通过将最后一个元素移到堆顶,然后通过与子节点比较并可能交换位置来实现,直到它不再违反堆的性质。
-
构建堆(Build Heap):从给定数组构建一个堆。这通常通过从最后一个非叶子节点开始,逐个应用Heapify Down操作来实现。
-
堆排序(Heap Sort):利用堆的性质进行排序。首先构建最大堆,然后不断移除堆顶元素并重新构建堆,直到堆为空。
交换位置的操作
上滤
插入新元素一般都放在数组末尾,之后交换位置的过程有“上滤”。
上滤操作通常用于在堆中插入新元素后维护堆的性质。具体步骤如下:
-
将新元素添加到堆的末尾。
-
比较新元素与其父节点的值。
-
如果新元素违反了堆的性质(在最大堆中小于父节点,在最小堆中大于父节点),则与父节点交换位置。
-
重复步骤2和3,直到新元素不再与其父节点交换位置,或者成为根节点。
这是在原来的基础上插入了一个9,但是,它明显破坏了大根堆的排序的性质,这时候,为了维护堆的性质,我们将进行上滤操作,即让该元素和它的父节点进行比较,如果大于父节点,则交换,即:
若排序后还是破坏了它的堆序性,再次进行排序,即:
这样,就符合堆序性,也就是大根堆的排序方法,这样从下往上移动的方式就叫他上滤。
下滤
再提及一个排序的方法,“下滤”。
下滤操作通常用于删除堆顶元素后维护堆的性质。具体步骤如下:
-
将堆顶元素(最大或最小元素)与堆的最后一个元素交换位置,然后移除最后一个元素。
-
比较新的堆顶元素与其子节点的值。
-
如果新堆顶元素违反了堆的性质(在最大堆中小于子节点,在最小堆中大于子节点),则与较大的子节点(最大堆)或较小的子节点(最小堆)交换位置。
-
重复步骤2和3,直到新堆顶元素不再与其子节点交换位置,或者成为叶子节点。
我们现在都以大根堆进行演示。
图中我们可以看到,整体来说,该堆是不满足大根堆的堆排序的,但是,根节点的两个子节点却又满足它的堆排序,这样,我们可以进行下滤操作。大根堆的下滤操作是和它的最大子节点进行交换,即:
第一次交换,仍然不满足堆序性,继续交换
这样就满足了堆序性,我们把根节点向下调整的操作叫做下滤。
建堆
如果有乱序的数组,我们该如何将它们建成堆的形式呢?一般有两种方法,即“自顶向下”和“自下而上”。从前面我们讲了上滤和下滤,这两个操作分别对应着这两种方法。
自顶向下建堆法
我们给出一组数组,例如[2,5,7,4,3,9,1]这个乱序组,我们从第一个编号进行插入,然后再插入第二个元素,对这两个元素进行上滤操作,再插入新元素,再上滤操作,以此类推,即:
这就是最后的堆排序
自下而上建堆法
我们仍然以[2,5,7,4,3,9,1]这个数组为例,刚开始,它的堆的形式是:
它的思路是先将下面的元素调整成堆,即
然后再对根节点进行下滤操作,即:
建堆完成。总的来说,自下而上的建堆是比自顶向下快的,因为自下而上的建堆复杂度更低,为O(N),而前者则为O(NlogN)
总结
好了,今天的分享就到此结束吧,这两种方法的代码还未写完,有兴趣的小伙伴可以找一找。
代码补充
自顶向下代码
#include <iostream>
#include <vector>
// 上滤操作函数
void heapifyUp(std::vector<int>& arr, int i) {
while (i > 0) {
int parent = (i - 1) / 2; // 父节点的索引
if (arr[i] > arr[parent]) {
std::swap(arr[i], arr[parent]); // 如果当前节点大于父节点,交换它们
i = parent; // 更新当前节点为父节点
} else {
break; // 如果当前节点不大于父节点,停止上滤
}
}
}
// 使用上滤方法构建大根堆
void buildHeap(std::vector<int>& arr) {
for (int i = 0; i < arr.size(); ++i) {
heapifyUp(arr, i); // 对每个新插入的元素执行上滤操作
}
}
int main() {
std::vector<int> arr;
int n, element;
// 获取用户输入的数组大小
std::cout << "请输入数组的大小: ";
std::cin >> n;
// 获取用户输入的数组元素
std::cout << "请输入数组的元素(用空格分隔): ";
for (int i = 0; i < n; ++i) {
std::cin >> element;
arr.push_back(element);
}
// 使用上滤方法构建大根堆
buildHeap(arr);
std::cout << "构建的大根堆是:\n";
for (int num : arr) {
std::cout << num << " ";
}
std::cout << "\n";
return 0;
}
自下而上代码
#include <iostream>
#include <vector>
// 下滤操作,用于维护大根堆的性质
void heapify(std::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) {
std::swap(arr[i], arr[largest]); // 交换
// 递归地定义子树作为堆
heapify(arr, n, largest);
}
}
// 自下而上构建大根堆
void buildHeap(std::vector<int>& arr) {
int n = arr.size();
// 从最后一个非叶子节点开始,逐个应用下滤操作
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
}
int main() {
int n;
std::cout << "请输入数组的大小: ";
std::cin >> n;
std::vector<int> arr(n);
std::cout << "请输入数组的元素(用空格分隔): ";
for (int i = 0; i < n; ++i) {
std::cin >> arr[i];
}
buildHeap(arr); // 构建大根堆
std::cout << "构建的大根堆是:\n";
for (int num : arr) {
std::cout << num << " ";
}
std::cout << "\n";
return 0;
}
#补充完毕,嘿嘿#