1.概念
什么是堆?堆和树的区别是什么?它的应用场景有哪些?
堆(Heap)是一种基于树形结构的数据结构,它是一种特殊的完全二叉树。堆的特点是每个节点都满足堆的性质,即父节点的键值总是大于或等于(小于或等于)任何一个子节点的键值。这种性质被称为“堆序性”。堆分为最大堆和最小堆两种类型,其中最大堆的根节点键值是所有节点中最大的,最小堆的根节点键值是所有节点中最小的。
- 最大堆(Max Heap):父节点的键值大于等于其子节点的键值。
- 最小堆(Min Heap):父节点的键值小于等于其子节点的键值。
堆和树的区别在于:
- 高效的插入和删除操作:由于堆是完全二叉树,插入和删除操作的时间复杂度为 ( O(log n) ),其中 ( n ) 是堆中元素的数量。这使得堆在优先级队列等场景中非常有用。
- 高效的获取最值操作:在最大堆中,最大值位于根节点;在最小堆中,最小值位于根节点。因此,获取最值的操作时间复杂度为 ( O(1) )。
2.堆的实际应用场景
堆在计算机科学中有着广泛的应用场景,其中最常见的应用之一是优先队列。除了优先队列之外,堆还被用于解决各种算法和数据结构中的问题,例如堆排序、最小(大)k个元素、图算法中的最短路径算法(如Dijkstra算法)等。
以下是堆的一些实际应用场景:
1. **优先队列**:堆可用于实现优先队列,其中元素按照优先级进行排序。优先队列常用于任务调度、事件处理等场景,例如操作系统中的进程调度、计算机网络中的路由器转发、模拟系统中的事件处理等。
2. **堆排序**:堆排序是一种高效的排序算法,其时间复杂度为 O(n log n)。堆排序利用堆的性质将数组转换为一个最小堆或最大堆,然后通过不断从堆顶取出元素并调整堆的结构来实现排序。
3. **最小(大)k个元素**:堆可以帮助我们在一个包含大量元素的数据集中快速找到最小或最大的 k 个元素。通过维护一个大小为 k 的堆,我们可以在 O(n log k) 的时间复杂度内找到所需的结果。
4. **Dijkstra算法**:Dijkstra算法是一种用于解决单源最短路径问题的算法,常用于网络路由、地图路线规划等场景。在Dijkstra算法中,堆可以用来高效地选择下一个要处理的节点,从而加速路径计算的过程。
5. **堆在操作系统中的应用**:堆内存管理是操作系统中的一个重要部分,用于动态分配内存给进程。操作系统通常会使用堆来管理进程的动态内存分配,例如通过malloc和free函数在堆中分配和释放内存。
这些只是堆在计算机科学中的一些应用场景,实际上堆还有更多用途,可以根据具体的问题需求来灵活应用。
3.堆的实现与操作
我们了解了什么是堆,那我们如何用代码实现呢
首先我们可以完成堆的push操作,
我们能够看得出来这是一个大堆,这颗像树一样的图是它的逻辑结构,而底下的数组是它的物理结构,也就是说,我们真正操作的是数组。当然,也是可以用指针串联起来的,不过那样操作会很复杂,也没必要。现在,我们想在2的后面插入一个22,在数组中我们可以直接将它放入,但是,这样操作之后,它将不是一个堆。因为它并不满足堆的要求,所以,我们还需要进一步的将数据调整。
具体操作如下:
放入代码又是怎样操作的呢,
/* 向上调整 */
void HeapIfyUp(HPDataType* a, int Child)
{
//因为我们的下标是整数,再求父亲节点的时候,不论是左孩子还是右孩子,都可以通过(Child - 1) / 2求出其父亲节点
int parent = (Child - 1) / 2;
while (Child > 0)
{
/* 大堆,它的父亲节点要大于孩子节点,否则就交换*/
if (a[Child] > a[parent])
{
Swap(&a[parent], &a[Child]);
Child = parent;
parent = (Child - 1) / 2;
}
else
{
break;
}
}
}
void HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
/* 申请空间 */
if (hp->_capacity == hp->_size)
{
size_t newCapacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * newCapacity);
if (NULL == tmp)
{
perror("HeapPush realloc fail");
return;
}
hp->_a = tmp;
hp->_capacity = newCapacity;
}
hp->_a[hp->_size] = x;
hp->_size++;
/* 将数据向上调整 */
HeapIfyUp(hp->_a, hp->_size - 1);
}
- 删除堆顶元素
- 堆一般都是进行删除堆顶元素,因为堆顶不是最大值就是最小值。一般不会删除非堆顶元素,这是堆这种数据结构的特性决定的。
- 我们先将堆中的最后一个元素赋值给堆顶元素,然后再删除最后一个元素,最后再进行自上而下的堆化操作。
代码实现如下:void HeapIfyDown(HPDataType* a, int n, int parent) { //算出最大的左右中一个孩子节点(假设最大的为左孩子) int child = (parent * 2) + 1; while (child < n) { //根据特性左孩子不存在,则右孩子一定不存在。 //如果假设错误,则更新最小孩子节点 if (child + 1 < n && a[child] < a[child + 1]) { child++; } //孩子节点大于父亲节点,交换并重新计算最大孩子节点 if (a[child] > a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = (parent * 2) + 1; } else { break; } } } void HeapPop(Heap* hp) { assert(hp); if (!HeapEmpty(hp)) { //交换堆顶和堆尾数据 Swap(&hp->_a[0], &hp->_a[hp->_size - 1]); hp->_size--; HeapIfyDown(hp->_a, hp->_size - 1, 0); } }
4.堆排序
- 给定一个数组,其中的元素都是无序杂乱的,我们怎么对它进行堆排序呢?
我们可以通过建堆来进行排序 - 代码实现如下
/* 向下调整 */ void HeapifyDown(HPDataType* a, int sz, int parent) { int largest = parent; int left = 2 * parent + 1; int right = 2 * parent + 2; // 比较左孩子和父节点 if (left < sz && a[left] < a[largest]) largest = left; // 比较右孩子和当前最大值节点 if (right < sz && a[right] < a[largest]) largest = right; // 如果最大值不是当前节点,交换并递归调整子树 if (largest != parent) { Swap(&a[parent], &a[largest]); HeapifyDown(a, sz, largest); } } // 堆排序函数 void HeapSort(HPDataType* a, int sz) { // 构建最大堆 for (int i = sz / 2 - 1; i >= 0; i--) { HeapifyDown(a, sz, i); } // 交换堆顶元素与堆尾元素,并调整堆 for (int i = sz - 1; i > 0; i--) { // 交换堆顶元素与堆尾元素 Swap(&a[0], &a[i]); // 调整堆 HeapifyDown(a, i, 0); } }
-
5,topK问题
-
//产生随机数 void CreationNum() { srand((size_t)time(NULL)); int sum = 10000; const char* file = "creation.txt"; FILE* fin = fopen(file, "w"); for (int i = 0; i < sum; i++) { int n = rand() % 10000; fprintf(fin, "%d\n", n); } } //找到数中最大的几个 void topK() { const char* file = "creation.txt"; FILE* fout = fopen(file, "r"); int max = 5; int* mindtopK = (int*)malloc(sizeof(int) * max); for (int i = 0; i < max; i++) { fscanf(fout, "%d", mindtopK + i); } //建小堆 for (int i = (max - 2) / 2; i > 0; i--) { HeapifyDown(mindtopK, max, i); } //读取文件中的数,遇到大于堆顶的数据就进行交换,最后调整堆的数据 int x = 0; while (!feof(fout)) { fscanf(fout, "%d", &x); if (mindtopK[0] < x) { mindtopK[0] = x; HeapifyDown(mindtopK, max, 0); } } for (int i = 0; i < max; i++) { printf("%d ", mindtopK[i]); } } int main() { //CreationNum(); topK(); }
-
6.总代码
- tree_heap.c
#include "tree_heap.h" void HeapCreate(Heap* hp, HPDataType* a, int n) { a = (HPDataType*)malloc(sizeof(HPDataType) * n); if (NULL == a) { perror("HeapPush realloc fail"); return; } hp->_a = a; hp->_capacity = n; hp->_size = 0; } void Swap(HPDataType* px, HPDataType* py) { HPDataType tmp = *px; *px = *py; *py = tmp; } /* 向上调整 */ void HeapIfyUp(HPDataType* a, int Child) { //因为我们的下标是整数,再求父亲节点的时候,不论是左孩子还是右孩子,都可以通过(Child - 1) / 2求出其父亲节点 int parent = (Child - 1) / 2; while (Child > 0) { /* 大堆,它的父亲节点要大于孩子节点,否则就交换*/ if (a[Child] > a[parent]) { Swap(&a[parent], &a[Child]); Child = parent; parent = (Child - 1) / 2; } else { break; } } } void HeapPush(Heap* hp, HPDataType x) { assert(hp); /* 申请空间 */ if (hp->_capacity == hp->_size) { int newCapacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2; HPDataType* tmp = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * newCapacity); if (NULL == tmp) { perror("HeapPush realloc fail"); return; } hp->_a = tmp; hp->_capacity = newCapacity; } hp->_a[hp->_size] = x; hp->_size++; /* 将数据向上调整 */ HeapIfyUp(hp->_a, hp->_size - 1); } void HeapDestory(Heap* hp) { if (hp->_a) { free(hp->_a); hp->_a = NULL; } } int HeapEmpty(Heap* hp) { assert(hp); return hp->_size == 0; } int HeapSize(Heap* hp) { assert(hp); return hp->_size; } HPDataType HeapTop(Heap* hp) { assert(hp); if (!HeapEmpty(hp)) { return hp->_a[0]; } } void HeapifyDown(HPDataType* a, int n, int parent) { //算出最大的左右中一个孩子节点(假设最大的为左孩子) int child = (parent * 2) + 1; while (child < n) { //根据特性左孩子不存在,则右孩子一定不存在。 //如果假设错误,则更新最小孩子节点 if (child + 1 < n && a[child] < a[child + 1]) { child++; } //孩子节点大于父亲节点,交换并重新计算最大孩子节点 if (a[child] > a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = (parent * 2) + 1; } else { break; } } } /* 向下调整 */ void HeapifyDown1(HPDataType* a, int sz, int parent) { int largest = parent; int left = 2 * parent + 1; int right = 2 * parent + 2; // 比较左孩子和父节点 if (left < sz && a[left] < a[largest]) largest = left; // 比较右孩子和当前最大值节点 if (right < sz && a[right] < a[largest]) largest = right; // 如果最大值不是当前节点,交换并递归调整子树 if (largest != parent) { Swap(&a[parent], &a[largest]); HeapifyDown1(a, sz, largest); } } void HeapPop(Heap* hp) { assert(hp); if (!HeapEmpty(hp)) { //交换堆顶和堆尾数据 Swap(&hp->_a[0], &hp->_a[hp->_size - 1]); hp->_size--; HeapifyDown(hp->_a, hp->_size - 1, 0); } } // 堆排序函数 void HeapSort(HPDataType* a, int sz) { // 构建最大堆 for (int i = 0; i < sz; i++) { HeapIfyUp(a, i); } // 交换堆顶元素与堆尾元素,并调整堆 for (int i = sz - 1; i > 0; i--) { // 交换堆顶元素与堆尾元素 Swap(&a[0], &a[i]); // 调整堆 HeapifyDown(a, i, 0); } }
tree_heap.h
-
#include <stdio.h> #include <string.h> #include <malloc.h> #include <assert.h> #include <stdbool.h> typedef int HPDataType; typedef struct { HPDataType* _a; int _size; int _capacity; }Heap;// 堆的构建 void HeapCreate(Heap* hp, HPDataType* a, int n); // 堆的销毁 void HeapDestory(Heap* hp); // 堆的插入 void HeapPush(Heap* hp, HPDataType x); void HeapInit(Heap* hp); // 堆的删除 void HeapPop(Heap* hp); // 取堆顶的数据 HPDataType HeapTop(Heap* hp); // 堆的数据个数 int HeapSize(Heap* hp); // 堆的判空 int HeapEmpty(Heap* hp); void HeapSort(HPDataType* a, int sz);
6.每期一问
- 上期答案
struct ListNode* insertionSortList(struct ListNode* head) { //创建哨兵位头结点 struct ListNode* pphead = (struct ListNode*)malloc(sizeof(struct ListNode)); //哨兵位头结点的next指向头结点 pphead->next = head; struct ListNode *cur, *next; cur = head; next = head->next; while (next) { if (cur->val <= next->val) { cur = next; } else { struct ListNode* prev = pphead; while (prev->next->val <= next->val) { prev = prev->next; } cur->next = next->next; next->next = prev->next; prev->next = next; } next = cur->next; } struct ListNode* ret = pphead->next; free(pphead); return ret; }
本期问题:. - 力扣(LeetCode)