🥁作者: 华丞臧.
📕专栏:【数据结构】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注
)。如果有错误的地方,欢迎在评论区指出。
推荐一款刷题网站 👉 LeetCode刷题网站
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
如果有一个关键码的集合K = { k0,k1 ,k2 ,…,kn-1 },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <=K2i+1 且Ki <=K2i+2 ( Ki >=K2i+1 且Ki >=K2i+2 ) i = 0,1,2…,则称为小堆(或大堆)。
堆的性质:
#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");
}
堆排序即利用堆的思想来进行排序,分为两个步骤:
直接插入建堆:
- 升序:建大堆
- 降序:建小堆
用向下调整算法来进行排序,依次选数,从后往前排。
调整次数 = 每一层节点数
* 这一层节点最坏调整次数
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)
对比两个建堆方式,向下调整建堆更好,其时间复杂度较小。
堆排序的大思路是选数排序,依次选数,从后往前排。堆的最后是叶子节点,叶子结点单独可以看做一个堆不用进行向下调整,所以从堆的倒数第一个非叶子节点开始调整,一直调整到根;而最后一个非叶子节点也就是最后一个父亲节点,就是最后节点非父节点。
#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;
}
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
当数据量不大的时候,可以直接把数据存放在内存当中进行选 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
个最大(最小)的元素。
同样的:
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);
}
#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);
#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;
}