目录
- 1、二叉树的顺序结构
- 2、堆的概念
- 3、堆的实现
- 3.1 堆实现的前提
- 3.1.1 向上调整
- 3.1.2 向下调整
- 3.2 堆实现
- 3.2.1 数据插入
- 3.2.2 数据删除
- 4、完整代码展示
- 4.1 Heap.h
- 4.2 Heap.c
- 4.3 Heaptest.c
- 5、拓展堆排序
- 6、结语
1、二叉树的顺序结构
二叉树的顺序结构是指将数据以完全二叉树的逻辑结构进行存储的结构。
但二叉树是多种多样的,为何在日常应用中很少见到普通的二叉树,且看下图中的普通二叉树与完全二叉树的展示。
上图中左侧是一个普通的二叉树,右侧是一个完全二叉树(也可称之为满二叉树),当使用顺序结构进行存储时,可以发现作图的存储存在着大量的空间浪费,而右侧的完全二叉树却没有浪费很多空间。
故在实现二叉树的顺序结构时所使用的是完全二叉树,而这一种存储方式,也称之为堆( PS. 此处的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段)。
2、堆的概念
若一组数据按照二叉树的顺序存储方式存储在一维数组中,就将其称作 “堆” 。
堆也有大堆和小堆之分:
大堆 :在二叉树中所有的父结点均比子结点所代表的数值大,称之为大堆。
小堆 :在二叉树中所有的父结点均比子结点所代表的数值小,称之为小堆。
( PS. 此处的大堆小堆是指父结点与子结点的大小关系,并不代表所存储数据在一维数组中一定有序。)
有关一维数组中如何寻找父子结点所在位置及其位置之间的关系,回看:
链接: 数据结构—二叉树相关概念【详解】【画图演示】
有关树中结点之间的关系及其概念,回看:
链接: 【树】简要理解树的概念
3、堆的实现
3.1 堆实现的前提
在日常使用中,所输入数据之间维持大堆或小堆关系的情况较少出现,所以在输入数据时,一般需要使用向下调整或向上调整算法对所输入的数据大堆或小堆化,以便更好地使用。
故在开始堆的实现前,需要先进行向上调整和向下调整算法的实现,后文命名为AdjustUp,AdjustDown。
3.1.1 向上调整
向上调整是指每一次输入数据时,从输入数据位置作为子结点,与父结点进行比较,若子结点大于父节点,进行数值上的交换(此时构建的是大堆,反之构建的为小堆),交换后将此时的父结点看作新的子结点,再向上进行交换,直至到达根节点或出现子结点小于父结点为止。
- 上图为输入28时所展现的向上调整示意图。
具体代码实现如下所示:
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
- 代码块中所展示的Swap函数是一个简单的交换变量数值的函数。
3.1.2 向下调整
向下调整的方式和向上调整的方式大致相同,以根作为父结点,与其两个子结点中最大的进行比较,若父结点所代表数值小于最大的子结点,则与之交换数值,而后以交换后的子结点作为新的父节点开启新一轮比较,直至出现父节点大于子结点的情况或到达叶子节点(此种方法所构建的是大堆,将上文中的比较词汇取反即可构建小堆)。
- 上图中所展示的是通过向下调整构建小堆的过程。
具体代码如下所示:
void AdjustDown(HPDataType* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if (child + 1 < size && a[child + 1] > a[child])//child + 1 < size 是为了避免子结点越界
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
3.2 堆实现
因堆的实践中只有核心代码需要解释,其余部分代码将会在标题 4、完整代码展示 中展示。
3.2.1 数据插入
- 结构体等相关定义展示
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HPPush(HP* php, HPDataType x);
因堆顺序结构的定义如上,且其中代表数组的是一个指向数据类型的指针,故我们需要进行数组的空间开辟,故出现了如下代码:
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("HPPush:realloc fail");
return;
}
php->a = tmp;
php->capacity = newcapacity;
}
上图代码中我们需要先判断数组中的空间是否足够,即php->size == php->capacity,若判断条件成立,则进入扩容阶段,一般在扩容时我们会进行原有数组空间大小的二倍扩容,这一依据是根据相关研究者的研究所得出的结论,并应用realloc对原数组进行扩容。
- 完整代码展示
void HPPush(HP* 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, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("HPPush:realloc fail");
return;
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
上述代码块中再插入数据后进行了向上调整,令所插入的数据形成大堆或小堆,以便后续使用。
向上调整代码及原理回看3.1.1向上调整 。
3.2.2 数据删除
数据删除阶段所删除的数据是根节点的数据,在直接删除根节点的数据后,堆中谁能作为新的根节点就成了不可避免的问题,为此可以直接将最后一位数据与第一位数据进行交换,再从根节点开始向下调整,这样就可以从堆中筛选出第二大或第二小的数据来作为新的根,并避免了不必要的麻烦。
- 完整代码展示
void HPPop(HP* php)
{
assert(php);
assert(php->size > 1);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
向下调整代码及原理回看3.1.2向下调整 。
4、完整代码展示
4.1 Heap.h
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void Swap(HPDataType* p1, HPDataType* p2);
void HPInit(HP* php);
void HPDestroy(HP* php);
void AdjustUp(HPDataType* a, int child);
void HPPush(HP* php, HPDataType x);
void AdjustDown(HPDataType* a, int size, int parent);
void HPPop(HP* php);
HPDataType HPTop(HP* php);
bool HPEmpty(HP* php);
int HPSize(HP* php);
4.2 Heap.c
#include "Heap.h"
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HPDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HPPush(HP* 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, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("HPPush:realloc fail");
return;
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
void AdjustDown(HPDataType* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if (child + 1 < size && a[child + 1] > a[child])
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HPPop(HP* php)
{
assert(php);
assert(php->size > 1);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
HPDataType HPTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
int HPSize(HP* php)
{
assert(php);
return php->size;
}
4.3 Heaptest.c
#include "Heap.h"
void test()
{
HP s;
HPInit(&s);
HPPush(&s, 123);
HPPush(&s, 12);
HPPush(&s, 1);
HPPush(&s, 321);
HPPush(&s, 32);
HPPush(&s, 3);
printf("输入数据为:123,12,1,321,32,3\n");
printf("插入数据后数组内成员展示:");
for (int i = 0; i < s.size; i++)
{
printf("%d ", s.a[i]);
}
printf("\n");
HPPop(&s);
HPPop(&s);
printf("删除数据后数组内成员展示:");
for (int i = 0; i < s.size; i++)
{
printf("%d ", s.a[i]);
}
printf("\n");
printf("此时调用判空函数后结果展示:");
if (HPEmpty(&s))
printf("yes\n");
else
printf("no\n");
printf("此时数组内成员个数:");
printf("%d\n", HPSize(&s));
printf("此时根节点的数据为:");
printf("%d\n", HPTop(&s));
}
int main()
{
test();
return 0;
}
5、拓展堆排序
堆排序的相关内容可以观看博主以往的博文:
链接: 图解堆排序【一眼看穿逻辑思路】
6、结语
十分感谢您观看我的原创文章。
本文主要用于个人学习和知识分享,学习路漫漫,如有错误,感谢指正。
如需引用,注明地址。