0-概述
堆,又称优先队列(Priority Queue),是一种特殊的队列,取出元素的顺序是按照元素的优先权(关键字)的大小,而不是元素进入队列的先后顺序。
一般用完全二叉树实现堆。堆可分为大顶堆和小顶堆两种,顾名思义,大顶堆就是对于任意子树,根节点都比左右子节点大;小顶堆就是对于任意子树,根节点都比左右子节点小,二者的实现方式相同,逻辑结构都是完全二叉树,采用数组存储。
下面是4个堆的例子,前2个是大顶堆,后2个是小顶堆:
堆的主要操作有:
- 创建堆
- 向堆中插入元素
- 删除堆顶元素
- 建堆(将一个无序数组排成堆)
相关函数声明如下:
//创建一个容量为MaxSize的最大堆
MaxHeap Create(int MaxSize);
//向堆中插入元素
void Insert(MaxHeap H, ElementType item);
// Pop掉大顶堆的根节点:从根节点开始,用最大堆中的最后一个元素向上过滤下层节点(比该元素大的上移)
ElementType DeleteMax(MaxHeap H);
//建堆:从最后一个有儿子的节点开始不断的向前迭代将其子树调整为堆
void buildMaxHeap(MaxHeap H);
//向下过滤节点
void percDown(MaxHeap H, int n);
其中percDown
是堆操作的核心函数,用于向下过滤节点。
下面是堆操作的实现:
1-堆结构的定义
首先是堆结构的定义:
//哨兵值
#define MAXDATA 99999
typedef int ElementType;
struct HeapStruct {
ElementType* Data;
int Size;
int Capacity;
};
//定义一个最大堆类型:指向HeapStruct的指针
typedef struct HeapStruct* MaxHeap;
- 宏定义一个极大的哨兵值
99999
,存放在堆中数组中的0号位置,方便操作(跳出循环)。 - 堆结构中包含三个分量:指向堆区的数组指针,堆的当前大小,堆的容量。
- 定义一个大顶堆
MaxHeap
类型为指向堆结构的指针。
2-创造堆
堆的创造很简单,就是给结构中的指针开辟空间,然后将成员变量做初始化,最后将哨兵设置好,代码如下:
MaxHeap Create(int MaxSize) {
MaxHeap H = malloc(sizeof(struct HeapStruct));
//为数组开空间,0号元素存的是哨兵,所以要+1
H->Data = malloc((MaxSize + 1) * sizeof(struct HeapStruct));
H->Size = 0;
H->Capacity = MaxSize;
//哨兵
H->Data[0] = MAXDATA;
return H;
}
3-向堆中插入元素
一个大顶堆的插入过程如下图所示:
堆采用数组存储,那么执行插入操作自然而然就插在最后一个位置,但是插入后很可能破坏堆的有序性,所以需要进行位置调整:比较当前节点是否小于父节点,若小于,则当前位置就正好可以插入,满足堆的有序性。否则,若当前节点大于父节点while (item > H->Data[i / 2])
,就让父节点下来到当前位置i
,自己上去到i/2
的位置,代码实现如下:
void Insert(MaxHeap H, ElementType item) {
if (isFull(H)) {
printf("堆已满\n");
return;
}
//刚开始假设插入的位置在数组的最后
int i = ++H->Size;
//只要item比父节点大:
while (item > H->Data[i / 2]) {
//父节点下来
H->Data[i] = H->Data[i / 2];
//我上去
i = i / 2;
}
//跳出循环时i锚定了合适的插入位置,赋值
H->Data[i] = item;
return;
}
4-删除堆顶元素
一个大顶堆的删除过程如下图所示:
首先我们将堆顶元素存入MaxItem
,然后将数组中最后一个元素存入tmp
,并将Size--
,接下来我们需要重新调整堆,就是要给tmp找到合适的存放位置(由Parent
指向)。
令Parent
初始时指向根节点,只要Parent
还有左孩子while (Parent * 2 <= H->Size)
,就执行循环:
Child
指向Parent
的左孩子;- 如果
Parent
还有右孩子,且右孩子比左孩子更大,就让Child
指向右孩子,现在Child
已经指向了Parent
的左右孩子中较大者; - 如果
tmp
比Child
大,说明tmp
可以放在这,break; - 否则,应该让
Child
上来,自己下去;
代码实现如下:
ElementType DeleteMax(MaxHeap H) {
if (isEmpty(H)) {
printf("堆为空\n");
return H->Data[0];
}
ElementType MaxItem = H->Data[1];
ElementType tmp = H->Data[H->Size--];
int Parent = 1;
int Child;
//算法核心:给tmp找到合适的位置
while (Parent * 2 <= H->Size) {
// Child指向左孩子
Child = 2 * Parent;
// Child指向左右孩子中最大者
if ((Child != H->Size) && H->Data[Child] < H->Data[Child + 1]) {
Child++;
}
//如果tmp>左右孩子最大者,说明tmp在这里坐得住,跳出循环
if (tmp > H->Data[Child]) {
break;
} else {
//让孩子上来
H->Data[Parent] = H->Data[Child];
//自己下去
Parent = Child;
}
}
H->Data[Parent] = tmp;
return MaxItem;
}
5-建堆(将一个无序数组排成堆)
作为树的一种,堆也是一种递归的结构:对于任何一个节点,其左子树是一个堆,右子树也是一个堆,以此类推,直到叶子节点。
建堆的过程buildMaxHeap
也类似于一个递归的过程:从最后一个有孩子的节点开始,将当前子树调整为堆,然后向前迭代,不断的将当前子树调整为堆,直到根节点。
建堆的核心在于调整节点使当前子树为堆,这在上面“删除堆顶元素”中已经提到过了,就是对于当前节点,要不断的跟左右孩子比较,寻找合适的插入位置,也就是上面的循环体中内容。
代码实现如下:
void buildMaxHeap(MaxHeap H) {
//从最后一个有儿子的节点开始
for (int i = H->Size / 2; i > 0; i--) {
percDown(H, i);
}
}
void percDown(MaxHeap H, int n) {
ElementType top;
int Child;
int Parent = n;
top = H->Data[n];
//向下过滤
while (Parent * 2 <= H->Size) {
Child = Parent * 2;
if (Child != H->Size && H->Data[Child] < H->Data[Child + 1]) {
Child++;
}
if (top > H->Data[Child]) {
break;
} else {
H->Data[Parent] = H->Data[Child];
Parent = Child;
}
}
H->Data[Parent] = top;
}