目录
一、堆的概念和结构
1:堆的概念
上文中我们已经提到过,堆是一个按照完全二叉树的储存方式顺序储存在一个一维数组中的结构。
堆:如果有一个关键码的集合K = {k0,k1,k2,k3…k(n-1)},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki<=K(2*i+1)且Ki<=K(2*i+2) {Ki>=K(2*i+1)且Ki>=K(2*i+2)} i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
1.小堆:每一个父结点的值均小于或等于其子结点的值,且其根结点的值是最小的。
2.大堆:每一个父结点的值均大于或等于其子结点的值,且其根结点的值是最大的。
· 堆的性质:
1. 堆中某个节点的值总是不大于或不小于其父节点的值;
2. 堆总是一棵完全二叉树。
2:堆的结构
二、堆的实现
1:堆的简单接口实现
创建堆结构
·思路:
堆是用数组实现的,所以创建堆结构实质上就是开辟一个动态数组。
· Heap.h文件:
//创建堆结构(以小堆为例) typedef int HPDataType; //堆中储存数据类型 typedef struct Heap { HPDataType* a; //数组地址 size_t size; //记录堆中有效数据个数 size_t capacity; //记录堆的容量 }HP;
初始化堆
·思路:
初始化堆和之前初始化顺序表,栈等都是同样的道理。
· Heap.h文件:
//初始化堆 void HeapInit(HP* php);
· Heap.c文件:
//初始化堆 void HeapInit(HP* php) { assert(php); //传过来的结构体指针不能为空 php->a = NULL; php->size = php->capacity = 0; }
堆的打印
·思路:
堆的物理结构是数组,所以打印堆实质上就是打印顺序表。
· Heap.h文件:
//堆的打印 void HeapPrint(HP* php);
· Heap.c文件:
//堆的打印 void HeapPrint(HP* php) { assert(php); for (size_t i = 0; i < php->size; i++) { printf("%d ", php->a[i]); } printf("\n"); }
堆的销毁
·思路:
free掉动态开辟的内存
· Heap.h文件:
//堆的销毁 void HeapDestory(HP* php);
· Heap.c文件:
//堆的销毁 void HeapDestory(HP* php) { assert(php); free(php->a); php->a = NULL; php->size = php->capacity = 0; }
堆的数据交换
·思路:
思路和交换两个整型数据一样,同时记得传地址。
· Heap.h文件:
//数据交换 void Swap(HPDataType* pa, HPDataType* pb);
· Heap.c文件:
//数据交换 void Swap(HPDataType* pa, HPDataType* pb) { HPDataType tmp = *pa; *pa = *pb; *pb = tmp; }
堆的判空
·思路:
判断size是否为0,如果size为0则说明堆中无元素,堆为空。
· Heap.h文件:
//堆的判空 bool HeapEmpty(HP* php);
· Heap.c文件:
//堆的判空 bool HeapEmpty(HP* php) { assert(php); return php->size == 0; }
获取堆中元素个数
·思路:
堆中元素个数也就是size的值。
· Heap.h文件:
//获取堆中元素个数 size_t HeapSize(HP* php);
· Heap.c文件:
//获取堆中元素个数 size_t HeapSize(HP* php) { assert(php); return php->size; }
获取堆顶的元素
·思路:
获取堆顶元素之前首先需要确定堆不为空。也就是当size>0的前提下返回堆顶元素。
· Heap.h文件:
//获取堆顶元素 HPDataType HeapTop(HP* php);
· Heap.c文件:
//获取堆顶元素 HPDataType HeapTop(HP* php) { assert(php); assert(php->size > 0); return php->a[0]; }
2:堆的核心接口实现
堆的向上调整算法(从下往上调整)
·思路:
堆的向上调整算法是为了当我们向堆中插入一个数据后,堆的结构仍然不发生改变,大堆依然是大堆,小堆依然是小堆。
举个例子:假如我们向下图中的小堆中插入一个10。
我们向小根堆中插入10,插入后孩子结点10<父结点28,这破坏了小根堆结构,所以此时我们就需要向上调整,依次比较父结点parent和子结点child的大小,当父结点小于子结点就结束,反之就一直交换结点数据,直到根部。
父结点与子结点关系:parent=(child-1)/2
✈交换过程如下图所示✈:
· Heap.h文件:
//堆的向上调整算法 void AdjustUp(HPDataType* a, size_t child);
· Heap.c文件:
//堆的向上调整算法 void AdjustUp(HPDataType* a, size_t child) { size_t parent = (child - 1) / 2; while (child > 0) { //if(a[child]>a[parent]) //大根堆 if (a[child] < a[parent]) //小根堆 { Swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } }
堆的向下调整算法(从上往下调整)
·思路:
1、找出左右孩子结点中较小的那一个
2、较小的孩子结点与父结点比较,如果孩子结点比父亲结点小,交换数据
3、重复向下调整
举个例子:假如我们要调整下面的堆
✈交换过程如下图所示✈:
· Heap.h文件:
//堆的向下调整算法 void AdjustDown(HPDataType* a, size_t size, size_t root);
· Heap.c文件:
//堆的向下调整算法 void AdjustDown(HPDataType* a, size_t size, size_t root) { int parent = root; int child = 2 * parent + 1; //默认较小孩子结点为左孩子结点 while (child < size) { //确保child对应的值为较小孩子结点的值 if (child + 1 < size && a[child + 1] < a[child]) //确保右孩子结点在堆得的范围内 { child++; //此时child中存的值为右孩子结点中的值 } //如果孩子结点小于父亲结点则交换,并继续向下调整 if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = 2 * parent + 1; } //如果孩子结点大于父亲结点,则结束调整 else { break; } } }
堆的插入
·思路:
虽然堆的物理结构是一个数组,但因为其逻辑结构是一个完全二叉树,所以当我们插入数据只能进行尾插。尾插后可能结构发生改变,这时候我们就需要进行向上调整,使堆恢复原来的结构。
举个例子:假如我们向下图中的小堆中插入一个10。
步骤:
1:检查扩容:和顺序表一样,当我们插入一个数据后我们首先要做的就是检查是否需要扩容。
2:调整堆结构:插入10以后我们可以发现子结点10小于父结点28,堆的结构遭到破环,此时我们就需要将10进行向上调整。
· Heap.h文件:
//堆的插入 void HeapPush(HP* php, HPDataType x);
· Heap.c文件:
//堆的插入 void HeapPush(HP* php, HPDataType x) { assert(php); //检查是否需要扩容 if (php->size == php->capacity) { size_t newcapacity = php->capacity == 0 ? 4 : php->capacity * 2; HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity); if (tmp == NULL) { printf("realloc fail\n"); exit(-1); } php->a = tmp; php->capacity = newcapacity; } php->a[php->size] = x; php->size++; //调整堆结构 AdjustUp(php->a, php->size - 1); }
✈下面我们来测试一下✈:
· Test.c文件:
void TestHeap() { HP hp; HeapInit(&hp); //插入数据 HeapPush(&hp, 1); HeapPush(&hp, 5); HeapPush(&hp, 3); HeapPush(&hp, 0); HeapPush(&hp, 8); HeapPush(&hp, 9); //打印 HeapPrint(&hp); //销毁 HeapDestory(&hp); }
测试效果:
堆的删除
·思路:
向堆中插入数据后需要保持堆的结构不发生改变,同理在堆中删除数据后我们仍需要使堆的结构不发生改变。所不一样的是在堆中插入数据我们是从尾部插入,而从堆中删除数据我们是从堆顶删除。
步骤:
First:把堆顶数据与堆中最后一个数据进行交换
Next:size--,删除掉堆顶数据。
Last :运用向下调整算法,确保其仍是堆结构
举个例子:假如我们删除下图小堆中的堆顶元素。
✈交换过程如下图所示✈:
‼⚠:时间复杂度分析
第一个数据和最后一个数据进行交换,时间复杂度为O(1)。因为向下调整的次数是树的深度,所以向下调整算法的时间复杂度为O(logN),所以总的时间复杂度为O(logN)。
· Heap.h文件:
//堆的删除 void HeapPop(HP* php);
· Heap.c文件:
//堆的删除 void HeapPop(HP* php) { assert(php); assert(php->size > 0); //确保堆中有元素 Swap(&php->a[0], &php->a[php->size - 1]); //交换堆头和堆尾的数据 php->size--; //删除掉堆顶数据 //向下调整,确保堆结构 AdjustDown(php->a, php->size, 0); }
✈下面我们来测试一下✈:
· Test.c文件:
void TestHeap2() { HP hp; HeapInit(&hp); //插入数据 HeapPush(&hp, 1); HeapPush(&hp, 5); HeapPush(&hp, 3); HeapPush(&hp, 0); HeapPush(&hp, 8); HeapPush(&hp, 9); //打印 HeapPrint(&hp); //删除堆顶数据 HeapPop(&hp); HeapPrint(&hp); //销毁 HeapDestory(&hp); }
测试效果:
我们可以发现删除前后都还是小根堆。
三、总代码
Heap.h文件
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
//创建堆结构(以小堆为例)
typedef int HPDataType; //堆中储存数据类型
typedef struct Heap
{
HPDataType* a; //数组地址
size_t size; //记录堆中有效数据个数
size_t capacity; //记录堆的容量
}HP;
//初始化堆
void HeapInit(HP* php);
//堆的打印
void HeapPrint(HP* php);
//堆的销毁
void HeapDestory(HP* php);
//数据交换
void Swap(HPDataType* pa, HPDataType* pb);
//堆的判空
bool HeapEmpty(HP* php);
//获取堆中元素个数
size_t HeapSize(HP* php);
//获取堆顶元素
HPDataType HeapTop(HP* php);
//堆的向上调整算法
void AdjustUp(HPDataType* a, size_t child);
//堆的向下调整算法
void AdjustDown(HPDataType* a, size_t size, size_t root);
//堆的插入
void HeapPush(HP* php, HPDataType x);
//堆的删除
void HeapPop(HP* php);
Heap.c文件
#include "Heap.h"
//初始化堆
void HeapInit(HP* php)
{
assert(php); //传过来的结构体指针不能为空
php->a = NULL;
php->size = php->capacity = 0;
}
//堆的打印
void HeapPrint(HP* php)
{
assert(php);
for (size_t i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
//堆的销毁
void HeapDestory(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
//数据交换
void Swap(HPDataType* pa, HPDataType* pb)
{
HPDataType tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//堆的判空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//获取堆中元素个数
size_t HeapSize(HP* php)
{
assert(php);
return php->size;
}
//获取堆顶元素
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
//堆的向上调整算法
void AdjustUp(HPDataType* a, size_t child)
{
size_t parent = (child - 1) / 2;
while (child > 0)
{
//if(a[child]>a[parent]) //大根堆
if (a[child] < a[parent]) //小根堆
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//堆的向下调整算法
void AdjustDown(HPDataType* a, size_t size, size_t root)
{
int parent = root;
int child = 2 * parent + 1; //默认较小孩子结点为左孩子结点
while (child < size)
{
//确保child对应的值为较小孩子结点的值
if (child + 1 < size && a[child + 1] < a[child]) //确保右孩子结点在堆得的范围内
{
child++; //此时child中存的值为右孩子结点中的值
}
//如果孩子结点小于父亲结点则交换,并继续向下调整
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
//如果孩子结点大于父亲结点,则结束调整
else
{
break;
}
}
}
//堆的插入
void HeapPush(HP* php, HPDataType x)
{
assert(php);
//检查是否需要扩容
if (php->size == php->capacity)
{
size_t newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
//调整堆结构
AdjustUp(php->a, php->size - 1);
}
//堆的删除
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0); //确保堆中有元素
Swap(&php->a[0], &php->a[php->size - 1]); //交换堆头和堆尾的数据
php->size--; //删除掉堆顶数据
//向下调整,确保堆结构
AdjustDown(php->a, php->size, 0);
}
Test.c文件
#include "Heap.h"
void TestHeap1()
{
HP hp;
HeapInit(&hp);
//插入数据
HeapPush(&hp, 1);
HeapPush(&hp, 5);
HeapPush(&hp, 3);
HeapPush(&hp, 0);
HeapPush(&hp, 8);
HeapPush(&hp, 9);
//打印
HeapPrint(&hp);
//销毁
HeapDestory(&hp);
}
void TestHeap2()
{
HP hp;
HeapInit(&hp);
//插入数据
HeapPush(&hp, 1);
HeapPush(&hp, 5);
HeapPush(&hp, 3);
HeapPush(&hp, 0);
HeapPush(&hp, 8);
HeapPush(&hp, 9);
//打印
HeapPrint(&hp);
//删除堆顶数据
HeapPop(&hp);
HeapPrint(&hp);
//销毁
HeapDestory(&hp);
}
int main()
{
TestHeap2();
return 0;
}