目录
本期我们将介绍一种特殊的二叉树:堆。
一.堆的概念
堆是一颗完全二叉树,一般使用数组作为底层结构,若有一个元素集合K = { k0 , k1 , k2 , ...,kn−1 } ,把它的所有元素按完全⼆叉树的顺序存储方式存储在⼀个⼀维数组中,并满足: Ki <= K2∗i+1(Ki >= K2∗i+1 且 Ki <= K2∗i+2), i = 0、1、2... ,则称为小堆(或大堆)。将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。
二.堆的性质
堆总是一颗完全二叉树,并且堆中某个结点的值总是不大于或不小于其父结点的值。
这里以大小根堆为示例,对于具有 n 个结点的完全二叉树,如果按照从上到下从左到右的数组顺序对所有结点从 0 开始编号,则对于序号为 i 的结点有:
1.若 i > 0,i 位置结点的双亲序号为 (i - 1) / 2,若 i = 0,无双亲结点。
2.若2*i + 1 < n,左孩子序号:2*i + 1,若2*i + 1 >= n,则无左孩子。
3.若2*i + 2 < n,右孩子序号:2*i + 2,若2*i + 2 >= n,则无右孩子。
三.堆的结构
由于堆的底层结构为数组,用_size记录堆中有效元素个数,_capacity来记录数组的容量
typedef int HPDataType;
typedef struct Heap
{
HPDataType* _a;
int _size;
int _capacity;
}Heap;
四.堆的实现
1.初始化
void HeapInit(Heap* php)
由于底层结构为数组,与顺序表的初始化相同。
void HeapInit(Heap* php)
{
assert(php);
php->_a = NULL;
php->_size = php->_capacity = 0;
}
2.堆的插入
void HeapPush(Heap* php, HPDataType x)
在插入前得先判断数组空间是否足够,空间不够则进行扩容,空间足够则直接插入数组尾部,并设置新容量。
if (php->_size == php->_capacity)
{
int newcapacity = php->_capacity == 0 ? 4 : 2 * php->_capacity;
HPDataType* tmp = (HPDataType*)realloc(php->_a, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("malloc");
exit(1);
}
php->_a = tmp;
php->_capacity = newcapacity;
}
以大堆为例,在这里直接插入,由于不确定x的大小,在插入后堆可能不再符合大堆的定义,所以我们需要一个方法来调整新堆的整体结构,使之成为一个大堆。
php->_a[php->_size] = x;
那么如何调整新堆使之称为一个大堆呢?在这里介绍一种算法:向上调整算法。
向上调整算法
void AdjustUp(HPDataType* arr, int child)
函数参数为指向数组空间的指针arr和当前插入的孩子结点在数组中的下标child,也就是数组最后一个元素的下标。
int parent = (child - 1) / 2;
首先,由于堆的性质我们得知了父结点的下标。
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
再对父结点和孩子结点进行比较,以大堆为例:
若孩子结点的值大于父结点的值,那么将孩子节点的父结点再数组中交换位置,由于不知道堆的高度,所以得循环操作,在一次交换完后,将孩子结点走到父结点,使之称为新的孩子结点,去和新的父结点进行比较,同时也更新父节点。
若孩子结点的值小于父节点的值,表明新堆此时已为大堆,那么直接跳出循环,当孩子结点为0时,即为根结点时终止循环。
图示:
最终数组中的数据:
完整向上调整代码:
void AdjustUp(HPDataType* arr, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
//大堆: >
//小堆: <
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
以此类推,将大于号改成小于号即可实现将新堆改为小堆的操作。
再回到插入,在执行向上调整后,此时新堆已为大堆,再将size加一就能完整实现插入操作。
完整插入代码:
void HeapPush(Heap* php, HPDataType x)
{
assert(php);
if (php->_size == php->_capacity)
{
int newcapacity = php->_capacity == 0 ? 4 : 2 * php->_capacity;
HPDataType* tmp = (HPDataType*)realloc(php->_a, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("malloc");
exit(1);
}
php->_a = tmp;
php->_capacity = newcapacity;
}
php->_a[php->_size] = x;
AdjustUp(php->_a,php->_size);
++php->_size;
}
3.堆的删除
void HeapPop(Heap* php)
在删除前首先需要对堆进行判空操作,这里单独封装一个函数,当有效数据个数size为0,则堆为空,返回非0值,当size大于0,堆不为空,返回0。
判空
int HeapEmpty(Heap* php)
{
assert(php);
return php->_size == 0;
}
在这里堆的删除是要删除堆顶的数据:
以大堆为例,与往堆中插入元素相同,若直接在数组头部删除元素,其他元素往前挪,那么堆结构被破坏,左右子树的元素可能不在原父节点下。
图示:
那么如何解决这一方法呢?在删除前我们先将堆顶元素和队中最后一个元素交换,再将堆中最后一个元素删除,这样除了根结点,堆中其他元素均符合大堆的结构。
图示:
那么现在只需让新堆调整为大堆即可,这里再介绍一种算法:向下调整算法。
向下调整算法
void AdjustDown(HPDataType* arr,int parent,int n)
函数的参数除了数组,还要求传输调整的父结点的下标parent,数组中元素个数n。
int child = 2 * parent + 1;
首先,我们先根据堆的性质,获取孩子结点的下标。
if (child + 1 < n && arr[child] < arr[child + 1])
{
child++;
}
再比较当前两个孩子结点,并选出最大的孩子结点,这里需要确保左右孩子的结点都存在,所以在比较时加上限制条件 child+1 < n。
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
在找出较大的孩子结点后,再与其父结点比较,以大堆为例:
当孩子节点的值大于父结点的值时,进行交换,由于并不知道堆的高度,所以也得循环操作,在交换完后让父结点走到当前的孩子节点,再让孩子节点走到当前父结点的左孩子结点,
当孩子结点的值小于父结点的值时,此时新堆已调整为大堆,跳出循环,循环条件为孩子结点下标小于数组元素个数。
图示:
完整向下调整代码:
//向下调整
void AdjustDown(HPDataType* arr,int parent,int n)
{
int child = 2 * parent + 1;
while (child < n)
{
//大堆: <
//小堆: >
if (child + 1 < n && arr[child] < arr[child + 1])
{
child++;
}
//大堆: >
//小堆: <
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
将判断符号改为相应的符号,即可实现小堆的向下调整,到这里新堆已变成大堆。
完整删除代码:
void HeapPop(Heap* php)
{
assert(!HeapEmpty(php));
Swap(&php->_a[0], &php->_a[php->_size - 1]);
--php->_size;
AdjustDown(php->_a,0,php->_size);
}
4.打印
堆的打印与数组相同,不再过多叙述。
void HeapPrint(Heap* php)
{
for (int i = 0; i < php->_size; i++)
{
printf("%d ", php->_a[i]);
}
printf("\n");
}
5.取堆顶的数据
判空后,返回数组首元素即可。
HPDataType HeapTop(Heap* php)
{
assert(!HeapEmpty(php));
return php->_a[0];
}
6.堆的数据个数
直接返回size。
int HeapSize(Heap* php)
{
assert(php);
return php->_size;
}
7.销毁
void HeapDestory(Heap* php)
与顺序表的销毁相同,若数组非空,将指向数组的指针释放,置空,并将size和capacity置为0。
void HeapDestory(Heap* php)
{
assert(php);
if (php->_a)
free(php->_a);
php->_a = NULL;
php->_size = php->_capacity = 0;
}
五.代码总览
Heap.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* _a;
int _size;
int _capacity;
}Heap;
void HeapInit(Heap* php);
// 堆的销毁
void HeapDestory(Heap* php);
// 堆的插入
void HeapPush(Heap* php, HPDataType x);
// 堆的打印
void HeapPrint(Heap* php);
// 堆的删除
void HeapPop(Heap* php);
// 取堆顶的数据
HPDataType HeapTop(Heap* php);
// 堆的数据个数
int HeapSize(Heap* php);
// 堆的判空
int HeapEmpty(Heap* php);
Heap.c
#include"Heap.h"
void HeapInit(Heap* php)
{
assert(php);
php->_a = NULL;
php->_size = php->_capacity = 0;
}
// 堆的销毁
void HeapDestory(Heap* php)
{
assert(php);
if (php->_a)
free(php->_a);
php->_a = NULL;
php->_size = php->_capacity = 0;
}
void HeapPrint(Heap* php)
{
for (int i = 0; i < php->_size; i++)
{
printf("%d ", php->_a[i]);
}
printf("\n");
}
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//向上调整
void AdjustUp(HPDataType* arr, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
//大堆: >
//小堆: <
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
// 堆的插入
void HeapPush(Heap* php, HPDataType x)
{
assert(php);
if (php->_size == php->_capacity)
{
int newcapacity = php->_capacity == 0 ? 4 : 2 * php->_capacity;
HPDataType* tmp = (HPDataType*)realloc(php->_a, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("malloc");
exit(1);
}
php->_a = tmp;
php->_capacity = newcapacity;
}
php->_a[php->_size] = x;
AdjustUp(php->_a,php->_size);
++php->_size;
}
// 堆的判空
int HeapEmpty(Heap* php)
{
assert(php);
return php->_size == 0;
}
//向下调整
void AdjustDown(HPDataType* arr,int parent,int n)
{
int child = 2 * parent + 1;
while (child < n)
{
//大堆: <
//小堆: >
if (child + 1 < n && arr[child] > arr[child + 1])
{
child++;
}
//大堆: >
//小堆: <
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
// 堆的删除
void HeapPop(Heap* php)
{
assert(!HeapEmpty(php));
Swap(&php->_a[0], &php->_a[php->_size - 1]);
--php->_size;
AdjustDown(php->_a,0,php->_size);
}
// 取堆顶的数据
HPDataType HeapTop(Heap* php)
{
assert(!HeapEmpty(php));
return php->_a[0];
}
// 堆的数据个数
int HeapSize(Heap* php)
{
assert(php);
return php->_size;
}
六.总结
堆中的向上调整算法和向下调整算法尤为重要,下一期我们将借助堆来实现堆排序和解决Top-k问题。