数据结构 之 【堆】(堆的概念及结构、大根堆的实现、向上调整法、向下调整法)(C语言实现)
目录
1.堆的概念及结构
1.1堆的概念
1.2堆的结构
2.堆的实现(以大堆为例)
2.1 前提准备
2.2初始化
2.3销毁及交换函数
2.4插入及向上调整法
向上调整法
2.5删除及向下调整法
向下调整法
2.6判空、堆顶元素、元素个数
这里的堆和操作系统虚拟进程地址空间中的堆是两回事,
一个是数据结构,一个是操作系统中管理内存的一块区域分段
1.堆的概念及结构
1.1堆的概念
一棵父亲节点的值总是不大于其子节点值的完全二叉树叫作小根堆(最小堆)
一棵父亲节点的值总是不小于其子节点值的完全二叉树叫作大根堆(最大堆),
(后续简称小堆、大堆)
1.2堆的结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费
而完全二叉树适合使用顺序结构存储,因为其每层节点从左至右连续排列,无间隔缺失
堆既然是一种完全二叉树,那就可以使用顺序结构的数组进行存储,即
按照从上至下从左至右的数组顺序对所有节点的值进行存储
这样,既不浪费空间,又能够高效率的遍历数据
由上图我们可以看到数组结构可以很好的模拟完全二叉树
注意:
逻辑结构是指我们想象出来的结构,物理结构是指数据在内存中实际存储时的结构
堆只要求了父亲节点与子节点值之间的关系,并没要求兄弟节点、堂兄弟节点值之间的关系
2.堆的实现(以大堆为例)
前面讲了二叉树的顺序存储结构更适用于完全二叉树,
所以实现堆就是在操作数组的基础上实现堆的性质
2.1 前提准备
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>typedef int HpDateType;
typedef struct heap
{HpDateType* a;int size;int capacity;}HP;
1.包含相关头文件;2.为了便于修改数据类型重命名一下
3.定义结构体,在堆上开空间来存储相关数据
2.2初始化
//初始化
void HpInit(HP* php)
{//php指向一个堆,不能为空assert(php);php->a = (HpDateType*)malloc(sizeof(HpDateType) * 4);if (php->a == NULL){perror("malloc fail");return;}php->size = 0;php->capacity = 4;
}void HpInitArray(HP* php, HpDateType* a, int n)
{assert(php);php->a = (HpDateType*)malloc(sizeof(HpDateType) * n);if (php->a == NULL){perror("malloc fail");return;}memmove(php->a, a, n * sizeof(HpDateType));php->size = n;php->capacity = n;//建堆for (int i = (n - 2) / 2; i >= 0; --i){AdjustDown(php->a, php->size, i);}
}
(1)无参初始化目的是预开一定的空间,
利用给定数组初始化目的是直接根据数组数据进行建堆(后续讲解)
(2)所给的堆结构体指针变量不应为空,所以断言一下
2.3销毁及交换函数
void HpDestroy(HP* php)
{assert(php);free(php->a);php->a = NULL;php->capacity = php->size = 0;
}
//交换
void Swap(HpDateType* p1, HpDateType* p2)
{HpDateType temp = *p1;*p1 = *p2;*p2 = temp;
}
(1)销毁时需正确释放空间并更新指针及其余变量
(2)后续频繁使用交换操作,这里单独设定一个函数
2.4插入及向上调整法
void HpPush(HP* php, HpDateType x)
{assert(php);//扩容if (php->capacity == php->size){HpDateType* temp = (HpDateType*)realloc(php->a, sizeof(HpDateType) * php->capacity * 2);if (temp == NULL){perror("malloc fail");return;}php->a = temp;php->capacity *= 2;}//实现插入,插入到末尾,实现大堆//需要依次与父亲进行比较,即向上调整php->a[php->size++] = x;AdjustUp(php->a, php->size - 1);
}
(1)插入数据第一步,思考扩容否
(2)顺序存储结构中,尾插效率更高,先将数据插入到尾部
(3)为了实现大堆,需要将所插入的数据进行调整,即AdjustUp();
向上调整法
向上调整法的前提是数据插入前本身就是一个堆
逻辑结构上,
如果所插入数据比其祖先节点的值大就进行交换,否则就不动,最终实现大堆
具体操作体现在物理结构,即数组上
通过下标间的关系,parent = ( child - 1 ) / 2 (计算机整除运算自动向下取整)
依次找到尾节点的祖先节点,然后比较数据大小,大就交换,小就停止交换
int parent = (child - 1) / 2; while (){if (a[child] > a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
这显然是一个循环!
通过前面的分析,我们很容易知道, 情况最坏是,所插入数据需要与根节点的值进行交换,自然地,就会把 parent(父亲节点下标)大于等于0作为循环结束条件,但正如上述代码所展示的一样,通过迭代更新父子节点的下标, parent恒大于等于0(最坏情况当child等于0时也成立),也就是说如果循环内部不能控制住结束条件,这将是一个 死循环
观察到, 所插入数据与根节点的值进行交换后,child 更新为 0 , parent仍等于0 那么就将 child > 0 作为循环结束条件
void AdjustUp(HpDateType* a, int child)
{//给定数组为空时,没必要进行下面步骤assert(a);int parent = (child - 1) / 2;//parent恒大于等于0while (child > 0){if (a[child] > a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}
2.5删除及向下调整法
void HpPop(HP* php)
{assert(php);//assert(!HpEmpty());//删除数据,只删除堆顶的数据Swap(&php->a[0], &php->a[php->size - 1]);php->size--;//向下调整,实现大堆AdjustDown(php->a, php->size, 0);
}
(1)数组中,尾删的效率较高,其余位置的删除操作都会因为移动数据造成效率低下
(2)堆的删除操作是针对堆顶元素的删除,原因如下:
在大堆中,最大元素位于堆顶,删除之后通过向下调整法(后续讲解) 次大元素就会位于堆顶
这样的删除更具功能性,更有利于在现有基础上直接找到最大元素
(3)具体操作就是先将堆顶元素与最后一个数据元素进行交换,然后删除堆顶元素,
最后对余下数据进行向下调整操作,使其重新成为一个大堆
向下调整法
向下调整法的前提是所调整节点的左右子树是一个堆
逻辑结构上,
如果该节点数据比其孩子节点中的较大值小就进行交换,否则就不动,最终实现大堆
具体操作体现在物理结构,即数组上
通过下标间的关系,leftchild = parent * 2 + 1 (计算机整除运算自动向下取整)
rightchild = leftchild + 1,依次找到该节点的孩子节点
然后与孩子节点中的较大值进行比较,大就交换,小就停止交换
int child = parent * 2 + 1;while () {//左孩子一个逻辑,右孩子一个逻辑,直接假设//child是左右孩子中较小的一个孩子的下标//注意右孩子的有无(防止越界访问与无效数据)if (child + 1 < n && a[child] > a[child + 1]){++child;}//与左右孩子中较大的一个孩子进行比较if (a[parent] > a[child]){Swap(&a[parent], &a[child]);parent = child;child = parent * 2 + 1;}else{break;} }
这显然是一个循环!
向下调整时,
(1)父亲节点有孩子节点时,就需要与孩子节点的值进行一次比较,所以 左孩子节点的下标小于元素个数,即 child < n 可作为循环结束条件
(2)交换操作的实质是如果父亲节点值比孩子节点中的较大值小,就进行交换,否则就不动
那么我们就先假设左孩子节点的值较大,再将左孩子节点的值与右孩子节点值进行比较,更新孩子节点,然后与父亲节点的值进行比较,从而简化比较交换操作
需要进行比较然后进行相同的操作时,先假设再判断更新的方法更好
(3)注意右孩子的有无(防止越界访问与无效数据)
if (child + 1 < n && a[child] > a[child + 1])
必须先判断右孩子的有无,再进行访问操作,否则就是越界访问或无效数据!
void AdjustDown(HpDateType* a, int n, int parent)
{int child = parent * 2 + 1;//有左孩子才进行调整while (child < n)//n是节点个数{//左孩子一个逻辑,右孩子一个逻辑,直接假设//child是左右孩子中较小的一个孩子的下标//注意右孩子的有无(防止越界访问与无效数据)if (child + 1 < n && a[child] > a[child + 1]){++child;}//与左右孩子中较大的一个孩子进行比较if (a[parent] > a[child]){Swap(&a[parent], &a[child]);parent = child;child = parent * 2 + 1;}else{break;}}
}
2.6判空、堆顶元素、元素个数
//判空
bool HpEmpty(HP* php)
{assert(php);return php->size == 0;
}
//堆顶元素
HpDateType HpTop(HP* php)
{//assert(php);//一举两得assert(!HpEmpty(php));return php->a[0];
}
//大小
int HpSize(HP* php)
{assert(php);return php->size;
}
(1)注意相关断言
(2)根据意义直接返回相应值即可