堆的应用(建堆、堆排序、TOP-K问题)
目录
1.建堆(以大堆为例)
1.1向上调整法建大堆
1.2向下调整法建大堆
1.3建堆方法的时间复杂度
2.堆排序(以升序为例)
3.TOP-K问题
筛选前K大
1.建堆(以大堆为例)
给定一个数组,在此基础上进行建大堆操作
1.1向上调整法建大堆
模拟堆插入数据的操作,从数组的第二个元素开始进行向上调整操作,直到最后一个元素调整结束为止,最终可实现大堆
原理:每一次向上调整(算法正确的前提下)都会将所给数据构成的树实现为大堆
void AdjustUp(HpDateType* a, int child)
{//给定数组为空时,没必要进行下面步骤assert(a);int parent = (child - 1) / 2;//parent恒大于等于0while (child > 0){if (a[child] > a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}//向上调整法建大堆,模拟插入的过程 O(N*logN)
for (int i = 1; i < n; ++i)
{AdjustUp(a, i);
}
1.2向下调整法建大堆
思路:从倒数第一个非叶子结点开始进行向下调整操作,直到根节点为止,最终可实现大堆
原理:每一次向下调整(算法正确的前提下)都会将以该节点为根节点的树调整为大堆
void AdjustDown(HpDateType* a, int n, int parent)
{int child = parent * 2 + 1;//有左孩子才进行调整while (child < n)//n是节点个数{//左孩子一个逻辑,右孩子一个逻辑,直接假设//child是左右孩子中较大的一个孩子的下标//注意右孩子的有无(防止越界访问与无效数据)if (child + 1 < n && a[child] < a[child + 1]){++child;}//与左右孩子中较大的一个孩子进行比较if (a[parent] < a[child]){Swap(&a[parent], &a[child]);parent = child;child = parent * 2 + 1;}else{break;}}
}//向下调整法建大堆 O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{AdjustDown(a, n, i);
}
1.3建堆方法的时间复杂度
向上调整法建堆,时间复杂度是O(N*logN)
向下调整法建堆,时间复杂度是O(N)
显然, 向下调整法建堆 优于 向上调整法建堆
记忆方法:向下建堆时,节点数越多的所在层的节点调整次数越少,节点数越少的所在层的节点调整次数越多;向上建堆时,节点数越多的所在层的节点调整次数越多,节点数越少的所在层的节点调整次数越少
可通过计算最坏情况下树高为h的满二叉树的调整次数得出相应时间复杂度的结论(需结合树高与节点数的关系以及错位相减法的知识点)
亦可通过估算最坏情况下底层节点数据的调整次数得出相应时间复杂度的结论,因为底层数据几乎占所有数据的一半
2.堆排序(以升序为例)
排升序,建大堆;排降序,建小堆
思路:先将所给数组进行建堆操作,再将堆顶元素与末尾元素进行交换,然后在除了末尾元素之外的数据基础上进行向下调整操作,使得次大元素位于堆顶,然后再将次大元素与倒数第二个元素进行交换........如此循环,直到堆顶元素与数组第二个元素进行交换后为止,最终可实现升序
void HeapSort(int* a, int n)
{//向下调整法建大堆 O(N)for (int i = (n - 1 - 1) / 2; i >= 0; --i){AdjustDown(a, n, i);}//排升序,建大堆 O(N*logN)int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;}
}
堆排序的原理:通过逐步缩小堆的范围来排序数组,当堆中只剩一个元素时(即
end = 0
),排序过程已完成所以 end > 0 作为循环结束条件
3.TOP-K问题
TOP-K问题:求数据中前K个最大的元素或者最小的元素,一般情况下数据量都比较大
求前K个最大的数据的思路:
(1)构建一个大小为K的小根堆,初始时存入数组前K个元素,并调整为堆结构。此时堆顶是堆内K个元素的最小值,作为全局前K大的“门槛”
(2)遍历剩余数据:若当前元素 大于堆顶,说明它可能属于前K大,于是替换堆顶,并执行向下调整操作;若当前元素 小于等于堆顶,则跳过(因为它不可能属于前K大)。
最终堆中保留的即为全局前K大的元素
原理:
(1)小堆的堆顶始终是当前候选集中的最小值,插入的较大元素会通过堆调整找到正确位置(不一定是叶子节点),而堆顶始终更新为新的最小值,从而保证堆内元素的正确性
不用大堆是因为如果所有数据中的最大值位于大堆堆顶,那么就无法通过有效的比较逻辑筛选出余下的较大值
求前K个最小的数据的思路:
(1)构建一个大小为K的大根堆,初始时存入数组前K个元素,并调整为堆结构。此时堆顶是堆内K个元素的最大值,作为全局前K小的“门槛”
(2)遍历剩余数据:若当前元素 小于堆顶,说明它可能属于前K小,于是替换堆顶,并执行向下调整操作;若当前元素 大于等于堆顶,则跳过(因为它不可能属于前K小)。
最终堆中保留的即为全局前K小的元素
原理:
(1)大堆的堆顶始终是当前候选集中的最大值,插入的较小元素会通过堆调整找到正确位置(不一定是叶子节点),而堆顶始终更新为新的最大值,从而保证堆内元素的正确性
不用小堆是因为如果所有数据中的最小值位于大堆堆顶,那么就无法通过有效的比较逻辑筛选出余下的较小值
筛选前K大
void PrintTopK(const char* file, int k)
{FILE* fp = fopen(file, "r");if (fp == NULL){perror("fopen fail");return;}//存储前k个int* a = (int*)malloc(sizeof(int) * k);if (a == NULL){perror("malloc fail");return;}for (int i = 0; i < k; ++i){fscanf(fp, "%d", &a[i]);}//建小堆for (int i = (k - 2) / 2; i >= 0; --i){AdjustDown(a, k, i);}//读取后续并比较int val = 0;int ret = fscanf(fp, "%d", &val);while (ret != EOF){if (val > a[0]){Swap(&val, &a[0]);AdjustDown(a, k, 0);}ret = fscanf(fp, "%d", &val);}for (int i = 0; i < k; ++i){printf("%d ", a[i]);}fclose(fp);fp = NULL;free(a);a = NULL;
}