做人要谦虚,多听听别人的意见,然后记录下来,看看谁对你有意见
一、二叉树的顺序(堆)结构及实现
1.二叉树的顺序结构
2.堆的概念及结构
3.堆的实现
3.1 向下调整算法 AdJustDown
3.2 向上调整算法 AdJustUP
3.3 堆的创建
3.3.1 向上建堆
3.3.2 向下建堆
3.3.3 堆的初始化与销毁
3.3.4 堆的插入(压栈)
3.3.5 取堆顶的数据
3.3.6 堆的删除
3.3.7 堆的数据个数
3.3.8 堆的判空
二、堆的完整实现代码
三、完结撒❀
–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀-正文开始-❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–
一、二叉树的顺序(堆)结构及实现
1.二叉树的顺序结构
堆
物理结构:数组
逻辑结构:二叉树
顺序结构存储就是使用数组来存储,普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费,而完全二叉树更适合使用顺序结构存储。
可能有些同学不太清楚普通二叉树使用数组来存储为什么会造成空间上的浪费,这里给大家讲解一下:
我们使用数组进行二叉树的存储时,父子节点之间需要满足的关系为:
parent = (child+1)/2;
leftchild = parent2-1;
rightchild = parent2+2;
ps:parent指父节点在数组中的下标位置,leftchild指左子节点在数组中的下标位置,rightchild指右子节点在数组中的下标位置
那么对于满二叉树和完全二叉树,我们按照一层一层(层序)往数组中进行存储。
举例如下图所示,大家可以按照下图简单计算验证一下父子之间是否满足父子关系:
可以知道,对于满二叉树和完全二叉树进行层序存储在数组中,按照下标计算都是满足上面所述的父子关系。
而对于普通二叉树(非满二叉树,非完全二叉树),我们依然按照上面存储进行计算
可以发现不符合父子节点之间的关系,问题在于2节点的右节点为空,而在存储时对于空节点我们并没有在数组中进行存储记录,相当于在数组中少了一个位置,那么我们如果把空节点加上,如下图:
这样就满足了父子间的关系,但是对于下标为4的位置并没有存储数据,就造成了空间浪费。
所以,对于普通二叉树我们一般使用链式结构进行存储,避免空间浪费。
现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
2.堆的概念及结构
如果有一个关键码的集合K = {K0,K1,K2,…,Kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki<=K2i+1且Ki<=K2i+2(Ki>=K2i+1且Ki>=K2i+2)i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
1.堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一棵完全二叉树;
堆的兄弟节点(同一父节点的子两个子节点)之间是不论大小的,而对于小堆其父节点的值一定小于子节点,对于大堆其父节点的值一定大于子节点。
3.堆的实现
堆初始化结构:
//堆初始化
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
在给定一个数组去实现成堆之前我们需要先了解内部实现的核心逻辑。
3.1 向下调整算法 AdJustDown
现在我们给出一个数组,逻辑上看做一颗完全二叉树。
int arr[] = {27,15,19,18,28,34,65,49,25,37}
我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。
向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
上面数组逻辑上的二叉树可画为:
可以看出来,根节点27影响了整体的小堆结构,那么我们如何将其转变为小堆呢?
(制作不是很好大家将就着看看就行)
按照上面图片显示的流程,我们在逻辑上就把之前的二叉树变成了小堆排序,而其逻辑实现思想就是向下调整。
向下调整:
再强调一边:
向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
从根开始,比较其子节点的大小,如果为大堆排序就将父节点与子节点中较大的节点交换,如果为小堆就将父节点与子节点中较小的节点交换。交换完毕继续重复以上逻辑,直到父节点大于子节点(大堆)或是父节点小于子节点(小堆)即可完成堆排序。
代码实现:
void Swap(HPDataType* px, HPDataType* py)
{
HPDataType tmp = *px;
*px = *py;
*py = tmp;
}
//向下调整O(logN)
void AdJustDown(HPDataType* a, int n, int parent)
{
//从左孩子开始,child为小孩子那个
int child = parent * 2 + 1;
while (child<n)
{
if (child + 1 < n && a[child] > a[child + 1])
{
++child;
}
if (a[child] < a[parent])//小堆<,大堆>
{
Swap(&a[parent], &a[child]);
parent = child;
child = child * 2 + 1;
}
else
{
break;
}
}
}
3.2 向上调整算法 AdJustUP
在一个二叉树的数组中如果我们尾插了一个数据,可能就导致结构不再是堆。
所以我们如果是在现有的一个堆里进行数据尾插存储,那么我们要保证数据插入后还是堆,这时一般使用向上调整算法。
下面我们给出一个数组,请画出其逻辑结构二叉树:
int arr[] = {10,15,56,25,30,70}
二叉树:
如果我将5尾插在数组当中,那么就相当于是将56节点的右孩子连了一个5节点:
这显然已经不是小堆了,调整逻辑如下:
这就是向上调整的整体逻辑:
如果是进行小堆排序,将尾节点值与其父节点的值进行比较,如果小于父节点就交换,如果进行大堆,那就判断子节点是否大于父节点,若是大于就交换。
代码实现:
void Swap(HPDataType* px, HPDataType* py)
{
HPDataType tmp = *px;
*px = *py;
*py = tmp;
}
//向上调整 O(logN)
void AdJustUP(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (1)严格来说不行
while(child>0)
{
if (a[child] < a[parent])//小堆<,大堆>
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
向下调整算法和向上调整算法的时间复杂度都为O(logN),大家感兴趣可以算一下。
3.3 堆的创建
向上调整和向下调整都是基于已经形成了堆上面,那么如果随便给一个本就不是堆的数组,我们该如何进行建堆呢?
3.3.1 向上建堆
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个小堆,现在我们通过算法,把它构建成一个小堆。
int a[] = {5,3,6,8,2,1};
根节点左右子树不是小堆,我们怎么调整呢?
这里我们从根的左孩子节点开始向上调整,根据数组存储顺序向后以此执行,直到最后一个节点为止。
其二叉树为:
调整逻辑:
从根节点的子节点开始,进行向上调整,一次调整完毕将子节点对应数组下标加1进入下一个节点进行向上调整,直到除了根的所有节点都调整完毕,二叉树便变成了堆。
代码实现:
//向上调整 O(logN)
void AdJustUP(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (1)严格来说不行
while(child>0)
{
if (a[child] < a[parent])//小堆<,大堆>
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
//初始化建堆
void HPInitArray(HP* php, HPDataType* a, int n)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
if (php->a == NULL)
{
perror("malloc fail");
return;
}
memcpy(php->a, a, sizeof(HPDataType) * n);
php->size = php->capacity = n;
//向上建堆 O(N*logN)
for (int i = 1; i < php->size; i++)
{
AdjustUp(php->a, i);
}
}
3.3.2 向下建堆
向下建堆就是根据向下调整的逻辑进行。
我们把二叉树分为其根和子树,再把子树分为其根和子树,将每一个分好的子树都进行向下调整,直到叶子节点为止。
我们还拿上面数组为例:
int a[] = {5,3,6,8,2,1};
这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成小堆。
调整逻辑:
代码实现:
//向下调整O(logN)
void AdJustDown(HPDataType* a, int n, int parent)
{
//从左孩子开始,child为小孩子那个
int child = parent * 2 + 1;
while (child<n)
{
//假设法选出左右节点中大/小的节点
if (child + 1 < n && a[child] > a[child + 1])
{
++child;
}
if (a[child] < a[parent])//小堆<,大堆>
{
Swap(&a[parent], &a[child]);
parent = child;
child = child * 2 + 1;
}
else
{
break;
}
}
}
//初始化建堆
void HPInitArray(HP* php, HPDataType* a, int n)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
if (php->a == NULL)
{
perror("malloc fail");
return;
}
memcpy(php->a, a, sizeof(HPDataType) * n);
php->size = php->capacity = n;
//向下建堆 O(N)
for (int i = (php->size - 1 - 1) / 2; i >= 0; i--)
{
AdJustDown(php->a, php->size, i);
}
}
向上建堆和向下建堆的时间复杂度分别为O(N*logN),O(N)。
因为向下建堆的时间复杂度小,所以我们在实际工作中进行建堆一般是选择向下建堆。
3.3.3 堆的初始化与销毁
在数据结构中,创建任何结构,都需要对其进行初始化和销毁。
代码实现:
//堆初始化
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
//堆销毁
void HPDestory(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
3.3.4 堆的插入(压栈)
插入一个数到数组的尾上,再进行向上调整算法,直到满足堆。
代码实现:
//向上调整 O(logN)
void AdJustUP(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (1)严格来说不行
while(child>0)
{
if (a[child] < a[parent])//小堆<,大堆>
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
//压栈 O(logN)
void HPPush(HP* php, HPDataType x)
{
assert(php);
//判断空间是否足够
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
//数据尾插向上调整
AdJustUP(php->a, php->size-1);
}
3.3.5 取堆顶的数据
在建好的堆中返回其根部数据。
代码实现:
//返回根数据
HPDataType HPTop(HP* php)
{
assert(php);
assert(php->size);
return php->a[0];
}
3.3.6 堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
代码实现:
//向下调整O(logN)
void AdJustDown(HPDataType* a, int n, int parent)
{
//从左孩子开始,child为小孩子那个
int child = parent * 2 + 1;
while (child<n)
{
if (child + 1 < n && a[child] > a[child + 1])
{
++child;
}
if (a[child] < a[parent])//小堆<,大堆>
{
Swap(&a[parent], &a[child]);
parent = child;
child = child * 2 + 1;
}
else
{
break;
}
}
}
//删除根数据O(logN)
void HPPop(HP* php)
{
assert(php);
assert(php->size);
//将根数据与最后一个子叶交换,再删除最后一个数据
Swap(&php->a[0], &php->a[php->size-1]);
php->size--;
//向下调整
AdJustDown(php->a, php->size, 0);
}
3.3.7 堆的数据个数
代码实现:
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
3.3.8 堆的判空
代码实现:
//判断堆是否为空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
二、堆的完整实现代码
Heap.h:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
//小堆
//堆初始化
void HPInit(HP* php);
//初始化建堆
void HPInitArray(HP* php, HPDataType* a, int n);
//堆销毁
void HPDestory(HP* php);
//压栈
void HPPush(HP* php, HPDataType x);
//返回根数据
HPDataType HPTop(HP* php);
//删除根数据
void HPPop(HP* php);
//堆的数据个数
int HPSize(HP* php);
//判断堆是否为空
bool HPEmpty(HP* php);
Heap.c:
#include "Heap.h"
//堆初始化
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
//堆销毁
void HPDestory(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
void Swap(HPDataType* px, HPDataType* py)
{
HPDataType tmp = *px;
*px = *py;
*py = tmp;
}
//向上调整 O(logN)
void AdJustUP(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (1)严格来说不行
while(child>0)
{
if (a[child] < a[parent])//小堆<,大堆>
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
//压栈 O(logN)
void HPPush(HP* php, HPDataType x)
{
assert(php);
//判断空间是否足够
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
//数据尾插向上调整
AdJustUP(php->a, php->size-1);
}
//返回根数据
HPDataType HPTop(HP* php)
{
assert(php);
assert(php->size);
return php->a[0];
}
//向下调整O(logN)
void AdJustDown(HPDataType* a, int n, int parent)
{
//从左孩子开始,child为小孩子那个
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] > a[child + 1])
{
++child;
}
if (a[child] < a[parent])//小堆<,大堆>
{
Swap(&a[parent], &a[child]);
parent = child;
child = child * 2 + 1;
}
else
{
break;
}
}
}
//删除根数据O(logN)
void HPPop(HP* php)
{
assert(php);
assert(php->size);
//将根数据与最后一个子叶交换,再删除最后一个数据
Swap(&php->a[0], &php->a[php->size-1]);
php->size--;
//向下调整
AdJustDown(php->a, php->size, 0);
}
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
//判断堆是否为空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//初始化建堆
void HPInitArray(HP* php, HPDataType* a, int n)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
if (php->a == NULL)
{
perror("malloc fail");
return;
}
memcpy(php->a, a, sizeof(HPDataType) * n);
php->size = php->capacity = n;
//向上建堆 O(N*logN)
/*for (int i = 1; i < php->size; i++)
{
AdjustUp(php->a, i);
}*/
//向下建堆 O(N)
for (int i = (php->size - 1 - 1) / 2; i >= 0; i--)
{
AdJustDown(php->a, php->size, i);
}
}
//建堆排序
//排序
// 升序
//小堆时间复杂度太大O(N^2),用大堆进行排序O(N*logN)
//大堆
//升序 大堆 O(N*logN)
//降序 小堆 O(N*logN)
void HeapSort(HPDataType* a, int n)
{
//根据数组直接建堆 O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdJustDown(a, n, i);
}
//交换根和尾的位置,删除尾,再向上调整 O(N*logN)
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdJustDown(a, end, 0);
--end;
}
}
三、完结撒❀
如果以上内容对你有帮助不妨点赞支持一下,以后还会分享更多编程知识,我们一起进步。
最后我想讲的是,据说点赞的都能找到漂亮女朋友❤