赞
踩
前情回顾:二叉树 —— 堆的实现(顺序存储)
上一篇,介绍了堆(Heap)的实现,并且用堆的结构特点简单的实现了堆排序,但是之前实现的堆排序的空间复杂度是〇(N),还可以吧继续优化。
本篇将详细讲解一下:
下面两端代码是原地建堆的核心:
//向上调整算法 void AdjustUp(int* a, size_t child) { size_t parent = (child - 1) / 2; while (child > 0) { if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } }
//向下调整算法 void AdjustDown(int* a, size_t size, size_t root) { size_t parent = root; size_t child = parent * 2 + 1;//左孩子 while (child < size) { //1.选出左右孩子中小的那一个 if (child + 1 < size && a[child + 1] < a[child]) { child++; } //2.如果孩子小于父亲,则交换,并继续往下调整 if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = parent * 2 + 1; } else { break; } } }
原地建堆有两种方法:
这里的建堆方法和上一篇的区别是,之前的建堆是要额外开辟大小为N的数组,空间复杂度〇(N) ,这里是原地建堆,不开辟额外的空间,空间复杂度:〇(1)
int main() { int a[] = { 4,2,7,8,5,1,0,6 }; //向上调整 -- 建堆〇(N*logN) int size = sizeof(a) / sizeof(int); for (int i = 1; i < size; i++)// 0 位置调整没价值 { AdjustUp(a, i); } //打印堆 for (int i = 0; i < size; i++) { printf("%d ", a[i]); } printf("\n"); return 0; }
向上调整建立在:前面是堆的前提下。
int main() { int a[] = { 4,2,7,8,5,1,0,6 }; //向下调整 -- 建堆(前提:左右子树都是堆的结构)〇(N) int size = sizeof(a) / sizeof(int); for (int i = (size - 1 - 1) / 2; i >= 0; i--) { AdjustDown(a, size, i); } //打印堆 for (int i = 0; i < size; i++) { printf("%d ", a[i]); } printf("\n"); return 0; }
* 不能开辟新空间,如果先将第一个数据从头往下调整的话,调完之后有可能就不是堆的结构!
这里依旧强调一点:向上向下调整都要建立在堆结构这个基础上!
正确向下建堆方法:
假设每次调整都是最坏的情况,每次插入时都要从堆底调到堆顶,那么总的时间复杂度该怎么算呢。
向上调整是从第二层开始调整,最坏的情况下:
所以向上建堆的时间复杂度:〇(N*logN)
向下调整是从倒数第二层开始调整,最坏的情况下:
所以向下建堆的时间复杂度:〇(N)
经过较为精确的计算,我们发现,向下建堆的时间复杂度比向上建堆要好,所以我们建堆基本上都是向下建堆。
那么问题来了升序是建大堆好呢还是建小堆好呢?
最小的数已经在第一个位置
拿走堆顶最小的数之后
剩下的数关系全乱了,需要重新建堆(向下),建堆要〇(N)
再选出次小的,不断建堆
那么时间复杂度就是等差数 - 时间复杂度〇(N^2)
如果这样,还不如直接遍历选数!搞得这么复杂。
遍历选数:时间复杂度〇(N)
这种方法可以是可以,但是效率不行,还更复杂!(不好!)
所以升序建小堆的话,时间复杂度是很不好的。
void HeapSort(int* a, size_t size) { //向下调整 -- 建堆(前提:左右子树都是堆的结构)〇(N) for (int i = (size - 1 - 1) / 2; i >= 0; i--) { AdjustDown(a, size, i); } size_t end = size - 1;//最后一个数据的下标 while (end > 0) { Swap(&a[0], &a[end]); AdjustDown(a, end, 0); end--; } } int main() { int a[] = { 4,2,7,8,5,1,0,6 }; HeapSort(a, sizeof(a) / sizeof(int)); for (int i = 0; i < sizeof(a) / sizeof(int); i++) { printf("%d ", a[i]); } printf("\n"); return 0; }
我们来算一下堆排序的时间复杂度:
所以升序建大堆效果最好,和快速排序一样好。
总结:
TOP - K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top - K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
方法:
这样虽然可以,但是当处理海量数据的时候,就会出现内存不够的现象
没那么大内存的电脑!
如何求解:
void PrintTopK(int* a, int n, int k) { // 1. 建堆--用a中前k个元素建堆 int* KminHeap = (int*)malloc(sizeof(int) * k); assert(KminHeap); //进堆 for (int i = 0; i < k; i++) { KminHeap[i] = a[i]; } //建小堆 - 向下建堆 for (int j = (k - 1 - 1) / 2; j >= 0; j--) { AdjustDown(KminHeap, k, j); } // 2. 将剩余n-k个元素依次与堆顶元素交换,不满则替换 for (int i = k; i < n; i++) { if (a[i] > KminHeap[0]) { KminHeap[0] = a[i]; AdjustDown(KminHeap, k, 0); } } for (int j = 0; j < k; j++) { printf("%d ", KminHeap[j]); } printf("\n"); free(KminHeap); } void TestTopk() { int n = 10000; int* a = (int*)malloc(sizeof(int) * n); assert(a); srand(time(0)); for (int i = 0; i < n; ++i) { a[i] = (int)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); } int main() { TestTopk(); return 0; }
通过设置随机值将所有的数设置成1000000以内的数,再随便设置是个数大于1000000,最后的结果一定是这十个数。
这里不是有序的,只是一个小堆的结构,因为堆不一定有序。
堆 : 数组实现堆,实际上操纵的是一个数组,逻辑上想象的是完全二叉树!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。