目录
堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。
如果有一个关键码的集合K = { k0,k1 ,k2 ,…,kn-1 },把它的所有元素按完全二叉树的顺序存储方式存储
在一个一维数组中,并满足:
则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
堆的实现
堆向下调整算法
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整
成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。int array[] = {27,15,19,18,28,34,65,49,25,37};
堆的创建
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算
法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的
子树开始调整,一直调整到根节点的树,就可以调整成堆。int a[] = {1,5,3,8,7,6};
建堆时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的
就是近似值,多几个节点不影响最终结果):因此:建堆的时间复杂度为O(N)。
堆的插入
先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。
堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调
整算法。
堆的代码实现
先看看具体需要实现哪些功能
typedef int HPDataType; typedef struct Heap { HPDataType* a; int size;//大小 int capacity;//最大容量 }Heap; //堆的初始化 void HeapInit(Heap* hp, HPDataType* a, int n); // 堆的销毁 void HeapDestory(Heap* hp); // 堆的插入 void HeapPush(Heap* hp, HPDataType x); // 堆的删除 void HeapPop(Heap* hp); // 取堆顶的数据 HPDataType HeapTop(Heap* hp); // 堆的数据个数 int HeapSize(Heap* hp); // 堆的判空 int HeapEmpty(Heap* hp); //堆的打印 void HeapPrint(Heap*hp); //向上调整(大堆/小堆) void AdjustUp(HPDataType* a, int n, int child); //向下调整(大堆/小堆) void AdjustDown(HPDataType*a,int n,int root); //交换 void swap(HPDataType*a, HPDataType*b);下面来看看具体实现
堆的初始化
void HeapInit(Heap* hp, HPDataType* a, int n) { assert(hp&&a); hp->a = (HPDataType*)malloc(sizeof(HPDataType)*n); hp->size = n; hp->capacity = n; for (int i = 0; i < n; i++) { hp->a[i] = a[i]; } for (int i=(n-2)/2;i>=0;i--) { AdjustDown(hp->a,hp->size,i); } }堆的销毁
void HeapDestory(Heap* hp) { assert(hp); hp->size = 0; hp->capacity = 0; free(hp->a); hp->a = NULL; }堆的插入
void HeapPush(Heap* hp, HPDataType x) { assert(hp); //扩容 if (hp->size == hp->capacity) { hp->capacity *= 2; hp->a = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * hp->capacity); } hp->a[hp->size] = x; hp->size++; //向上调整 AdjustUp(hp->a,hp->size,hp->size-1); }堆的删除
// 堆的删除 void HeapPop(Heap* hp) { assert(hp); //先把第一个和最后一个交换 swap(&hp->a[0], &hp->a[hp->size - 1]); //删掉最后一个 hp->size--; //向下调整 AdjustDown(hp->a,hp->size,0); }获取堆顶的数据
// 取堆顶的数据 HPDataType HeapTop(Heap* hp) { assert(hp); return hp->a[0]; }堆的数据个数
// 堆的数据个数 int HeapSize(Heap* hp) { assert(hp); return hp->size; }堆的判空
// 堆的判空 int HeapEmpty(Heap* hp) { assert(hp); return hp->size == 0 ; }堆的打印
//堆的打印 void HeapPrint(Heap*hp) { assert(hp); for (int i = 0; i < hp->size; i++) { printf("%d ", hp->a[i]); } printf("\n"); }向上调整
//向上调整(大堆) void AdjustUp(HPDataType* a, int n, int child) { assert(a); int parent = (child - 1) / 2; while (child>0&&a[child]>a[parent]) { swap(&a[parent],&a[child]); child = parent; parent = (child - 1) / 2; } } //向上调整(小堆) void AdjustUp(HPDataType* a, int n, int child) { assert(a); int parent = (child - 1) / 2; while (child>0&&a[child]<a[parent]) { swap(&a[parent],&a[child]); child = parent; parent = (child - 1) / 2; } }向下调整
//向下调整(大堆) void AdjustDown(HPDataType*a,int n,int root) { assert(a); int parent = root; int child = root*2+1; if (child+1<n&&a[child+1] > a[child ]) { child++; } while (child<n&&a[child]>a[parent]) { swap(&a[child],&a[parent]); parent = child; child = 2 * parent + 1; if (child<n && a[child+1] > a[child ]) { child++; } } } //向下调整(小堆) void AdjustDown(HPDataType*a,int n,int root) { assert(a); int parent = root; int child = root*2+1; if (child+1<n&&a[child+1] < a[child ]) { child++; } while (child<n&&a[child]<a[parent]) { swap(&a[child],&a[parent]); parent = child; child = 2 * parent + 1; if (child<n && a[child+1] <a[child ]) { child++; } } }交换
//交换 void swap(HPDataType*a, HPDataType*b) { HPDataType tmp = *a; *a = *b; *b = tmp; }
堆的应用
堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
升序:建大堆
降序:建小堆
2. 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。下面是借用大堆来实现排升序的代码
//建大堆排升序 void HeapSort(int *a,int n) { //先建大堆 for (int i = (n - 2) / 2; i >= 0; i--) { AdjustDown(a , n, i);//大堆 } //再排序 for (int i = n-1; i > 0; i--) { swap(&a[0], &a[i]); AdjustDown(a, i, 0);//大堆 } }TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能
数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
//建小堆来取最大前K个数 void PrintTopK(int* a, int n, int k) { assert(a); Heap hp; // 创建一个K个数的小堆 HeapInit(&hp,a,k); for (int i=k;i<n;i++) { if (HeapTop(&hp) < a[i]) { HeapPop(&hp); HeapPush(&hp, a[i]); } } for (int i = 0; i < k; i++) { printf("%d ", HeapTop(&hp)); HeapPop(&hp); } } //测试 void TestTopk() { int n = 10000; int* a = (int*)malloc(sizeof(int)*n); srand(time(0)); for (size_t i = 0; i < 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; PrintTopK( a, n, 10); }






