本文章主要介绍堆的概念、大堆小堆的区别、以及关于堆的相关操作:插入、求堆顶元素、删除、堆的创建以及堆排序。
1. 堆的概念
如果有一个关键码的集合K={K0,K1,K2,...Kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:
若Ki<=K2*i+1且Ki<=K2*i+2,其中i=0,1,2...则称为小堆;若Ki>=K2*i+1且Ki>=K2*i+2,其中i=0,1,2...则称为大堆。
(1)小堆:任一节点的关键码均小于等于它的左右孩子的关键码,位于堆顶节点的关键码最小,从根节点到每个节点的路径上数组元素组成的序列都是递增的。举例如下为小堆:
(2)大堆:任一节点的关键码均大于等于它的左右孩子的关键码,位于堆顶节点的关键码最大,从根节点到每个节点的路径上数组元素组成的序列都是递减的。举例如下为大堆:
(3)堆的存储
堆存储在下标从0开始的数组中,因此在堆中给定下标为i的节点时:
①如果i=0时,节点i是根节点,根节点没有父节点;
②如果i!=0时,节点i不是根节点,该节点i的父节点下标为(i-1)/2
③如果2*i+1<=n-1时,则节点i的左孩子为节点2*i+1,否则节点i无左孩子
④如果2*i+2<=n-1时,则节点i的右孩子为节点2*i+2,否则节点i无右孩子
2. 堆的相关操作
2.1 头文件heap.h
由于堆是以数组的形式保存起来,故堆的结构体类型中应包含数组data(用于保存堆节点值)、数组的有效元素个数size(用于表示数组的关于堆节点的有效元素个数)、以及一个函数指针cmp(由于堆分为大堆小堆,为了方便用户去选择堆的类型,故在堆的结构体类型中定义一个函数指针,用于表示该堆为大堆还是小堆)。下面为头文件heap.h的内容:
//堆是一个完全二叉树,堆分为:小堆、大堆
//小堆:当前节点的值均小于其左右子树的值
//大堆:当前节点的值均大于其左右子树的值
//由于堆的特点,故堆以数组的形式将堆节点值保存起来
#pragma once
//宏定义一个标识符用于每次测试代码前打印其函数名
#define HEADER printf("============%s==========\n",__FUNCTION__);
//宏定义数组的大小
#define HeapMaxSize 1024
//宏定义数组元素的类型,增强代码的健壮性
#define HeapType char
//由于堆分为大堆和小堆,为了方便用户选择,故定义一个函数指针表示是大堆还是小堆
typedef int(*Compare)(HeapType a,HeapType b);
//定义堆的结构体类型
typedef struct Heap{
//保存堆节点值的数组
HeapType data[HeapMaxSize];
//size表示数组中含有堆的有效元素的个数
size_t size;
Compare cmp;
}Heap;
//函数声明
void HeapInit(Heap* heap,Compare cmp);
void HeaapDestroy(Heap* heap);
void Swap(HeapType* a,HeapType* b);
void AdjustUp(Heap* heap,size_t index);
void HeapInsert(Heap* heap,HeapType value);
int HeapRoot(Heap* heap,HeapType* value);
void HeapPrintChar(Heap* heap,char* msg);
void HeapErase(Heap* heap);
void AdjustDown(Heap* heap,size_t index);
2.2 堆的初始化
在定义堆的结构体类型时,包含三部分:数组data、有效元素个数size、以及函数指针cmp。初始化堆即表示将堆初始化为空堆,要想初始化为空堆,即合理地初始化堆的结构体,故将堆的有效元素个数size置为0,将函数指针传入指定的值用于在初始化时就确定用户选择的堆为大堆还是小堆。
//1.初始化堆
//思路:根据堆的结构体类型,将size置为0即表示为空堆,并将用户选择为大堆还是小堆确定
void HeapInit(Heap* heap,Compare cmp)
{
//非法输入
if(heap==NULL)
{
return;
}
//将size置为0表示为空堆
heap->size=0;
//将用户选择的堆类型确定
heap->cmp=cmp;
}
关于传入的函数指针,即定义堆表示大堆还是小堆的函数如下:
//由于堆分为大堆和小堆,需要用户根据自己选择决定,故编写两个函数用于选择为大堆还是小堆
//大堆
int Greater(HeapType a,HeapType b)
{
return a>b?1:0;
}
//小堆
int Less(HeapType a,HeapType b)
{
return a<b?1:0;
}
2.3 销毁堆
其实销毁和初始化的概念是一样的,销毁堆即表示将堆数组中的有效元素个数置为0并且将已建立的堆表示的大堆还是小堆的函数指针置为NULL,这样就表示已销毁了创建好的堆。
//2.销毁堆
//思路:将堆的有效元素size置为0并将函数指针置为NULL即可
void HeapDestroy(Heap* heap)
{
//非法输入
if(heap==NULL)
{
return;
}
//将size置为0
heap->size=0;
//将函数指针置为NULL
heap->cmp=NULL;
}
2.4 插入
(1)在已经初始化好的堆后面先插入新元素
(2)插入之后,可能不满足堆的性质,则需要对堆进行调整
(3)调整的原则是(拿小堆举例):任意节点值均小于其左右孩子的节点值。
(4)采用的是上浮的调整方式:当插入元素值与其父节点的值时不满足堆的性质时,交换二者的值。再继续循环判断插入元素值是否满足堆的性质,直到满足时,则调整结束。
//3.在堆中插入元素
//思路:先将元素尾插到堆中,再根据元素的值调整元素的位置
void HeapInsert(Heap* heap,HeapType value)
{
//非法输入
if(heap==NULL)
{
return;
}
//判断堆是否已满
if(heap->size>=HeapMaxSize)
{
return;
}
//先将元素尾插到堆中
heap->data[heap->size]=value;
heap->size++;
//根据元素值调整元素在堆中的位置
AdjustUp(heap,heap->size-1);
}
void AdjustUp(Heap* heap,size_t index)
{
//定义变量表示孩子节点的位置
size_t child=index;
//定义变量表示父节点的位置
size_t parent=(child-1)/2;
//循环去调整元素的位置
while(child>0)
{
//方便理解,假设这是大堆
//当不满足父节点值大于孩子节点值时,进行交换
if(!heap->cmp(heap->data[parent],heap->data[child]))
{
Swap(&heap->data[parent],&heap->data[child]);
}
else
{
//当当前位置符合要求时表示均已符合堆的要求,故直接break
break;
}
//更新孩子节点的位置以及父节点的位置
child=parent;
parent=(child-1)/2;
}
}
void Swap(HeapType* a,HeapType* b)
{
HeapType tmp=*a;
*a=*b;
*b=tmp;
}
2.5 求堆顶元素
根据堆的插入,求堆顶元素即表示堆数组的第一个元素值。
//4.取堆顶元素
//思路:由于堆元素存储在数组中,取堆顶元素即表示取数组的第一个元素
int HeapRoot(Heap* heap,HeapType* value)
{
//非法输入
if(value==NULL||heap==NULL)
{
//取堆顶元素失败返回0
return 0;
}
//判断堆为空堆时
if(heap->size==0)
{
//取堆顶元素失败返回0
return 0;
}
//非空堆时,堆顶元素即为数组第一个元素
*value=heap->data[0];
return 1;
}
2.6 堆删除
堆删除即删除堆顶元素,堆删除的方法是:
(1)将堆中最后一个元素与堆顶元素交换
(2)对堆的有效元素个数size进行减1即表示将堆中最后一个元素删除
(3)此时,堆的结构可能受到破坏,故根据堆的性质,调整交换后的堆顶元素的位置。
(4)以下沉的方式进行调整(以小堆为例说明):将堆顶元素与其左右孩子节点的最小值比较,若不符合小堆的性质,则进行交换,若符合小堆的性质则表示之后的节点也均符合小堆的性质。再继续判断交换后的节点是否符合小堆的性质。
(5)以下沉的方式进行调整(以大堆为例说明):将堆顶元素与其左右孩子节点的最大值比较,若不符合大堆的性质,则进行交换,若符合大堆的性质则表示之后的节点也均符合大堆的性质。再继续判断交换后的节点是否符合大堆的性质。
//5.堆的删除:删除堆顶元素
//思路:1.将堆中的最后一个元素与堆顶元素进行交换
// 2.将堆中元素个数减1即表示删除了堆中的最后一个元素
// 3.此时,堆的结构可能被破坏,从数组首元素开始调整使其满足堆的特性
void HeapErase(Heap* heap)
{
//非法输入
if(heap==NULL)
{
return;
}
//空堆
if(heap->size==0)
{
return;
}
//非空堆
//1.交换堆顶元素与堆中最后一个元素的值
Swap(&heap->data[0],&heap->data[heap->size-1]);
//2.将堆中元素个数减1,相当于将堆中最后一个元素删除
--heap->size;
//3.堆的结构可能被破坏,从根节点开始调整
AdjustDown(heap,0);
}
void AdjustDown(Heap* heap,size_t index)
{
size_t parent=index;
size_t child=2*parent+1;
while(child<heap->size)
{
//为了方便理解,假设此堆是大堆
//1.若右子树存在且比左子树大时,child指向child+1
if(child+1<heap->size&&heap->cmp(heap->data[child+1],heap->data[child]))
{
child=child+1;
}
//2.在其左右子树中找到最大值与当前根节点比较,若不满足堆特性则进行交换
if(heap->cmp(heap->data[child],heap->data[parent]))
{
Swap(&heap->data[child],&heap->data[parent]);
}
else
{
//一旦当前根节点符合堆特性,则表示之后的节点均符合,故直接break
break;
}
parent=child;
child=2*parent+1;
}
}
2.7 堆的创建
堆的创建是根据给定数组创建堆,那么根据给定数组对堆进行插入操作即可。
//6.创建堆(根据数组)
//思路:将数组元素依次插入到堆中即可
void HeapCreate(Heap* heap,HeapType array[],size_t size)
{
//非法输入
if(heap==NULL)
{
return;
}
//遍历数组array将其数组元素插入堆中
size_t i=0;
for(i=0;i<size;i++)
{
HeapInsert(heap,array[i]);
}
}
2.8 堆排序
在堆的删除操作中,我们会发现,每次删除的元素为堆中最大元素或最小元素,故可以根据给定未排序的数组利用堆的创建以及删除对数组进行排序。由于堆的性质,在堆排序中:
当要进行升序排序时,需要创建大堆再进行删除。
当要进行降序排序时,需要创建小堆再进行删除。
//7.堆排序
//思路:由于堆的特点是根节点的值均大于其左右子树的值,故对堆进行一次删除相当于将最大值或最小值进行删除
//1.根据数组创建堆,要想升序排序则创建大堆,要想降序排序则创建小堆
//2.依次删除堆
void HeapSort(HeapType array[],size_t size)
{
//1.创建堆,若想要升序排序则创建大堆
Heap heap;
HeapInit(&heap,Greater);
HeapCreate(&heap,array,size);
//2.堆堆元素依次删除
while(heap.size!=0)
{
HeapErase(&heap);
}
//拷贝结果
memcpy(array,heap.data,size*sizeof(HeapType));
}
3. 测试代码
以下为测试代码以供参考。
/*==========测试代码块==========*/
//测试HeapInit
void Test_HeapInit()
{
HEADER;
Heap heap;
HeapInit(&heap,Less);
printf("size expected 0,actual %d\n",heap.size);
printf("expected 1,actual %d\n",heap.cmp(1,2));
}
//测试HeapDestroy
void Test_HeapDestroy()
{
HEADER;
Heap heap;
HeapInit(&heap,Greater);
HeapDestroy(&heap);
printf("size expected 0,actual %d\n",heap.size);
printf("expected NULL,actual %p\n",heap.cmp);
}
//测试HeapInsert
//写一个打印函数用于测试
void HeapPrintChar(Heap* heap,char* msg)
{
printf(msg);
if(heap==NULL)
{
return;
}
int i=0;
for(i=0;i<heap->size;i++)
{
printf("%c ",heap->data[i]);
}
printf("\n");
}
void Test_HeapInsert()
{
HEADER;
Heap heap;
HeapInit(&heap,Greater);
HeapInsert(&heap,'c');
HeapInsert(&heap,'a');
HeapInsert(&heap,'d');
HeapInsert(&heap,'b');
HeapInsert(&heap,'f');
HeapInsert(&heap,'e');
HeapPrintChar(&heap,"插入元素:c a d b f e\n");
}
//测试HeapRoot
void Test_HeapRoot()
{
HEADER;
Heap heap;
HeapInit(&heap,Greater);
HeapInsert(&heap,'c');
HeapInsert(&heap,'a');
HeapInsert(&heap,'d');
HeapInsert(&heap,'b');
HeapInsert(&heap,'f');
HeapInsert(&heap,'e');
HeapType value;
int ret=HeapRoot(&heap,&value);
printf("expected 1,actual %d\n",ret);
printf("expected f,actual %c\n",value);
}
//测试HeapErase
void Test_HeapErase()
{
HEADER;
Heap heap;
HeapInit(&heap,Greater);
HeapInsert(&heap,'c');
HeapInsert(&heap,'a');
HeapInsert(&heap,'d');
HeapInsert(&heap,'b');
HeapInsert(&heap,'f');
HeapInsert(&heap,'e');
HeapPrintChar(&heap,"堆元素\n");
HeapErase(&heap);
HeapPrintChar(&heap,"删除堆顶元素\n");
}
//测试HeapCreate
void Test_HeapCreate()
{
HEADER;
Heap heap;
HeapInit(&heap,Greater);
HeapType array[]={'c','a','d','b','f','e'};
size_t size=sizeof(array)/sizeof(HeapType);
HeapCreate(&heap,array,size);
HeapPrintChar(&heap,"创建堆\n");
}
//测试HeapSort
void Test_HeapSort()
{
HEADER;
HeapType array[]={'c','a','d','b','f','e'};
size_t size=sizeof(array)/sizeof(HeapType);
HeapSort(array,size);
//打印数组
int i=0;
printf("升序排序\n");
for(i=0;i<size;i++)
{
printf("%c ",array[i]);
}
printf("\n");
}
/*==========主函数========*/
int main()
{
Test_HeapInit();
Test_HeapDestroy();
Test_HeapInsert();
Test_HeapRoot();
Test_HeapErase();
Test_HeapCreate();
Test_HeapSort();
return 0;
}
以上就是关于堆的相关操作!