目录
谢谢大佬们的光顾能给个赞就更好啦QWQ
0.堆的基本概念
堆实际上是一个完全二叉树,堆又被分为小堆与大堆,小堆就是每个双亲结点的值小于子节点,大堆就是每个双亲结点的值大于子节点。
一.堆的存储结构与逻辑结构
逻辑结构
想要实现堆首先我们要知道它的逻辑结构,就是在我们的大脑中和在我们使用中要将一个堆给看成一个完全二叉树,
存储结构
而它在计算机的存储中实质就是一个数组,那怎么用数组去表示一个二叉树呢,这里就不展开叙述了,如图如果它是一个数组,我们可以看作是这样的
根据在二叉树学习到的知识,我们可以知道,当有父亲结点想要找到左子节点时只需要parent(父节点下标)*2+1,当有子节点想要找到父节点只需要(child(子节点下标)-1)/2
然而上面还只是一个完全二叉树,而一个真正的堆应该是有关系的如上面所说小堆就是每个双亲结点的值小于子节点,大堆就是每个双亲结点的值大于子节点。就拿上面那个图来说(小堆时),我们的堆应该长这样
这就是小堆,关于如何建堆的和怎样将一个数组排序变成小堆后面将会讲到
二.堆的实现
0.堆结构体定义与基本接口声明
typedef int HpDataType;
typedef struct Heap
{
HpDataType* arr;
int size;//size为将要插入的下标 与目前的大小
int capacity;//为容量
}Heap;
void InitHp(Heap* php);
void DestoryHp(Heap* php);
void PushHp(Heap* php, HpDataType x);
void PopHp(Heap* php);
HpDataType TopHp(Heap* php);
bool HpIsEmpty(Heap* php);
void AdjustUp(HpDataType* arr, int child);
void AdjustDown(HpDataType* arr, int parent, int max);
void HeapSort(int* a, int max);
void Swap(int* a, int* b);
1.堆的初始话与销毁,
这里与顺序表是相同的概念不过多介绍
void InitHp(Heap* php)
{
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
void DestoryHp(Heap* php)
{
assert(php);
free(php->arr);
php->arr = NULL;
php->capacity = 0;
php->size = 0;
}
2.堆的插入(入堆)
这里其实我们遇到了一个问题
当我们想要往堆里面插入一个10的时候但是这时就破坏了堆之间的关系,父节点必须大于子节点,
所以这里我们需要用到向上调整算法使得堆的数据之间有关系,
向上调整算法
当需要调整的时候肯定是将当前的这个结点与祖先节点(22,4等)相比较如果10比父节点小就交换位置,如图所示
像这样与22进行了交换与4比较大于4调整结束,详细代码如下
void AdjustUp(HpDataType* arr, int child)
{
assert(arr);
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
这样就实现的小堆的插入问题
void PushHp(Heap* php, HpDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
php->capacity = newCapacity;
php->arr = (HpDataType*)realloc(php->arr, sizeof(HpDataType) * newCapacity);
if (php->arr == NULL)
{
perror("realloc fail!");
return;
}
}
php->arr[php->size++] = x;
AdjustUp(php->arr,php->size-1);//第二个是传入的是最后一个元素的下标
}
3.堆的删除(出堆)
堆的删除是删除数组的第一个元素,但是如果我们之间删除那么堆的关系不久乱了吗?
可以看见当我们如果之间删除1的话是有可能之间乱套的,所以聪明的程序员又想到了一个方法,
先将第一个要删除的数据与最后一个数据进行交换,
在进行向下调整
向下调整算法
那么这个向下调整算法又是怎么是实现的呢
我们肯定是从根节点开始与子孙节点比较,来开始调整,父节点比儿子节点小就进行交换
void AdjustDown(HpDataType* arr, int parent, int maxIdex)//max最后一个元素的下标
{
int child = parent * 2 + 1;
while (child < maxIdex)//
{
//先找到小的那个数据
if (child+1<maxIdex&&arr[child] > arr[child + 1])
{
child++;
}
if (arr[parent]>arr[child])
{
Swap(&arr[parent], &arr[child]);
parent = child;
child = child * 2 + 1;
}
else
{
break;
}
}
}
然后在这里需要注意很多细节,在代码的第7行的表达式的含义是什么,首先在进行寻找子节点小的元素的时候,如果只有一个子节点是否会发生数组越界?,后面向下调整也是先找到小的那个数据再进行比较交换
这样我们就实现了堆的删除
void PopHp(Heap* php)
{
assert(php);
assert(php->size > 0);
php->size--;
Swap(&php->arr[php->size], &php->arr[0]);//进行交换
AdjustDown(php->arr, 0, php->size);//第二个参数为传入要调整的堆的根节点第三个为堆的最大数(最后一个下标)
}
三.堆排序的实现
我们实现这个堆到底是为了什么呢,其实其中之一就是堆排序的应用,提高排序的效率,
当我们想要对数据进行排序使用堆排序的时间复杂度为,排序效率将会比较优秀
1.建堆的选择
当我们想要进行排序的时候首先要模拟堆的插入过程进行建堆,这里有个问题如果想要降序排列那么应该建小堆还是大堆呢,在我刚开学学习的时候想的是肯定是建大堆啊因为这样根节点就是最大的然后再出堆,其实这样想就错了,这样只是出堆降序,不是数组本身进行了降序排列,所以降序是其实是建小堆,升序其实是建大堆.那又是怎么实现的呢
2.堆排序的实现
在进行建堆之后(小堆降序来举例),我们就只需要模拟出堆,这时每次最小的数就被放在了最后的位置,最后完成排序后,整个数据就有序下面是完整的代码
void HeapSort(int* a, int max)//max为最大个数
{
//建队
for (int i = 0; i <max; i++)
{
AdjustUp(a, i);//模拟插入建小堆
}
//排序
int end = max - 1;//找到最后一个元素的下标进行交换
while (end>0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, 0, end);
--end;//最后的有序不在排序
}
}
其实第一步建堆算法还没有做到极致,还可以倒着向下调整
//建堆法2
for (int i = (max - 1 - 1) / 2; i >= 0; i--)//找到第一个非叶子结点 倒着进行想下调整
{
AdjustDown(a, i, max-1);
}
先找到第一个非叶子节点,将这个位置建堆,知道最后整个堆变成大堆,提升了效率为O(N)
四.堆排序的练习题
想要巩固所学知识关看肯定是没有多少用的,练习才是王道
下面的题我必须去做(用堆排序解决)
1561. 你可以获得的最大硬币数目 - 力扣(LeetCode)
由于个人精力有限,如有错误欢迎批评指正!
小bit!!!!