本篇博客由 CSDN@先搞面包再谈爱 原创,转载请标注清楚,请勿抄袭。
堆是啥
其实堆也算是完全二叉树(不知道完全二叉树的先了解一下完全二叉树是啥再来看这个),只不过堆专门是用顺序表的形式来存储的。
堆的分类
大根堆:父比儿子大,就是大根堆。
小根堆:父比儿子小,就是小根堆。
来个图:
芝士小根堆:
芝士大根堆:
根据上面这两句话和这张图就可以得出:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
上面基本上涵盖了所有堆的概念了,我们下来讲讲怎么实现。
堆的实现(以大根堆为例)
首先,堆使用顺序表存储的,也就是数组,那么怎么初始化一个堆呢?
我们先来给一组数据:
a[6] = { 27, 39, 18, 5, 9, 32};
怎么用这个数组建堆?
看图解:
初始状态下是这样的:
建堆有两种方法:向上调整和向下调整
向上调整
我们要从数组中第0个元素开始建堆。我们每次选第i个数(i从0开始),将这个数与先前建好的堆进行构建新堆,不断往堆顶挪,直到到达堆顶或者是遇到了比自己大的数就停止,这样调整堆的方法就叫做向上调整。
我可能解释的不是很到位,但是我已经很尽力了,下面就看一下图解:
代码实现:
//向上调整
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//确定范围
while (child > 0)
{
//儿子比爹大就交换
if (a[child] > a[parent])
swap(&a[child], &a[parent]);
//调整儿子与爹的位置
child = parent;
parent = (child - 1) / 2;
}
}
向下调整
从倒数第一个非叶子节点开始,将该数与其下方的数进行对比,若出现比两个子树元素大的情况,就将子树中最大的挪到父结点处直至越界停止,然后再判断非叶子节点的前一个结点不断重复直至到堆顶,这样的调整就叫做向下调整。
看图解:
代码实现:
//向下调整
void AdjustDown(HPDataType* a, int parent, int n)
{ //这里parent是下标
//直接先选择左子树
int child = parent * 2 + 1;
//确定范围
while (child < n)
{
//找出左右子树中最大的那个
//先判断右越界了没有
if (child + 1 < n && a[child + 1] > a[child])
child++;//如果右大就选右边的
if (a[child] > a[parent])
swap(&a[child], &a[parent]);
//调整儿子与爹的位置
parent = child;
child = parent * 2 + 1;
}
}
建堆
建堆的时候,是要将堆中的数组进行调整,一步一步调整成大堆/小堆的。
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
//上面的这些就是把数组中的元素放到堆里,没什么用,下面的两个调整堆才有用。
/*hp->_a = (HPDataType*)malloc(sizeof(HPDataType) * n);
if (hp->_a == NULL)
{
printf("malloc fail\n");
exit(-1);
}
for (int i = 0; i < n; i++)
{
hp->_a[i] = a[i];
}
hp->_size = n;
hp->_capacity = n;*/
//向上调整建堆
for (int i = 1; i < hp->_size; i++)
{
AdjustUp(hp->_a, i);
}
//向下调整建堆
//这里i的初始值就是第一个非叶子节点的下标。
for (int i = (hp->_size - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(hp->_a, i, hp->_size);
}
}
用向上调整建堆和向下调整建堆的时间复杂度
向下调整建堆的时间复杂度
我们可以看到,向下调整建堆时,最后一层的结点不需要向下移动,而满二叉树中最后一层的结点占了总结点个数的一半+1;但是向上调整的时候最后一层的结点还需要移动,只有堆顶的数不需要移动。所以向下调整是要比向上调整更简单一些的。
向上调整的时间复杂度计算方法类似,我这里就直接给结果了:O(N*logN)
所以总的来说,建堆还是推荐使用向下调整建堆。
堆的插入
插入数的时候是在数组的末尾插的,所以我们只需要将该节点与其祖先结点惊醒调整就可以了,也就是说只需要调一次。
比如说我现在在末尾插入一个57,那么看图:
代码实现:
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
//上面这些是判断当前堆中的数组是否满了
/*if (hp->_capacity == hp->_size)
{
int NewCapacity = hp->_capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(hp->_a, NewCapacity * sizeof(HPDataType));
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
hp->_a = tmp;
hp->_capacity = NewCapacity;
}*/
hp->_a[hp->_size] = x;
hp->_size++;
//插入是在数组的尾插入的,所以要用到向上调整
AdjustUp(hp->_a, hp->_size - 1);
}
堆的删除
删除删的是堆顶元素,我们需要先将堆顶元素与数组中的最后一个元素交换,然后再从堆顶开始调整堆。
看图解:
上面这些基本上就是堆实现的方法了,最主要掌握的就是向上调整和向下调整。
给几道题奖励一下:
- 下列关键字序列为堆的是:()
A 100,60,70,50,32,65
B 60,70,65,50,32,100
C 65,100,70,32,50,60
D 70,65,100,32,50,60
E 32,50,100,70,65,60
F 50,100,70,65,60,32 - 已知小根堆为8,15,10,21,34,16,12,删除关键字 8 之后需重建堆,在此过程中,关键字之间的比较次
数是()。
A 1
B 2
C 3
D 4 - 一组记录排序码为(5 11 7 2 3 17),则利用堆排序方法建立的初始堆为
A(11 5 7 2 3 17)
B(11 5 7 2 17 3)
C(17 11 7 2 3 5)
D(17 11 7 5 3 2)
E(17 7 11 3 5 2)
F(17 7 11 3 2 5) - 最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()
A[3,2,5,7,4,6,8]
B[2,3,5,7,4,6,8]
C[2,3,4,5,7,8,6]
D[2,3,4,5,6,7,8]
答案:1.A 2.C 3.C 4.C
堆排序
大致思路
首先堆排序,得先建堆,建大堆还是建小堆是由排升序还是排降序来决定的。
排升序就建大堆,排降序就建小堆。(记住)
如果你想对一个数组排序,那么数组本身就是一个堆,所以我们就可以直接对数组进行调整,而不是再创建一个堆来进行堆排序。
怎么调整呢,和调整堆中的数组一样的方法,向上调整或者向下调整,这两个方法都可以建堆。上面也讲到了,推荐使用向下调整建堆。所以我下面就使用向下调整来建堆了。
代码实现:
//建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, i, n);
}
建完堆之后就要开始堆排序了。
这是一个循环,每次都要将头(0)尾(n-1)进行交换,将尾–,然后再从头开始向下调整,直到当尾变成1就停止。
看图:
代码实现:
void HeapSort(int* a, int n)
{
//建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, i, n);
}
//首尾交换,向下调整
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]);
end--;
AdjustDown(a, 0, end + 1);
}
}
TopK问题
// TopK问题:找出N个数里面最大/最小的前K个问题。
比如:陕西排名前10的羊肉泡馍,郑州排名前五的烩面,王者荣耀全国排名前10的李白。等等问题都是Topk问题,
首先还是建堆
如果要的是N个数中最大的前K个数,那就建小堆。
如果要的是N个数中最小的前K个数,那就建大堆。
大致思路
以大的前K个数为例:
先搞一个大小为K的数组,然后把N个数中的前K个数放到数组中,然后把大小为K的数组建堆成小堆,然后将后N - K个数与堆顶进行比较,如果比堆顶数小,就比较下一个,如果比堆顶数大,就替换掉堆顶(这样能保证前K个数能进入堆内)。不断遍历,直到N个数遍历完毕。
如果你建的是大堆的话,当我们没有遍历完N个数时,最大的数可能已经占到堆顶位置了,所以无论再怎么遍历,都无法将前K大的数入队
代码实现
void PrintTopK(int* a, int n, int k)
{
int* Top = (int*)malloc(k * sizeof(int));
if (Top == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//搞出存放前K个数的数组
for (int i = 0; i < k; i++)
{
Top[i] = a[i];
}
//建堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(Top, i, k);
}
for (int i = k; i < n; i++)
{
if (a[i] > Top[0])
Top[0] = a[i];
AdjustDown(Top, 0, k);
}
for (int i = 0; i < k; i++)
{
printf("%d ", Top[i]);
}
}
到此结束。