[数据结构]二叉树之堆的实现
创始人
2024-04-09 16:03:31
0

🥁作者华丞臧.
📕​​​​专栏:【数据结构
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注)。如果有错误的地方,欢迎在评论区指出。
推荐一款刷题网站 👉 LeetCode刷题网站


文章目录

  • 一、堆
    • 1.1 堆的概念及结构
  • 二、堆的实现
    • 2.1 堆的接口
      • 🚀初始化堆
      • 🚀销毁堆
      • 🚀堆插入
      • 🚀堆删除
      • 🚀取堆定元素
      • 🚀堆的数据个数
      • 🚀堆判空
      • 🚀打印
  • 三、堆的应用
    • 3.1 堆排序
      • 3.1.1 建堆的方式
        • 🚀向下调整建堆
        • 🚀向上调整建堆
      • 3.1.2 堆排序实现
    • 3.2 TOP-K问题
      • 3.2.1 代码实现
        • 🚀数据量不大
        • 🚀数据量很大
  • 四、堆完整代码
    • 4.1 heap.h
    • 4.2 heap.c


一、堆

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段

1.1 堆的概念及结构

如果有一个关键码的集合K = { k0,k1 ,k2 ,…,kn-1 },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <=K2i+1 且Ki <=K2i+2 ( Ki >=K2i+1 且Ki >=K2i+2 ) i = 0,1,2…,则称为小堆(或大堆)

  • 最大堆或大根堆:根节点最大的堆。
  • 最小堆或小根堆:根节点最小的堆。

堆的性质:

  1. 堆中某个节点的值总是不大于或不小于其父节点的值;
  2. 堆总是一棵完全二叉树。

在这里插入图片描述

二、堆的实现

2.1 堆的接口

#define _CRT_SECURE_NO_WARNINGS 1
#include 
#include 
#include 
#include typedef int HPDataType;typedef struct Heap
{HPDataType* data;int size;int capacity;
}HP;//初始化堆
void HeapInit(HP* php);//堆销毁
void HeapDestroy(HP* php);//堆插入
void HeapPush(HP* php, HPDataType x);//堆删除
void HeapPop(HP* php);//打印
void HeapPrint(HP* hp);//取堆顶元素
HPDataType HeapTop(HP* hp);//堆的数据个数
int HeapSize(HP* hp);//判空
bool HeapEmpty(HP* hp);

🚀初始化堆

初始化堆。

//初始化堆
void HeapInit(HP* php)
{assert(php);php->data = NULL;php->size = php->capacity = 0;
}

🚀销毁堆

//堆销毁
void HeapDestroy(HP* php)
{assert(php);free(php->data);php->data = NULL;php->size = php->capacity = 0;
}

🚀堆插入

先将元素插入到堆的尾部,如果插入之后堆的性质遭到破坏,将新插入的元素顺着其双亲往上调整到合适的位置上即可。

//交换
void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}//向上调整
void AdjustUp(HPDataType* a, int child)
{assert(a);int parent = (child - 1) / 2;while (parent >= 0){if (a[parent] > a[child]){/*HPDataType tmp = a[parent];a[parent] = a[child];a[child] = tmp;*/Swap(&a[parent], &a[child]);child = parent;parent = (child - 1) / 2;}else{break;}}
}//堆插入
void HeapPush(HP* php, HPDataType x)
{assert(php);//检查是否需要扩容if (php->size == php->capacity){int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;HPDataType* tmp = (HPDataType*)realloc(php->data,sizeof(HPDataType) * newCapacity);if (tmp == NULL){perror("realloc fail");exit(-1);}php->data = tmp;php->capacity = newCapacity;}php->data[php->size] = x;++php->size;//向上调整AdjustUp(php->data,php->size - 1);
}

🚀堆删除

删除堆是删除堆顶的数据,将堆顶的数据和最后一个数据交换,然后删除数据最后一个数据即size 减一,再进行向下调整,把堆顶元素向下调整到满足堆的特性即可。

//向下调整
void AdjustDown(HPDataType* a, int size)
{assert(a);int parent = 0;int minchild = parent * 2 + 1;while (parent < size && minchild < size){if (minchild < size && a[minchild] > a[minchild + 1]){minchild += 1;}if (a[minchild] < a[parent]){/*HPDataType tmp = a[parent];a[parent] = a[minchild];a[minchild] = tmp;*/Swap(&a[parent], &a[minchild]);parent = minchild;minchild = parent * 2 + 1;}else{break;}}
}//堆删除
void HeapPop(HP* php)
{assert(php);assert(!HeapEmpty(php));HPDataType tmp = php->data[0];php->data[0] = php->data[php->size - 1];php->data[php->size - 1] = tmp;--php->size;//向下调整AdjustDown(php->data, php->size);
}

🚀取堆定元素

堆顶也就是数组下标为 0 的位置。

//取堆顶元素
HPDataType HeapTop(HP* php)
{assert(php);return php->data[0];
}

🚀堆的数据个数

size 表示堆的有效数据的个数。

//堆的数据个数
int HeapSize(HP* php)
{assert(php);return php->size;
}

🚀堆判空

size 等于 0 时,堆为空。

//判空
bool HeapEmpty(HP* php)
{assert(php);return php->size == 0;
}

🚀打印

//打印
void HeapPrint(HP* php)
{int i = 0;while (i < php->size){printf("%d ", php->data[i]);i++;}printf("\n");
}

三、堆的应用

3.1 堆排序

堆排序即利用堆的思想来进行排序,分为两个步骤:

  1. 建堆

直接插入建堆:

  • 升序:建大堆
  • 降序:建小堆
  1. 利用堆删除思想来进行排序

用向下调整算法来进行排序,依次选数,从后往前排。

3.1.1 建堆的方式

🚀向下调整建堆

在这里插入图片描述
调整次数 = 每一层节点数 * 这一层节点最坏调整次数

T(N) = 20 *(h-1)+21 *(h-2)+22 *(h-3)+……+2(h-3) *2+2(h-2) *1 ----(1)

2 * T(N) = 21 *(h-1)+22 *(h-2)+23 *(h-3)+……+2(h-2) *2+2(h-1)*1 ----(2)
上面两式(1)-(2)相减可得: T(N) = 20 +21 +22 +……+2(h-3) +2(h-2) +2(h-1) -h = 2h-1-h
高度为h,节点数量为N的完全二叉树

  • N = 2h-1
  • h = log2(N+1)

所以T(N) = N-log2(N+1) ≈ N
因此:建堆的时间复杂度为O(N)

🚀向上调整建堆

在这里插入图片描述
调整次数 = 每一层节点数 * 这一层节点最坏调整次数

T(N) = 20 *1+21 *2+22 *3+……+2(h-2) *(h-2)+2(h-1) *(h-1) ----(1)
2 * T(N) = 21 *1+22 *2+23 *3+……+2(h-1) *(h-2)+2h *(h-1) ----(2)
上面两式(1)-(2)相减可得: T(N) = 21 *1+22 *2+23 *3+……+2(h-1) +2h *(h-1) - 20
高度为h,节点数量为N的完全二叉树

  • N = 2h-1
  • h = log2(N+1)

所以T(N) = 2(h-1)+2h * (h-1) -2 ≈ 2h * h
因此:建堆的时间复杂度为O(N*log2N)

对比两个建堆方式,向下调整建堆更好,其时间复杂度较小。

3.1.2 堆排序实现

堆排序的大思路是选数排序,依次选数,从后往前排。堆的最后是叶子节点,叶子结点单独可以看做一个堆不用进行向下调整,所以从堆的倒数第一个非叶子节点开始调整,一直调整到根;而最后一个非叶子节点也就是最后一个父亲节点,就是最后节点非父节点。

#include 
#include 
#include 
#include void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}//向下调整
void AdjustDown(int* a, int size,int parent)
{assert(a);int minchild = parent * 2 + 1;while ( minchild < size){if (minchild + 1 < size && a[minchild] > a[minchild + 1]){minchild += 1;}if (a[minchild] < a[parent]){Swap(&a[parent], &a[minchild]);parent = minchild;minchild = parent * 2 + 1;}else{break;}}
}void HeapSort(int* a, int n)
{//向下调整建堆 -- O(N)for (int i = (n-2) / 2; i >= 0; --i){AdjustDown(a, n,i);}//选数int i = 1;while (i < n){Swap(&a[0],&a[n - i]);AdjustDown(a,n - i,0);++i;}
}int main()
{int a[] = { 1,5,3,8,7,6 };HeapSort(a,sizeof(a)/sizeof(a[0]));for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++){printf("%d ",a[i]);}return 0;
}

3.2 TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前K个元素来建堆
    • 前k个最大的元素,则建小堆
    • 前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

3.2.1 代码实现

🚀数据量不大

当数据量不大的时候,可以直接把数据存放在内存当中进行选 TOP-K 的操作。

#include 
#include 
#include 
#include void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}//向下调整
void AdjustDown(int* a, int size,int parent)
{assert(a);int minchild = parent * 2 + 1;while ( minchild < size){if (minchild + 1 < size && a[minchild] > a[minchild + 1]){minchild += 1;}if (a[minchild] < a[parent]){Swap(&a[parent], &a[minchild]);parent = minchild;minchild = parent * 2 + 1;}else{break;}}
}void HeapSort(int* a, int n)
{//向下调整建堆 -- O(N)for (int i = (n-2) / 2; i >= 0; --i){AdjustDown(a, n,i);}//选数int i = 1;while (i < n){Swap(&a[0],&a[n - i]);AdjustDown(a,n - i,0);++i;}
}void PrintTopK(int* a, int n, int k)
{// 1. 建堆--用a中前k个元素建堆for (int i = (k - 2) / 2; i > 0; --i){AdjustDown(a,k,i);}// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换int i = k;while (i < n){if (a[0] < a[i]){Swap(&a[0], &a[i]);AdjustDown(a,k,0);}i++;}HeapSort(a, k);for (i = 0; i < k; i++){printf("%d ",a[i]);}
}void TestTopk()
{int n = 10000;int* a = (int*)malloc(sizeof(int) * n);if (a == NULL){perror("malloc fail");return;}srand(time(0));for (size_t i = 0; i < (size_t)n; ++i){a[i] = rand() % 1000000;}a[5] = 1000000 + 1;a[1231] = 1000000 + 2;a[531] = 1000000 + 3;a[5121] = 1000000 + 4;a[115] = 1000000 + 5;a[2335] = 1000000 + 6;a[9999] = 1000000 + 7;a[76] = 1000000 + 8;a[423] = 1000000 + 9;a[3144] = 1000000 + 10;/*int a[] = { 4,1,5,3,6,8,9,7,0,2 };int n = sizeof(a) / sizeof(a[0]);*/PrintTopK(a, n, 10);
}int main()
{TestTopk();return 0;
}

代码运行结果:
在这里插入图片描述

🚀数据量很大

对于数据量非常大时,内存存放不下,数据存放在本地文件中;这时就需要我们从文件中依次读取数据,然后再比较并且保留前 K 个最大(最小)的元素。
同样的:

  • 前k个最大的元素,则建小堆
  • 前k个最小的元素,则建大堆
void PrintTopK(const char* filename, int k)
{assert(filename);//filename存放数据的文件名FILE* fout = fopen(filename, "r");if (fout == NULL){perror("fopen fail");exit(-1);}int* minHeap = (int*)malloc(sizeof(int)*k);if (minHeap == NULL){perror("malloc fail");exit(-1);}// 读取前K个数据for (int i = 0; i < k; ++i){fscanf(fout, "%d", &minHeap[i]);}//向下调整建堆for (int i = (k - 2) / 2; i >= 0; --i){AdjustDown(minHeap,k,i);}//选前TOP-K个int val = 0;while (fscanf(fout,"%d",&val) != EOF){if (minHeap[0] < val){minHeap[0] = val;AdjustDown(minHeap,k,0);}}for (int i = 0; i < k; ++i){printf("%d ", minHeap[i]);}free(minHeap);fclose(fout);
}

四、堆完整代码

4.1 heap.h

#define _CRT_SECURE_NO_WARNINGS 1
#include 
#include 
#include 
#include typedef int HPDataType;typedef struct Heap
{HPDataType* data;int size;int capacity;
}HP;//初始化堆
void HeapInit(HP* php);//堆销毁
void HeapDestroy(HP* php);//堆插入
void HeapPush(HP* php, HPDataType x);//堆删除
void HeapPop(HP* php);//打印
void HeapPrint(HP* php);//取堆顶元素
HPDataType HeapTop(HP* php);//堆的数据个数
int HeapSize(HP* php);//判空
bool HeapEmpty(HP* php);

4.2 heap.c

#include "heap.h"//初始化堆
void HeapInit(HP* php)
{assert(php);php->data = NULL;php->size = php->capacity = 0;
}//堆销毁
void HeapDestroy(HP* php)
{assert(php);free(php->data);php->data = NULL;php->size = php->capacity = 0;
}void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}//向上调整
void AdjustUp(HPDataType* a, int child)
{assert(a);int parent = (child - 1) / 2;while (child > 0){if (a[parent] > a[child]){Swap(&a[parent], &a[child]);child = parent;parent = (child - 1) / 2;}else{break;}}
}//堆插入
void HeapPush(HP* php, HPDataType x)
{assert(php);//检查是否需要扩容if (php->size == php->capacity){int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;HPDataType* tmp = (HPDataType*)realloc(php->data,sizeof(HPDataType) * newCapacity);if (tmp == NULL){perror("realloc fail");exit(-1);}php->data = tmp;php->capacity = newCapacity;}php->data[php->size] = x;++php->size;//向上调整AdjustUp(php->data,php->size - 1);
}//打印
void HeapPrint(HP* php)
{int i = 0;while (i < php->size){printf("%d ", php->data[i]);i++;}printf("\n");
}//向下调整
void AdjustDown(HPDataType* a, int size, int parent)
{assert(a);int minchild = parent * 2 + 1;while (minchild < size){if (minchild + 1 < size && a[minchild] > a[minchild + 1]){minchild++;}if (a[minchild] < a[parent]){Swap(&a[parent], &a[minchild]);parent = minchild;minchild = parent * 2 + 1;}else{break;}}
}//堆删除
void HeapPop(HP* php)
{assert(php);assert(!HeapEmpty(php));Swap(&php->data[0], &php->data[php->size - 1]);--php->size;//向下调整AdjustDown(php->data, php->size,0);
}//取堆顶元素
HPDataType HeapTop(HP* php)
{assert(php);return php->data[0];
}//堆的数据个数
int HeapSize(HP* php)
{assert(php);return php->size;
}//判空
bool HeapEmpty(HP* php)
{assert(php);return php->size == 0;
}

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...