目录
一、堆的概念
二、堆的结构
三、性质
(1)堆的性质
(2)二叉树的性质
四、堆的实现
(1)头文件——Heap.h
(2)源文件——Heap.c
1.堆的初始化
2.堆的销毁
3.向上调整算法
4.堆的插入
4.判断堆是否为空
5. 向下调整算法
6.堆的删除
7.取堆顶数据
8.求有效数据个数
五、两种调整算法
(1)向上调整算法
1.作用
2.复杂度
(2)向下调整算法
1.前提
2.作用
3.复杂度
六、堆的应用
(1)堆排序
1.版本一
2.版本二
3.复杂度
(2)TOP-K问题
1.描述
2.思路
3.实现
4.时间复杂度
七、写在最后
一、堆的概念
堆一般使用顺序结构的数组来存储数据,堆是⼀种特殊的⼆叉树,具有⼆叉树的特性的同时,还具备其他的特性。
如果有⼀个关键码的集合K = {k0 , k1 , k2 , ...,kn−1 } ,把它的所有元素按完全⼆叉树的顺序存储方式存储,在⼀个⼀维数组中,并满足:K = {k0 , k1 , k2 , ...,kn−1 }, i = 0、1、2... ,则称为小堆(或大堆)。
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。
二、堆的结构
三、性质
(1)堆的性质
1.堆中某个结点的值总是不大于或不小于其父结点的值;
2.堆总是一棵完全二叉树。
(2)二叉树的性质
对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从 0 开始编号,则对于序号为 i 的结点有:
1. 若 i>0 , i 位置结点的双亲序号: (i-1)/2 ;当i=0 , i 为根结点编号,无双亲结点 ;
2. 若 2i+1< n,左孩子序号: 2i+1 ;若 2i+1>=n 则无左孩子;
3. 若 2i+2< n,右孩子序号: 2i+2 ;若2i+2>=n 则无右孩子。
四、堆的实现
(1)头文件——Heap.h
typedef int HPDatatype;
typedef struct Heap
{
HPDatatype* arr;
int capacity;
int size;
}Heap;
//堆的初始化
void HeapInit(Heap* php);
//堆的销毁
void HeapDestroy(Heap* php);
//向上调整算法
void AdjustUp(HPDatatype* arr,int child);
//堆的插入
void HeapPush(Heap* php, HPDatatype x);
//判空
bool HeapEmpty(Heap* php);
//向下调整算法
void AdjustDown(HPDatatype* arr, int n, int parent);
//堆的删除
void HeapPop(Heap* php);
//取堆顶数据
HPDatatype HeapTop(Heap* php);
//求有效数据个数
int HeapSize(Heap* php);
(2)源文件——Heap.c
1.堆的初始化
void HeapInit(Heap* php)
{
assert(php);
php->arr = NULL;
php->capacity = php->size = 0;
}
2.堆的销毁
void HeapDestroy(Heap* php)
{
assert(php);
if(php->arr != NULL)
{
free(php->arr);
}
php->arr = NULL;
php->capacity = php->size = 0;
}
3.向上调整算法
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void AdjustUp(Heap* arr, int child)
{
//父结点
int parent = (child - 1) / 2;
while(child > 0)
{
//如果child<parent,则交换位置
if(arr[child] < arr[parent])
{
swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
4.堆的插入
void HeapPush(Heap* php, HPDatatype x)
{
assert(php);
//判断空间是否充足
if(php->capacity == php->size)
{
int newCapacity = php->capacity == 0 ? 4 : 2*php->capacity;
HPDatatype* tmp = (HPDatatype*)realloc(php->arr, sizeof(HPDatatype)*newCapacity);
if(tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
php->arr = tmp;
php->capacity = newCapacity;
}
php->arr[php->size] = x;
AdjustUp(php->arr, php->size);
php->size++;//这句要放在最后
//因为如果放在倒数第二句,插入之后size实际上应该加1,此时的size对应的数据和初始时不一样了
}
4.判断堆是否为空
bool HeapEmpty(Heap* php)
{
assert(php);
return php->size == 0;
}
5. 向下调整算法
void AdjustDown(HPDatatype* arr, int n, int parent)
{
assert(php);
//左孩子
int child = 2 * parent + 1;
while(child < n)
{
//找左右孩子中最小的
if(child < 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;
}
}
}
6.堆的删除
//堆的删除
void HeapPop(Heap* php)
{
assert(php);
assert(!HeapEmpty(php));
//先交换第一个与最后一个的位置
swap(&php->arr[0], &php->arr[php->size - 1]);
php->size--;
将新的第一个数据进行向下调整
AdjustDown(php->arr, 0, php->size);
}
7.取堆顶数据
HPDatatype HeapTop(Heap* php)
{
assert(php && php->size);
return php->arr[0];
}
8.求有效数据个数
int HeapSize(Heap* php)
{
assert(php);
return php->size;
}
五、两种调整算法
(1)向上调整算法
1.作用
堆的插入:元素插入到堆的末尾后,即为最后一个孩子。
向上调整算法:插入之后如果堆的性质遭到破坏,将新结点顺着双亲结点往上调整到合适位置。
2.复杂度
向上调整算法建堆的时间复杂度为:O(n* log2 n)。
由于堆是完全二叉树,而满二叉树是特殊的完全二叉树,因此我们用满二叉树来简化证明。
分析:
第一层:2^0个结点,需要向上移动0层;
第二层:2^1个结点,需要向上移动1层;
第三层:2^2个结点,需要向上移动2层;
……
第h层:2^(h-1)个结点,需要向上移动h-1层;
因此:需要移动的结点的总的移动步数:每层结点的个数*向上调整的次数。
(2)向下调整算法
1.前提
左右子树必须是一个堆才能调整。
2.作用
删除:是删除堆顶的数据,将堆顶的数据与最后一个数据互换,然后删除最后一个数据,进行向下调整算法。
向下调整算法:将堆顶元素向下调整到满足堆特性为止。
3.复杂度
向下调整算法建堆的时间复杂度为:O(n)。
分析:
第一层:2^0个结点,需要向下移动h-1层;
第二层:2^1个结点,需要向下移动h-2层;
第三层:2^2个结点,需要向下移动h-3层;
……
第h-1层:2^(h-2)个结点,需要向下移动1层;
第h层:2^(h-1)个结点,需要向上移动0层;
因此:需要移动的结点的总的移动步数:每层结点的个数*向下调整的次数。
六、堆的应用
(1)堆排序
1.版本一
基于已有的数组建堆、取堆顶元素完成排序。
void HeapSort(int* arr, int n)
{
Heap hp;
//将已有的数组中的元素插入到堆中(同时进行了排序)
for(int i = 0 ; i < n ; i ++)
{
HeapPush(&hp, arr[i]);
}
int i = 0;
//将堆中的数据更新到数组中
while(!HeapEmpty(&hp))
{
arr[i++] = HeapTop(&hp);
HeapPop(&hp);
}
//销毁
HeapDestroy(&hp);
}
2.版本二
数组建堆,首尾交换,交换后的堆尾数据从堆中删除,将堆顶数据向下调整选出次大的数据。
前提:必须有现成的数据结构堆。
//升序,建大堆
//降序,建小堆
void HeapSort(int* arr, int n)
{
//数组建堆
//从最后一个父结点开始调整
for(int i = (n - 1 - 1) / 2; i >= 0 ; i--)
{
AdjustDown(arr, n, i);
}
int end = n - 1;
while(end > 0)
{
swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
end--;
}
}
3.复杂度
堆排序的时间复杂度为:O(n*logn)。
分析:
第一层:2^0个结点,交换到根结点后,需要向下移动0层;
第二层:2^1个结点,交换到根结点后,需要向下移动1层;
第三层:2^2个结点,交换到根结点后,需要向下移动2层;
……
第h层:2^(h-1)个结点,交换到根结点后,需要向下移动h-1层。
堆排序第⼆个循环中的向下调整与建堆中的向上调整算法时间复杂度计算⼀致。因此,堆排序的时间复杂度为O(n + n ∗ log n) ,即O(n log n)。
(2)TOP-K问题
1.描述
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,⼀般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
2.思路
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 (可能数据都不能⼀下子全部加载到内存中)。
最佳的方式就是用堆来解决:
(1)用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆;前k个最小的元素,则建大堆。
(2)用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
3.实现
void CreateNDate()
{
// 造数据
int n = 100000;
srand(time(0));
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) % 1000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
void topk()
{
printf("请输⼊k:>");
int k = 0;
scanf("%d", &k);
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error");
return;
}
int val = 0;
int* minheap = (int*)malloc(sizeof(int) * k);
if (minheap == NULL)
{
perror("malloc error");
return;
}
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &minheap[i]);
}
// 建k个数据的⼩堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(minheap, k, i);
}
int x = 0;
while (fscanf(fout, "%d", &x) != EOF)
{
// 读取剩余数据,⽐堆顶的值⼤,就替换他进堆
if (x > minheap[0])
{
minheap[0] = x;
AdjustDown(minheap, k, 0);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", minheap[i]);
}
fclose(fout);
}
4.时间复杂度
O(n) = k + (n - k)log2 k。
七、写在最后
二叉树不仅能够顺序实现,而且可以通过链式结构实现。
敬请期待“链式结构实现二叉树”~