堆就是顺序结构二叉树。
如果有一个关键码的集合K = { K0,K1 ,K2 ,K3…,Kn-1 },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki<=K2i+1 且Ki<= K2i+2 (Ki >=K2i+1 且 Ki>=K2i+2 ) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆有以下性质:
(1)堆中某个节点的值总是不大于或不小于其父节点的值;
(2)堆总是一棵完全二叉树。
我们在构建堆时,要想象堆的逻辑结构进行操作,但实际堆在计算机中是以数组的方式储存的。
堆的逻辑结构是一颗树,树上的每个节点都有一个值,根据节点之间的大小关系,堆被分为小根堆和大根堆两种类型。堆的储存结构一般采用数组来实现,数组中的元素的排列顺序是按照完全二叉树的顺序来存储的,即从上到下,从左到右的顺序。
对于一个大根堆而言,储存结构中数组的第一个元素即为堆中的最大值,小堆则相反,也就是说逻辑上靠近根节点的实际上也是储存结构中靠前的元素。
由于堆的储存结构是按照完全二叉树的形式存储的,因此在进行堆的插入和删除操作时,可以方便的通过数组来进行操作,无需关注堆的具体逻辑结构,即可实现堆的调整和维护。
堆的插入操作需要保证插入新数之后仍然满足堆的性质,即每个节点都大于或等于其子节点。插入操作的具体过程如下:
(1)将新数插入堆的最后一个位置,保持完全二叉树的形状。
(2)将新数与其父节点比较,如果新数大于其父节点,则交换它们的位置,直到新数不再大于其父节点或者到达堆的根节点。
AdjustUp函数实现了从下向上调整堆的结构,使得满足堆的性质,具体实现过程如下:
(1)从子节点child开始,计算其父节点parent的位置,parent的位置为(child - 1) / 2;
(2)如果子节点比父节点的值大,则交换两个节点的值,否则结束调整;
(3)将child和parent向上移动,继续比较和交换它们的值,直到child小于它的父节点或者已达到堆的根部(即parent为0)。
这个过程也被称为上滤(Percolate Up)操作,用于添加一个新的元素时,将新元素添加到堆的底部,然后执行上滤操作,将新元素放到合适的位置,以满足堆的性质。
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;
}
else
{
break;
}
}
}
堆的删除操作需要保证删除后仍然满足堆的性质,即每个节点都大于或等于其子节点。删除操作的具体过程如下:
(1)将堆顶元素与堆中最后一个元素进行交换。
(2)删除堆中最后一个元素。
(3)将新的堆顶元素与其子节点比较,如果新堆顶元素小于其子节点,则将其与子节点中较大的一个交换,直到新的堆顶元素不再小于其子节点或到达堆的底部。
这段代码实现了从上向下调整堆的结构,使得满足堆的性质,具体实现过程如下:
(1)从父节点parent开始,计算其左子节点child的位置为parent*2+1;
(2)如果父节点的值比其子节点中的较小值还小,则将两个节点的值交换;
(3)将parent向下移动到child的位置,计算新的child的索引;
(4)重复上述2、3步,直到parent比其子节点的值都小或者已经到达堆的底部。
这个过程也被称为下滤(Percolate Down)操作,用于删除堆顶元素后重新调整堆的结构。
void AdjustDown(HPDataType* a, int n, int 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;
}
else
{
break;
}
}
}
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
//这里以小堆为例
int array[] = {27,15,19,18,28,34,65,49,25,37};
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):
因此:建堆的时间复杂度为O(N)。
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
(1)建堆(升序:建大堆 降序:建小堆)
(2)利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
现在只是简单介绍一下,以后我们会对堆排序进行详细的介绍。
Heap.h
#pragma once
#include
#include
#include
#include
#include
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HeapPrint(HP* php);
void HeapInit(HP* php);
void HeapDestroy(HP* php);
void HeapPush(HP* php, HPDataType x);
void HeapPop(HP* php);
HPDataType HeapTop(HP* php);
bool HeapEmpty(HP* php);
int HeapSize(HP* php);
void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int n, int parent);
void Swap(HPDataType* p1, HPDataType* p2);
Heap.c
#include"Heap.h"
void HeapPrint(HP* php)
{
assert(php);
if (php->a == NULL)
{
printf("该堆为空堆\n");
}
else
{
printf("该堆中的元素是:");
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
}
void HeapInit(HP* php)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
if (php->a == NULL)
{
perror("malloc fail");
return;
}
php->size = 0;
php->capacity = 4;
}
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->capacity = 0;
php->size = 0;
php->a = NULL;
}
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType x = *p1;
*p1 = *p2;
*p2 = x;
}
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;
}
else
{
break;
}
}
}
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->capacity == php->size)
{
HPDataType*tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * php->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity *= 2;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
void AdjustDown(HPDataType* a, int n, int 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;
}
else
{
break;
}
}
}
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
HPDataType HeapTop(HP* php)
{
assert(php);
return php->a[0];
}
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
Test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
void HeapTest1()
{
HP hp;
HeapInit(&hp);
HeapPrint(&hp);
HeapPush(&hp, 12);
HeapPush(&hp, 4);
HeapPrint(&hp);
HeapPush(&hp, 2);
HeapPush(&hp, 22);
HeapPush(&hp, 18);
HeapPrint(&hp);
HeapPush(&hp, 6);
HeapPush(&hp, 10);
HeapPush(&hp, 54);
HeapPush(&hp, 43);
HeapPrint(&hp);
}
int main()
{
HeapTest1();
return 0;
}