1.堆的概念
将一个关键码的集合K = {k0 , k1,k2,k3……kn-1}把他所有元素按完全二叉树的存储方式放在一个一维数组中,并且满足双亲节点大于孩子节点,或者双亲节点小于孩子节点将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
1.2堆的性质
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树
2.堆的实现及其接口详解
2.1Heap.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
typedef int Datatype;
typedef int (*func)(Datatype left, Datatype right);
typedef struct Heap {
Datatype* arr;
int capacity;
int size;
func Comper;
}Heap;
//堆的创建 用回调函数判断是要创建小堆还是大堆
void HeapCreate(Heap* p, Datatype *arr, int size, func Comper);
//向上调整
void HeapAdjustup(Heap* p, int child);
//向下调整
void HeapAdjustDown(Heap* p, int parent);
//堆的插入
void HeapPush(Heap* p, Datatype data);
//堆的移除
void HeapErase(Heap* p);
//获取堆顶元素
Datatype HeapTop(Heap* p);
//获取堆的长度
int HeapSize(Heap* p);
//堆是否为空
int HeapEmpty(Heap* p);
//堆的销毁
void HeapDestroy(Heap* p);
//堆的扩容
void HeapCheakCapacity(Heap* p);
2.2Heap.c
#include "Heap.h"
//交换函数
void Swap(Datatype* left, Datatype* right) {
Datatype temp = *left;
*left = *right;
*right = temp;
}
int Max(Datatype left, Datatype right)
{
return left > right;
}
int Min(Datatype left, Datatype right)
{
return left < right;
}
//只是在除了根节点以外 他的两个子树也是堆结构的情况下才成立
//而且你要小堆我得改一次大于号,远没有回调函数那样的便捷
//回调函数忘记了可以自己手动实现一次qsort
//冒泡排序
void BobSort(Datatype* arr,int size) {
for (int i = 0; i < size - 1; i++) {
for (int j = 0; j < size - 1 - i; j++) {
if (arr[j - 1] > arr[j]) {
Swap(&arr[j - 1], &arr[j]);
}
}
}
}
//向下调整
void HeapAdjustDown(Heap* p, int parent)
{
int child = (parent << 1) + 1;
int size = p->size;
while (child < size) {
//因为我么对parent * 2 + 1找到的是这个节点对应的左孩子
//要是我右孩子还小呢
//利用回调函数找到更小的那一个将child指针移动到右孩子那块
if (child + 1 < size && p->Comper(p->arr[child + 1], p->arr[child])) {
child += 1;
}
//是否满足堆的特性
if (p->Comper(p->arr[child], p->arr[parent])) {
Swap(&p->arr[parent], &p->arr[child]);
parent = child;
child = (parent << 1) + 1;
}
else {
return;
}
}
}
//向上调整
void HeapAdjustup(Heap* p, int child)
{
int parent = (child - 1) >> 1;
while (child) {
if (p->Comper(p->arr[child], p->arr[parent])) {
Swap(&p->arr[child], &p->arr[parent]);
child = parent;
parent = (child - 1) >> 1;
}
else {
return;
}
}
}
//堆的扩容
void HeapCheakCapacity(Heap* p)
{
assert(p);
//判断是否需要扩容
if (p->size == p->capacity) {
//把容量变成原来的两倍
int NewCapaty = p->capacity << 1;
//从堆上开辟新的内存空间
Datatype* temp = (Datatype*)malloc(sizeof(Datatype) * NewCapaty);
if (NULL == temp) {
assert(0);
return;
}
//把原来的数据拷贝
memcpy(temp, p->arr, sizeof(Datatype) * p->size);
//释放旧空间改变指针指向
free(p->arr);
p->arr = temp;
p->capacity;
}
}
//堆的创建
// 创建堆的时候需要用到向下调整
void HeapCreate(Heap* p, Datatype *arr, int size, func Comper)
{
assert(p);
//动态申请内存完成堆的初始化
p->arr = (Datatype*)malloc(sizeof(Datatype) * size);
//检测是否成功开辟空间
if (NULL == p->arr) {
assert(0);
return;
}
//更新容量
p->capacity = size;
//把你要调整的数据放在你创建的堆里面
memcpy(p->arr, arr, sizeof(Datatype) * size);
p->size = size;
p->Comper = Comper;
for (int root = (size - 2) / 2; root >= 0; root--)
{
HeapAdjustDown(p, root);
}
}
//插入
void HeapPush(Heap* p, Datatype data)
{
HeapCheakCapacity(p);
p->arr[p->size] = data;
p->size++;
HeapAdjustup(p, p->size - 1);
}
void HeapErase(Heap* p)
{
if (HeapEmpty(p)) {
return;
}
Swap(&p->arr[0], &p->arr[p->size - 1]);
p->size--;
HeapAdjustDown(p, 0);
}
//获取堆顶元素
//因为我们是顺序表构建的堆
//那他的0号下标必是root
Datatype HeapTop(Heap* p)
{
assert(p);
return p->arr[0];
}
//获取堆的长度
int HeapSize(Heap* p)
{
assert(p);
return p->size;
}
//检验堆是否为空
int HeapEmpty(Heap* p)
{
assert(p);
return 0 == p->size;
}
//堆的销毁
void HeapDestroy(Heap* p)
{
assert(p);
//看是否不为空 然后就直接free 最后更新堆的数据
if (p->arr) {
free(p->arr);
p->arr = NULL;
p->capacity = 0;
p->size = 0;
}
}
int main() {
int arr[] = { 49, 27, 37, 65, 28, 34, 25, 15, 18, 19 };
Heap p;
HeapCreate(&p, arr, sizeof(arr) / sizeof(arr[0]), Min);
printf("top = %d\n", HeapTop(&p));
printf("size = %d\n", HeapSize(&p));
HeapPush(&p, 10);
printf("top = %d\n", HeapTop(&p));
printf("size = %d\n", HeapSize(&p));
HeapErase(&p);
printf("top = %d\n", HeapTop(&p));
printf("size = %d\n", HeapSize(&p));
HeapDestroy(&p);
}
2.3接口实现以及算法详解
2.3.1向下调整算法
//向下调整
void HeapAdjustDown(Heap* p, int parent)
{
int child = (parent << 1) + 1;
int size = p->size;
while (child < size) {
//因为我么对parent * 2 + 1找到的是这个节点对应的左孩子
//要是我右孩子还小呢
//利用回调函数找到更小的那一个将child指针移动到右孩子那块
if (child + 1 < size && p->Comper(p->arr[child + 1], p->arr[child])) {
child += 1;
}
//是否满足堆的特性
if (p->Comper(p->arr[child], p->arr[parent])) {
Swap(&p->arr[parent], &p->arr[child]);
parent = child;
child = (parent << 1) + 1;
}
else {
return;
}
}
}
- 我们知道对于二叉树双亲节点乘2+1就是本双亲节点的左孩子。
- 假如我现在要创建一个小堆结构,但是我的双亲节点比我两个子节点都大,那就应该有两个指针一个找到最小的孩子,一个指向双亲,然后交换。
- 先要判断参数合法性,也就是我有没有右孩子要是我的左孩子++,大于size了那就是没有右孩子了。
- 在创建堆的时候我们提供了一个函数指针这样我们就可以根据选用不同的函数进行大堆还是小堆的创建了,这就是一个回调函数,这里就是检验是否右孩子比我左孩子小,检测是然后将这个指针进行移动。
- 再往下要检验是否满足你要创建的堆的特性,假如我现在是要创建小堆双亲节点小于两个子节点,那就不交换了,但是发现我比你最小的都大那就你俩交换。
- 交换完成之后原来的parent指针移动到子节点,对这个子节点进行运算找到他的左孩子。
- 我们的一系列操作都是在循环中完成的,那什么时候结束循环呢,那就是child长于size就终止了被。
2.3.2向上调整算法
//向上调整
void HeapAdjustup(Heap* p, int child)
{
int parent = (child - 1) >> 1;
while (child) {
if (p->Comper(p->arr[child], p->arr[parent])) {
Swap(&p->arr[child], &p->arr[parent]);
child = parent;
parent = (child - 1) >> 1;
}
else {
return;
}
}
}
- 跟向下调整简直是异曲同工,你从传参就能看出来,向下调整穿的是parent,也就是你要通过基本性质,获得child这个刚好是反过来,通过child获得parent。
- 还是先判断是否满足堆,在交换,循环的出口就是root那块了
2.3.3堆的扩容
//堆的扩容
void HeapCheakCapacity(Heap* p)
{
assert(p);
//判断是否需要扩容
if (p->size == p->capacity) {
//把容量变成原来的两倍
int NewCapaty = p->capacity << 1;
//从堆上开辟新的内存空间
Datatype* temp = (Datatype*)malloc(sizeof(Datatype) * NewCapaty);
if (NULL == temp) {
assert(0);
return;
}
//把原来的数据拷贝
memcpy(temp, p->arr, sizeof(Datatype) * p->size);
//释放旧空间改变指针指向
free(p->arr);
p->arr = temp;
p->capacity;
}
}
- 这个就跟顺序表的扩容是一样的,就是开辟新空间释放旧空间,然后指针指向新空间
- 最后在进行数据更新。
2.3.4堆的创建
//堆的创建
// 创建堆的时候需要用到向下调整
void HeapCreate(Heap* p, Datatype *arr, int size, func Comper)
{
assert(p);
//动态申请内存完成堆的初始化
p->arr = (Datatype*)malloc(sizeof(Datatype) * size);
//检测是否成功开辟空间
if (NULL == p->arr) {
assert(0);
return;
}
//更新容量
p->capacity = size;
//把你要调整的数据放在你创建的堆里面
memcpy(p->arr, arr, sizeof(Datatype) * size);
p->size = size;
p->Comper = Comper;
for (int root = (size - 2) / 2; root >= 0; root--)
{
HeapAdjustDown(p, root);
}
}
- 这里就是用到了向下调整算法了,先开辟新空间,拷贝数据。
- 最后在进行数据调整,通过对size的改变进行对赋值改变。
这里用这个数组进行堆的创建
int arr[] = { 49, 27, 37, 65, 28, 34, 25, 15, 18, 19 };
2.3.5堆的插入
//插入
void HeapPush(Heap* p, Datatype data)
{
HeapCheakCapacity(p);
p->arr[p->size] = data;
p->size++;
HeapAdjustup(p, p->size - 1);
}
- 先查看是否要扩容之后在进行数据的放入。
- 放入数据都是在size位置放的,在进行向上调整
还是刚刚这个数组
int arr[] = { 49, 27, 37, 65, 28, 34, 25, 15, 18, 19 };
我插入一个99
2.3.6堆的删除
void HeapErase(Heap* p)
{
if (HeapEmpty(p)) {
return;
}
Swap(&p->arr[0], &p->arr[p->size - 1]);
p->size--;
HeapAdjustDown(p, 0);
}
- 删除堆是删除堆顶的数据。
- 先检验是否为空为空就不进行操作了
- 堆顶跟最后一个进行交换
- 在向下调整算法。
还是刚刚的数组
2.3.7堆顶元素获取
//获取堆顶元素
//因为我们是顺序表构建的堆
//那他的0号下标必是root
Datatype HeapTop(Heap* p)
{
assert(p);
return p->arr[0];
}
2.3.8堆顶长度获取
//获取堆的长度
int HeapSize(Heap* p)
{
assert(p);
return p->size;
}
2.3.9堆是否为空
//检验堆是否为空
int HeapEmpty(Heap* p)
{
assert(p);
return 0 == p->size;
}
2.3.10堆的销毁
//堆的销毁
void HeapDestroy(Heap* p)
{
assert(p);
//看是否不为空 然后就直接free 最后更新堆的数据
if (p->arr) {
free(p->arr);
p->arr = NULL;
p->capacity = 0;
p->size = 0;
}
}
以上代码用的都是跟顺序表一个套路,我在此就不赘述了,顺序表还不会写的同学可以移步这篇文章