前言
- 🚄 输入是学习的本质,输出是学习的手段。
- 🔖 分享每一次的学习,期待你我都有收获。
- 🎇 欢迎🔎关注👍点赞⭐️收藏✉评论,共同进步!
- 🌊 “要足够优秀,才能接住上天给的惊喜和机会”
- 💬 博主水平有限,如若有误,请多指正,万分感谢!
☁️堆的概念及结构
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储
在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为
小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
堆可以按其存储数据的特点可以分为两种:
- 大根堆,即对于任意一个结点而言,它的父亲总比孩子大。
- 小根堆,即对于任意一个结点而言,它的父亲总比孩子小
堆总是一棵完全二叉树。
☁️向下调整算法——调整堆
有这样一组数组 int a[] = {27,15,19,18,28,34,65,49,25,37};
我们先将它以完全二叉树形式表现出来
不难发现,由于 27 比它的左右子树都大,但 15 并没有比它的左右子树都大,因此这并不是一个堆的结构。而我们要做的就是将它调整为一个堆。
由图可观察到,虽然整体上这还不是一个堆结构,但是单独看它的左右子树已经是两个小根堆了,因此,我们只要将堆顶数据进行适当向下调整,即可得到一个堆结构。
堆的向下调整算法,使用的条件为:完全二叉树的左右子树均为堆结构,若条件不符,则不可使用!!
本例中,左右子树均为小根堆,因此考虑将整个二叉树调整为小根堆。
具体操作为:
- 从父亲结点开始,选择左右子树中较小的孩子
(下面简称较小的孩子为孩子)。 - 若孩子的值 比 父亲的值小。将父亲的值与孩子的值交换
- 重复以上过程,直到左右孩子不存在,或父亲的值比左右孩子都小。
图示如下:
代码实现:
#include<stdio.h>
void Swap(int* p1, int* p2) //交换两数
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//其中,形参 a为数组,sz为数组元素个数,parent为根结点的下标。
void AdjustDown(int* a, int sz, int parent)
{
//我们要选择左右孩子中较小的孩子与父亲交换
//先默认左孩子比右孩子小
int child = parent * 2 + 1; //左孩子的下标为父亲下标*2+1。自己验证即可,这里不做验证
while (child < sz)//若孩子存在,则必在数组下标范围内
{
if (child + 1 < sz && a[child + 1] < a[child]) //存在右孩子,且右孩子比左孩子小
{
++child;//右孩子下标为左孩子+1
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);//交换两数
parent = child;//继续向下比较
child = parent * 2 + 1;
}
else //否则父亲比较小的孩子还小,已经满足小根堆
{
break;
}
}
}
int main()
{
int a[] = { 27,15,19,18,28,34,65,49,25,37 };
int sz = sizeof(a) / sizeof(a[0]);//数组元素个数(堆元素个数)
AdjustDown(a, sz, 0);//向下调整
for (int i = 0; i < sz; i++) //打印调整后的数组
{
printf("%d ", a[i]);
}
return 0;
}
☁️图文理解——建立堆
介绍完向下调整算法,一些铁子们不免会存在疑惑:
向下调整算法只能运用在二叉树的左右子树已经是堆结构的前提。
那如果给我们的是一个完全无规律的数组呢?
int a [ ] = { 15,19,25,11,22,65,28,18,37 };
如果我们将每个堆细分为每个结点,即一个结点就是一个堆,则每个堆都是成立的
注意:这里的数字表示的是一个个结点,并不是结点的值。这一个个结点就可以看做是一个个堆。
既然向下调整的要求是二叉树的左右子树已经是堆,我们就将问题细分:
要用向下调整法使二叉树成为堆——>二叉树的左右子树必须是堆,
即①、②是堆
-
树①要是堆,那①的左右子树③、④必须是堆。
-
树②要是堆,那②的左右子树必须是堆,②的左右子树都是单个结点,刚才说了,单个结点可以看做已经有序,是堆。
-
树③要是堆,那③的左右子树必须是堆,③的左右子树都是单个结点,刚才说了,单个结点可以看做已经有序,是堆。
因此要运用向下调整算法,只要不断对 左右子树 的 左右子树…
使用向下调整,最后就能达到整个二叉树的左右子树为堆的效果。
这也是分治的思想
实际上,堆总是完全二叉树这一性质可以得知,
虽然逻辑上我们可以理解为从单个结点开始逐渐扩大树的范围,将堆一一调整,
但是实际上我们调整算法的最小单位是一个最小且是在最后一个位置的二叉树。即要找最后一个非叶子结点
因为单个结点可看作堆,是既定的事实,考虑它并没有意义。
最后一个孩子的父亲,就是最后一个非叶子结点,
因此我们只要找到最后一个孩子,就能找到最后一个二叉树了,再由此逐渐扩大树的范围。
int a [ ] = { 15,19,25,11,22,65,28,18,37 };
最后一个孩子所在位置,就是数组中最后一个元素的下标。child=sz-1;
父亲结点的下标
parent = (child-1) / 2;
依次类推,倒数第二个堆的父亲结点就是倒数第一个堆的父亲结点的前一个结点。
如图:
图中数字表示倒数第n个堆的父亲结点,显然他们一个挨着一个。
由这样的思路,即使是完全无规律的数组,我们也能建立出大根堆或者小根堆,这里举一个建大根堆的例子。
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustUp(int* a, int sz, int parent)
{
//先默认左孩子比右孩子大
int child = parent * 2 + 1; //左孩子
while (child < sz)//孩子存在,则必有孩子下标小于数组长度
{
if (child + 1 < sz && a[child + 1] 》 a[child])//右孩子大于左孩子
{
++child;
}
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);//交换父亲和孩子的数值
parent = child;//新的父亲结点
child = parent * 2 + 1;//新的孩子结点
}
else //父亲比较小的孩子还小,就不用调整了,已经是堆了。
{
break;
}
}
}
int main()
{
int a[] = { 15,19,25,11,22,65,28,18,37 };
int sz = sizeof(a) / sizeof(a[0]);
//建立大根堆
int parent = (sz - 1 - 1) / 2;//从最后一个堆开始调整,要调整最后一个堆,就要找到最后一个堆的父亲
for (parent; parent >= 0; --parent)//--parent意为调整完一个堆后,再找前一个堆的父亲调整。
{
AdjustUp(a, sz, parent);//调整堆
}
for (int i = 0; i < sz; i++)//打印数组
{
printf("%d ", a[i]);
}
return 0;
}
☁️图文理解——堆排序
排序分为:
- 升序
- 降序
1.升序排序要建大根堆,
为什么不建小根堆?
这是一个小根堆,我们每次取最小——堆顶,放入数组,那么第一次的时候我们把15取下,放在a[0],
15不再参与排序,因此将15从堆中删除。
这时候问题就来了,堆顶没元素了,那我们是要把18移上去还是把19移上去?
其实无论是把18往上移还是把19往上移,整个二叉树的父子、兄弟关系都将变得混乱,需要我们重新建堆,
而建堆的时间复杂度为O(N),光是建堆就花了O(N*N)的时间复杂度,这样还不如直接排序,堆排序就没有价值了。
还是刚才那个数组,如果建大根堆
既然我们是要升序排序,那不妨我们将 65 与 28 交换位置,
最大的数在最后一个位置,符合升序排序。再将 65 从 堆中移除——不参与接下里对堆的一系列操作
我们发现,除了根以外,剩余的数据的位置都是保持不变的,左右子树仍然是大根堆,他们的关系并没有被打乱。因此我们不需要重新建堆,只需要对根进行向下调整。
再将堆顶与最后一个数据的位置进行交换—— 49 与 15位置交换,然后依然是将 最后一个数据移出堆,再进行堆的调整。
若有N个节点,则我们建堆需要时间复杂度O(N),每取出一个最大值,需要调整一次堆O(logN),则N个节点调整堆的时间复杂度为O(N*logN)
因此堆排序整体的时间复杂度为O(N+NlogN)= O(NlogN)
☁️堆的实现
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef struct Heap Heap;
typedef int HpDataType;
struct Heap
{
HpDataType* arr;
int capacity;//容量
int sz;//数据个数
};
//初始化堆
void HeapInit(Heap* ph, HpDataType* arr, int size);
//检查增容
void CheckMalloc(Heap* ph);
//判断堆是否为空
bool HeapEmpty(Heap* ph);
//增加数据,并调整堆
void HeapPush(Heap* ph, HpDataType x);
//删除堆顶,并调整堆
void HeapPop(Heap* ph);
//堆中元素个数
int HeapSize(Heap* ph);
//提取堆顶元素
HpDataType HeapTop(Heap* ph);
//销毁堆
void HeapDestory(Heap* ph);
//打印
void HeapPrint(Heap* ph);
//交换两数
void Swap(int* a, int* b);
//向上调整
void AdjustUp(HpDataType* arr, int child);
//向下调整
void AdjustDown(HpDataType* arr, int sz, int parent);
//交换两数
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//向上调整
void AdjustUp(HpDataType* arr, int child)
{
//先找到它的父亲
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[parent] < arr[child])
{
Swap(&arr[parent], &arr[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void AdjustDown(HpDataType* arr, int sz, int parent)
{
//先默认左孩子比右孩子大
int parent = parent;
int child = parent * 2 + 1;
while (child < sz)
{
if (child + 1 < sz && arr[child + 1] > arr[child])
{
++child;
}
if (arr[parent] < arr[child])
{
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//初始化堆
void HeapInit(Heap* ph, HpDataType* arr, int size)
{
ph->arr = (HpDataType*)malloc(sizeof(HpDataType) * size);
memcpy(ph->arr, arr, sizeof(HpDataType)*size);
ph->capacity = size;
ph->sz = size;
//建堆
//从最后一个非叶子结点,即最后一个父亲开始
int end = (ph->sz - 1 - 1) / 2;
for (end; end >= 0; --end)
{
AdjustDown(ph->arr, ph->sz, end);
}
}
//检查增容
void CheckMalloc(Heap* ph)
{
assert(ph);
if (ph->sz == ph->capacity)
{
HpDataType* newarr = (HpDataType*)realloc(ph->arr, sizeof(HpDataType) * ph->capacity * 2);
if (newarr == NULL)
{
perror("malloc:");
exit(-1);
}
else
{
ph->arr = newarr;
ph->capacity *= 2;
}
}
}
//判断堆是否为空
bool HeapEmpty(Heap* ph)
{
assert(ph);
return ph->sz == 0;
}
//增加数据,并调整堆
void HeapPush(Heap* ph,HpDataType x)
{
assert(ph);
CheckMalloc(ph);
ph->arr[ph->sz-1] = x;
ph->sz++;
//加入数据后要调整堆
//只需沿新增节点的路径进行调整
int newchild = ph->sz - 1;
AdjustUp(ph->arr, newchild);
}
//删除堆顶,并调整堆
void HeapPop(Heap* ph)
{
assert(ph);
assert(!HeapEmpty(ph));
//将堆顶挪至最后一个位置,这样删除时不会影响整个堆的关系结构
//删除完后再进行向下调整即可
Swap(&ph->arr[0], &ph->arr[ph->sz - 1]);
ph->sz--;
AdjustDown(ph->arr, ph->sz, 0);
}
//堆中元素个数
int HeapSize(Heap* ph)
{
assert(ph);
return ph->sz;
}
//提取堆顶元素
HpDataType HeapTop(Heap* ph)
{
assert(ph);
return ph->arr[0];
}
//销毁堆
void HeapDestory(Heap* ph)
{
free(ph->arr);
ph->arr = NULL;
ph->capacity = ph->sz = 0;
}
//打印
void HeapPrint(Heap* ph)
{
for (int i = 0; i < ph->sz; i++)
{
printf("%d ", ph->arr[i]);
}
printf("\n");
}
☁️堆排序算法
分为四步:
1.建立堆(大根堆 / 小根堆)。
2.将堆顶数据与最后一个数据交换位置。
3.调整堆。
4.重复以上过程,直至堆中只剩最后一个数据,不必再排。
这里举例升序排序,降序排序只需将向下调整算法改为调整小根堆即可。
#include<stdio.h>
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustDown(int* a, int sz, int parent)//大根堆
{
//先默认左孩子比右孩子大
int child = parent * 2 + 1; //左孩子
while (child < sz)//孩子存在,则必有孩子下标小于数组长度
{
if (child + 1 < sz && a[child + 1] > a[child])//右孩子大于左孩子
{
++child;
}
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);//交换父亲和孩子的数值
parent = child;//新的父亲结点
child = parent * 2 + 1;//新的孩子结点
}
else //父亲比较小的孩子还小,就不用调整了,已经是堆了。
{
break;
}
}
}
int main()
{
int a[] = { 15,19,25,11,22,65,28,18,37 };
int sz = sizeof(a) / sizeof(a[0]);
//升序排列
//为什么是建大根堆?
int i = 0;
for (i = (sz - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, sz, i);
}
//每次取一个最大值放到数组后面(第一个数的位置与最后一个数的位置交换),
//并且将该值移出堆(不再参与堆的操作),重新调整堆
int end = sz - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);//交换位置
AdjustDown(a, end, 0);//end为sz-1,因此已经将最后一个数无视了。
end--;
}
int num = 2;
int t = 3;
for (int i = 0; i < sz; i++)//遍历打印
{
int k = 0;
k++;
printf("%d ", a[i]);
if (i == 0)
{
printf("\n");
}
else if (k % num == 0)
{
printf("\n");
num =pow(2,t);
t++;
k = 0;
}
}
return 0;
}