文章目录
- 前言
- 堆(Heap)
- 1、堆的概念及结构
- 2、堆的分类
- 2.1、小堆的结构
- 2.2、大堆的结构
- 2.3、找到规律并证明
- 3、堆的实现(小堆)
- 3.1、堆的结构以及接口
- 3.2、初始化、销毁
- 3.3、交换父子结点(后续需要)
- 3.4、插入
- 3.5、删除
- 3.6、堆顶
- 3.7、获取堆的有效元素个数
- 3.8、判空
- 总代码
- Heap.h源文件
- Heap.c源文件
- Test.c源文件(头文件)
- 堆排序(HeapSort)——升序
前言
堆(Heap)
1、堆的概念及结构
果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: <= 且 <= ( >= 且 >= ) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
2、堆的分类
大顶堆:每个节点的值都大于或者等于它的左右子节点的值。
小顶堆:每个节点的值都小于或者等于它的左右子节点的值。
2.1、小堆的结构
由下图我们可以看出,这是个小堆,因为每一个父亲结点都比其子结点小,例如:父亲12小于子结点17和58,父亲结点17又小于它的子结点27和32,58也小于72,所以它是个小堆。
而树形图右侧则是它在数组里面的存储结构。
2.2、大堆的结构
理解了小堆,大堆同理,只不过和小堆相反,由下图所示 此时的每一个父亲结点都大于其子结点,
2.3、找到规律并证明
既然堆的逻辑结构是完全二叉树,那么它就同样具有完全二叉树的性质。
对于完全二叉树,若从上至下、从左至右编号,以根节点为0开始,则编号为i的节点,其左孩子结点编号为2i+1,右孩子节点编号为2i+2,父亲节点为 (i-1) / 2。
我们以小堆为例证明一下!
3、堆的实现(小堆)
3.1、堆的结构以及接口
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
#include<time.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
//初始化
void HeapInit(HP* php);
//销毁
void HeapDestory(HP* php);
//交换父子结点
void Swap(HPDataType* p1, HPDataType* p2);
//插入
void HeapPush(HP*php,HPDataType x);
//删除
void HeapPop(HP* php);
//堆顶
HPDataType HeapTop(HP* php);
//有效元素个数
size_t HeapSize(HP* php);
//判空
bool HeapEmpty(HP* php);
3.2、初始化、销毁
初始化和销毁我相信各位大佬都没问题,这个我就不再解释了哈
//初始化
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
//销毁
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
3.3、交换父子结点(后续需要)
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
3.4、插入
插入操作就是先将元素插到堆的末尾,即最后一个孩子的后面,插入之后如果堆的性质发生破坏,将新插入结点顺着其双亲网上调整到合适位置即可。
如下图所示:10就是我们新插入的元素,插入时它在最后一个元素的后面,这时候10为孩子,我们可以根据孩子的下标通过公式parent = (child - 1) / 2可以找到父亲的下标,父亲元素为28,已知我们要建小堆,即孩子>父亲,而现在堆的性质已经受到破坏了,所以我们就要进行“向上调整”。过程如下图:
代码:
//向上调整
void AdjustUp(HPDataType* a, int child)
{
//根据孩子找到父亲结点
int parent = (child - 1) / 2;
//while (parent >= 0)
while (child > 0)
{
//因为是小堆,所以父亲比孩子大就交换
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
//child = (child - 1) / 2;
//parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
// O(logN)
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
3.5、删除
这里可不敢和顺序表链表搞杂了,堆的删除就是删除堆顶元素
步骤:
- 首先把第一个数据和最后一个数据进行交换
- 减减size
- 向下调整
过程如下图所示:
代码:
//AdjustDown就是向下调整
void AdjustDown(int* a, int size, int parent)
{
//根据父亲找到孩子(左孩子)
int child = parent * 2 + 1;
while (child < size)
{
// 假设左孩子小,如果解设错了,更新一下
if (child+1 < size && a[child + 1] < a[child])
{
++child;
}
//如果父亲大于孩子就交换
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
3.6、堆顶
这个实现起来就很简单了,直接返回堆顶就好啦,记得断言一下哦~
代码:
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
3.7、获取堆的有效元素个数
也是一样简单,直接返回size就可以啦,也没什么讲的,断言~
直接上代码:
size_t HeapSize(HP* php)
{
assert(php);
return php->size;
}
3.8、判空
这个就和上一节的栈的判空一模一样啦,断言然后直接若size为0就返回
直接上代码:
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
总代码
Heap.h源文件
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
#include<time.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HeapInit(HP* php);
void HeapDestory(HP* php);
void Swap(HPDataType* p1, HPDataType* p2);
void HeapPush(HP*php,HPDataType x);
void HeapPop(HP* php);
HPDataType HeapTop(HP* php);
size_t HeapSize(HP* php);
bool HeapEmpty(HP* php);
Heap.c源文件
#include"Heap.h"
// 小堆
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0)
while (child > 0)
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
//child = (child - 1) / 2;
//parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
// O(logN)
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
void AdjustDown(int* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
// 假设左孩子小,如果解设错了,更新一下
if (child+1 < size && a[child + 1] < a[child])
{
++child;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
size_t HeapSize(HP* php)
{
assert(php);
return php->size;
}
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
Test.c源文件(头文件)
#include"Heap.h"
int main()
{ int a[] = {4,6,2,1,5,8,2,9};
int n=sizeof(a) / sizeof(int);
HP hp;
HeapInit(&hp);
for (int i = 0; i < n;i++)
{
HeapPush(&hp, a[i]);
}
//堆顶前k个元素
// int k = 3;
// while (k--)
// {
// printf("%d\n", HeapTop(&hp));
// HeapPop(&hp);
// }
// }
while (!HeapEmpty(&hp))
{
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
printf("\n");
return 0;
}
堆排序(HeapSort)——升序
步骤:
- 先将待排序的数组构成大堆,这个时候堆顶的元素就是整个堆里最大的元素
- 然后将堆顶元素和末尾元素进行交换,现在最大元素就是末尾那个结点了,调整排序的时候不把它算进去,此时待排序元素个数为n-1。
- 把其他的待排元素构成大堆,再回到第一步,交换首尾元素,然后待排元素个数再-1…如此循环
注意:
- 升序建大堆
- 降序建小堆
步骤一:向下调整算法建大堆
思路图:
步骤二:排序
思路图:
结论:
已知建堆的时间复杂度为O(N),而在排序过程中,由于要每次选出剩余数中最小的数,并保存到每次最后的节点,并要再执行一次向下调整算法,总共需要进行N-1(≈N)次,则向下调整算法的时间复杂度:O(NlogN),再加上建堆的时间复杂度,则=N*logN+N,综上,堆排序的时间复杂度:O(NlogN)
代码:
//升序
void HeapSort(int* a, int n)
{
//建大堆
// for(int i=1; i<n; i++)
// {
// AdjustUp(a,i);
// }
for (int i = (n-1-1)/2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
//n是数组最后一个元素下标的下一个
int end=n-1;
while(end>0)
{
Swap(&a[0],&a[end]);
//需要注意的是 每次交换完之后关系就全乱了 所以需要再次向下调整
AdjustDown(a,end,0);
--end;
}
}
int main()
{
int a[] = { 4, 6, 2, 1, 5, 8, 2, 9 };
HeapSort(a, sizeof(a)/sizeof(int));
for (int i = 0; i < sizeof(a)/sizeof(int); i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
测试结果: