一、堆的概念及特性
相对来说,堆的概念虽然也源于生活,但是又高于生活。因为他保留了现实中的一些特征,又新增了一些新的特征。
(1) 现实中的堆
![](https://img2022.cnblogs.com/blog/1689184/202204/1689184-20220412145508619-439344482.png)
现实中,我们熟悉的堆一般如上图所示,比如木堆。
一般具有这个规律:每层的元素数量比上一层多一个,如果用公式表示则为 f(n) = f(n-1) + 1,初值 f(1) = 1, n表示自顶向下的层数。
其实很容易就得出 f(n) = n 这个公式了,这不是重点,重点是理解现实中的堆。
(2)数据结构中的堆
![](https://img2022.cnblogs.com/blog/1689184/202204/1689184-20220412150703010-1048380195.png)
对比现实中的堆,虽然每层的元素数量相较于上一层都增加了,但是不再是增加固定的数值1了。
观察其规律:每层的元素数量都为上一层的两倍,用公式表示则为 f(n) = f(n-1) * 2, 初值f(1) = 1, n表示自顶向下的层数。
同样可以得出 f(n) = 2^(n-1), 也就是数据结构中的堆是这样一个结构。
我们可以换一种形式理解,这样更加直观,如下:
![](https://img2022.cnblogs.com/blog/1689184/202204/1689184-20220412152211253-1178123682.png)
这样看起来是否更加清晰了呢?
其实从上述的性质也知道,每层的元素数量都为上层的2倍,这其实就是二叉树的节点的性质,所以:
堆本身其实就是一种二叉树的结构,而且是完全二叉树。
二、堆的实现
就如同二叉树一样,无序的树结构、堆结构是没啥实用性的,咱们用到的一般都是有序的树和堆。其中有序的树往往用于二叉查找树,而有序的堆则是指的大根堆、小根堆(这两个概念在其他高级语言中,往往叫优先队列 priority_queue)。
有了前面实现队列和栈的思路,堆的总体思路这里就不再赘述了,具体实现可以看后续代码,这里只谈其中的难点。
那么怎么实现堆的有序性呢?
如上述所知,堆实际上是一种完全二叉树,那么在二叉树中如何实现有序性的呢?这点我们可以借鉴二叉搜索树,二叉搜索树中,有序性是由每个节点的特性来保证的,对于每个节点来说, 左子节点 > 根节点 > 右子节点, 满足该特性,就保证了整个二叉树的数据有序性。
那么在堆中,就可以借鉴这种思想,由每个节点特性来保证整个堆的数据有序性。
由于堆本身的需求和二叉搜索树不同,我们只需要保证每次从堆中取数据,取的值都是堆中最大(或者最小),那么就满足我们的需要了。大根堆(小根堆)的概念就是由此而来。
(1)上述过程中,我们说到了根节点、左子节点、右子节点的关系,那么他们之间是如何联系起来的?也就是说,如何通过根节点,确定左子节点和右子节点呢?
就如同队列和栈一样,堆的实现也是使用的数组进行实现。观察上面的图,其实我们就能发现,根节点和左子节点、右子节点的下标之间是有联系的。如下:
left = root * 2
right = root * 2 + 1
left 为左子节点下标, right为右子节点下标,root为根节点下标。
注意:堆中元素位置从1开始,数组首位一般都是填充位。
(2)往堆中新增元素时,如何保证堆的有序性? (这里用大根堆进行说明)
这个问题其实就是为新元素寻找合适的位置。由于在插入新元素之前,这个堆本身是有序的(也就是对每个节点来说,都有根节点 > 左右子节点)。
那么,我们就可以在堆末尾插入新的元素,这样只需要比较新插入的元素和他的根节点,然后交换位置,直到找到合适的位置,这样就完成了插入新元素的操作。(这个操作,我们称之为自底向上堆化)
如下:
void AddToHeap(Heap *obj, int val)
{
obj->arr[++obj->num] = val; // 先在末尾插入
// 然后调整顺序
int cur = obj->num;
while(cur / 2 > 0 && obj->arr[cur / 2] < obj->arr[cur])
{
Swap(&obj->arr[cur / 2], &obj->arr[cur]);
cur = cur / 2;
}
}
(3)从堆中取出元素时,如何保证堆的有序性? (同样用大根堆进行说明)
由于直接取出堆顶元素,然后再对剩余元素进行堆化操作复杂度过高,一般不采用这种方式。我们可以交换堆顶元素和堆底元素,然后再对堆顶元素进行堆化操作。(这个操作,我们称之为自顶向下堆化)
如下:
void DeleteFromHeap(Heap *obj)
{
// 先交换首尾元素
Swap(&obj->arr[1], &obj->arr[obj->num]);
// 然后调整堆内顺序
obj->num--;
int index = 1;
while(true)
{
int maxIndex = index;
if(index * 2 <= obj->num && obj->arr[maxIndex] < obj->arr[index * 2])
{
maxIndex = index * 2;
}
if(index * 2 + 1 <= obj->num && obj->arr[maxIndex] < obj->arr[index * 2 + 1])
{
maxIndex = index * 2 + 1;
}
if(maxIndex == index)
{
break;
}
Swap(&obj->arr[index], &obj->arr[maxIndex]);
index = maxIndex;
}
}
三、代码实现
typedef struct
{
int *arr; // 实现堆的数组,申请的大小为size + 1
int size; // 堆的size,也是最大的下标值
int num; // 堆中的元素个数
}Heap; // 大根堆
Heap *CreatHeap(int size)
{
Heap *newHeap = malloc(sizeof(Heap));
if(newHeap == NULL)
{
return NULL;
}
newHeap->arr = malloc(sizeof(int) * (size + 1));
if(newHeap->arr == NULL)
{
free(newHeap);
return NULL;
}
newHeap->size = size;
newHeap->num = 0;
return newHeap;
}
void FreeHeap(Heap *obj)
{
free(obj->arr);
free(obj);
}
int GetHeapTop(Heap *obj)
{
return obj->arr[1];
}
void Swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void AddToHeap(Heap *obj, int val)
{
obj->arr[++obj->num] = val; // 先在末尾插入
// 然后调整顺序
int cur = obj->num;
while(cur / 2 > 0 && obj->arr[cur / 2] < obj->arr[cur])
{
Swap(&obj->arr[cur / 2], &obj->arr[cur]);
cur = cur / 2;
}
}
void DeleteFromHeap(Heap *obj)
{
// 先交换首尾元素
Swap(&obj->arr[1], &obj->arr[obj->num]);
// 然后调整堆内顺序
obj->num--;
int index = 1;
while(true)
{
int maxIndex = index;
if(index * 2 <= obj->num && obj->arr[maxIndex] < obj->arr[index * 2])
{
maxIndex = index * 2;
}
if(index * 2 + 1 <= obj->num && obj->arr[maxIndex] < obj->arr[index * 2 + 1])
{
maxIndex = index * 2 + 1;
}
if(maxIndex == index)
{
break;
}
Swap(&obj->arr[index], &obj->arr[maxIndex]);
index = maxIndex;
}
}