1 堆的概念和结构
1、简单来说,堆就是一个数组,只不过它的所有元素是按完全二叉树的顺序来存储,来模拟实现二叉树。
2、堆分为小堆和大堆,就比如下图的小根堆示例图:每一个非叶子节点都比它的左右孩子都要小,根节点是最小的元素,并且它的逻辑结构是一棵完全二叉树;对于大根堆示例图则相反:每一个非叶子节点都比它的左右孩子都要大,根节点是最大的元素,并且它的逻辑结构是一棵完全二叉树。
3、对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从 0 开始编号,则对于序号为 i 的结点有:
- 若 i>0 , i 位置结点的双亲序号: (i - 1) / 2 ; i=0 , i 为根结点编号,无双亲结点
- 若 2i+1 < n ,左孩子序号: 2i+1 ;若 2i+1>=n ,无左孩子
- 若 2i+2 < n ,右孩子序号: 2i+2 ;若 2i+2>=n ,无右孩子

2 堆的实现
了解了大堆和小堆后,我们来实现一个小堆,因为堆本质是一个数组,所以我们定义堆的结构为:
#pragma once
//定义堆的结构---数组
#include<stdio.h>
#include<stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* arr;
int size;//有效的数据个数
int capacity;//空间大小
}HP;
void HPInit(HP* php);
void HPDestroy(HP* php);
void HPPush(HP* php, HPDataType x);
void HPPop(HP* php);
HPDataType HPTop(HP* php);
// 判空
bool HPEmpty(HP* php);
void Swap(int* x, int* y);
void AdjustUp(HPDataType* arr, int child);
void AdjustDown(HPDataType* arr, int parent, int n);
我们重点放在向上和向下调整算法
2.1 向上调整算法
定义:对于堆的插入,先将元素插入到堆的末尾,即最后一个孩子之后;插入之后如果堆的性质遭到破坏(这里指比它的父节点要小),将新插入结点顺着其双亲往上调整到合适位置即可,下面给出图例

数组原来是小根堆,现在插入10,因为要建立小根堆,10的父节点28比10大,那么10就要和父节点28交换位置;交换之后还没有结束,因为此时10比父节点18小,两者还要交换,一直交换到10成为根节点,或者10比它的父节点还要小才能停止向上调整。(编号都是从0开始)
//小堆
void HPPush(HP* php, HPDataType x)
{
assert(php);
//空间是否足够
if (php->capacity == php->size)
{
int capacity = (php->capacity == 0) ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->arr, capacity * sizeof(HPDataType));
if (tmp == NULL) return;
php->arr = tmp;
php->capacity = capacity;
}
php->arr[php->size++] = x;
AdjustUp(php->arr, php->size - 1);
}
//向上调整算法
void AdjustUp(HPDataType* arr, int child)
{
int parent = (child - 1) / 2;
//根节点不需要往上调
while (child > 0)
{
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else break;//一旦不用往上调整,前面自然是都符合小堆的结构
}
}
计算向上调整算法建堆时间复杂度:堆是完全二叉树,为了便于计算,我们用特殊的完全二叉树--满二叉树来进行计算。

分析:第1层,个结点,需要向上移动0层;第2层,
个结点,需要向上移动1层;第3层,
个结点,需要向上移动2层....第h层,
个结点,需要向上移动h-1层。由于需要移动结点总的移动步数为:每层结点个数 * 向上调整次数(第⼀层调整次数为0)可以得到总的移动步数为:

根据错位相减和公式 和
可得
,时间复杂度为O(NlogN)。
2.2 向下调整算法
定义:删除堆是删除堆顶的数据,将堆顶的数据跟最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法,直到满足小堆特性。
对元素28向下调整时,需要满足它的左右子树都是一个小堆,

首先,28的左右子树都是小堆。第一步:比较左右孩子,15更小去和28比较,发现需要交换位置;第二步也是重复第一步的过程,直到第三步,发现28小于37,已经符合小堆特性了,就不用再向下调整。
//删除堆顶元素
void HPPop(HP* php)
{
assert(php && php->size);
Swap(&php->arr[0], &php->arr[php->size - 1]);
php->size--;
AdjustDown(php->arr, 0, php->size);
}
//n-->arr有效个数
void AdjustDown(HPDataType* arr, int parent, int n)
{
int child = parent * 2 + 1;//左孩子
while (child < n)
{
if (child + 1 < n && arr[child] > arr[child+1])
{
child++;
}
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else break;
}
}
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
计算向下调整算法建堆时间复杂度:


计算方式和向上调整算法一样,最后得到向下调整算法建堆时间复杂度为O(N),从另一方面来讲:向上调整法建堆时,层数小的结点少,需要往上移动次数少,层数大的结点多,需要往上移动次数多;向下调整法建堆时,层数小的结点少,需要往下移动次数多,层数大的结点多,需要往下移动次数少,明显向下调整法建堆效率更高。所以建堆时可以优先考虑向下调整法(从最后一个非叶子节点开始建堆,一直到根节点)。
3 堆的应用
3.1 堆排序
堆排序有两种做法,第一种就是前面的建堆函数HpPush()和删除根节点元素函数HpPop()结合,但是有个弊端就是,必须要借助类似现成的数据结构Hp才能实现;第二种就是在数组建堆,建堆后,首尾交换,交换后的堆尾数据从堆中删掉,将堆顶数据向下调整。下面给出版本二代码:
//堆排序
void HeapSort(int* arr, int n)
{
//原arr不是堆
// 建堆
//升序--建大堆
//降序--建小堆
// 向上调整法建堆O(NlogN)
//for (int i = 0; i < n; i++)
//{
// AdjustUp(arr, i);
//}
//向下调整法建堆O(N)
for (int i = (n-1-1)/2; i >=0; i--)
{
AdjustDown(arr, i, n);
}
int end = n - 1;
//O(NlogN)
while (end)
{
Swap(&arr[end], &arr[0]);
AdjustDown(arr, 0, end);
end--;
}
}
3.2 Top-K问题
即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
(1)用数据集合中前K个元素来建堆:要前k个最大的元素,则建小堆(可以理解为,要往堆里放大的数,那每一次都要牺牲最小的数,就是堆顶元素);要前k个最小的元素,则建大堆(可以理解为,要往堆里放小的数,那每一次都要牺牲最大的数,就是堆顶元素)
(2)用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素:前k个最大的元素,比堆顶大就替换;前k个最小的元素,比堆顶小就替换。
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素
下面给出创造一些数据的代码,和如何求前K个最大的元素或者最小的元素(注意小堆和大堆的向下调整代码要区分以及srand,rand,malloc头文件的添加!!!)
void CreateNDate()
{
//造100000个数据
int n = 100000;
srand(time(NULL));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < n; i++)
{
int x = (rand() + i) % 500000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
void Top_K_Min()
{
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error");
exit(1);
}
//若要前K个小的数据
int k = 0;
printf("输入K的大小:");
scanf("%d", &k);
int* minHeap = (int*)malloc(k * sizeof(int));
if (minHeap == NULL)
{
perror("malloc error");
exit(2);
}
//1 取文件前K个数据建大堆
for (int i = 0; i < k; i++)
{
if (fscanf(fout, "%d", &minHeap[i]) != 1)
{
printf("文件数据不足或格式异常!在第 %d 个数据处失败\n", i);
exit(4);
}
}
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(minHeap, i, k);
}
//2 用剩余的N-K个元素依次与堆顶元素比较,比堆顶元素大就替换
int x = 0;
while (fscanf(fout, "%d", &x) != EOF)
{
if (x < minHeap[0])
{
minHeap[0] = x;
AdjustDown(minHeap, 0, k);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", minHeap[i]);
}
fclose(fout);
}
void Top_K_Max()
{
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error");
exit(1);
}
//若要前K个大的数据
int k = 0;
printf("输入K的大小:");
scanf("%d", &k);
int* minHeap = (int*)malloc(k * sizeof(int));
if (minHeap == NULL)
{
perror("malloc error");
exit(2);
}
//1 取文件前K个数据建小堆
for (int i = 0; i < k; i++)
{
if (fscanf(fout, "%d", &minHeap[i]) != 1)
{
printf("文件数据不足或格式异常!在第 %d 个数据处失败\n", i);
exit(4);
}
}
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(minHeap, i, k);
}
//2 用剩余的N-K个元素依次与堆顶元素比较,比堆顶元素大就替换
int x = 0;
while (fscanf(fout, "%d", &x)!=EOF)
{
if (x > minHeap[0])
{
minHeap[0] = x;
AdjustDown(minHeap, 0, k);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", minHeap[i]);
}
fclose(fout);
}

8347

被折叠的 条评论
为什么被折叠?



