1.堆的定义
- 堆(Heap)是一种数据结构,通常是一个完全二叉树。在堆中,每个节点都有一个与其相关的值,并且满足堆的性质。堆分为两种类型:大堆和小堆。
- 大堆:在大堆中,对于每个非叶子节点,其值都大于或等于它的子节点的值。也就是说,根节点的值是整个堆中的最大值。
- 小堆:与大堆相反,在小堆中,对于每个非叶子节点,其值都小于或等于它的子节点的值。根节点的值是整个堆中的最小值。
左边的这幅图就是大堆,大堆中所有的父结点都大于子结点,并且是完全二叉树。右边这幅图就是小堆,小堆所有的父结点都小于子结点。
因为堆也就是完全二叉树的性质,我们可以用数组来实现,在数组中呢,所有的数据都是按顺序排的,相比于非完全二叉树,空间浪费的情况会好很多,这也就是实现二叉树中的顺序存储。
2.代码示例
因为堆分为大堆和小堆,所以实现哪一种都行,这里我实现的是大堆。(这会影响下面的向上调整算法和向下调整算法)
typedef int HPDateType;
typedef struct HeapNode
{
HPDateType* arr;
int size;
int capacity;
}HP;
堆的结构
因为底层结构是数组,所以堆的结构就和顺序表一样,定义一个数组arr,数组中有效数据的个数size和数组的容量capacity。
void HPInit(HP* php)
{
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
堆的初始化
1.判断传入的指针是否为假
2.初始化跟顺序表也一样,数组置为空,size和capacity也都初始化为0
void HPDestory(HP* php)
{
assert(php);
if (php->arr != NULL)
{
php->arr = NULL;
php->size = php->capacity = 0;
}
}
销毁堆
1.判断传入的指针是否为假
2.判断数组此时是否为空,非空的话就将数组置为空,size和capacity置为0
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
交换函数
因后面的入堆操作会用到,所以将交换功能封装成一个函数。
因为形参相当于实参的一份临时拷贝,所以形参的改变那不会影响实参,故我们如果想要交换两个数,需要传地址过去,也就是传址调用,这样解引用过后还是实参,之后就进行交换即可。
void HPAdjustUp(HPDateType* 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;
}
}
}
向上调整算法(关键!!!)
向上调整算法和向下调整算法是实现堆的核心,所以我讲这个功能封装成一个函数。向上调整算法主要用于入堆操作,在解释这段代码之前,先解释一下这个算法是干什么的。
前提:使用向上调整算法的前提是你插入的数据会破坏堆的结构,例如:插入到尾端的数据比它的父结点要大,那么此时这颗子树就不是大堆,而整体也就不是大堆。就是不管插入还是删除数据,都必须严格遵守大堆或小堆的结构,而让数组保持大堆或小堆就要用到向上调整算法来调整数组的结构,使其发生改变后也能调整回大堆或小堆。
向上调整算法(大堆):顾名思义,就是让子结点和父结点进行比较。如果子结点比父结点要大,我们就要交换子结点和父结点的值,这样这颗子树才是大堆,之后让新的子结点,也就是之前那棵子树的父结点继续向上比较,直到子结点转移到根结点时结束,因为根结点无父结点。
在实现向上调整算法时我们要用到子结点(child)和父结点(parent)
1.因为是入堆操作,所以传入的子结点(child)就是最后一个数据,而它的父结点就是:(下标-1)/2
2.因为要不断向上比较,当子结点到根结点时就要停止,所以循环条件是child>0,然后根据入堆的数据来和它的父结点进行比较,如果小于父结点,我们就不需要调整,此时还是大堆。如果大于它的父结点,就要交换两者的值,并将child移到原父结点的位置,再根据新的child来找到新的parent以此类推
注意:传入的数据只需要数组arr和子结点的下标即可,不需要父结点的下标
void HPPush(HP* php, HPDateType x)
{
assert(php);
//判断空间是否充足
//空间不足
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDateType* tmp = (HPDateType*)realloc(php->arr, newcapacity * sizeof(HPDateType));
if (tmp == NULL)
{
perror("realloc fail");
exit(1);
}
php->arr = tmp;
php->capacity = newcapacity;
}
//空间充足
php->arr[php->size] = x;
//向上调整算法
HPAdjustUp(php->arr, php->size);
php->size++;
}
入堆
1.判断传入的指针是否为假
2.分情况讨论。空间不足时需要增容,增容操作我在线性表那一章讲过,故这里就不再赘述
3.空间充足时,直接将数据放到下标为size的位置处
4.利用向上调整算法来调整整个数组的结构
5.size++
注意:size++这步操作要放在向上调整算法之后,如果放在向上调整算法之前,那么最后一个数据的下标就是size-1,到时我们传入的child就要是size-1
void HPPrint(HP* php)
{
assert(php);
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->arr[i]);
}
printf("\n");
}
打印堆中的数据
1.判断传入的指针是否为假
2.遍历整个数组,打印出每个位置的数据
3.换行操作
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
判断堆是否为空
因为下面的出堆操作要判断堆是否为空,故将这个功能封装成一个函数。
1.判断传入的指针是否为假
2.判断size是否等于0,等于0说明数组为空,不等于0说明数组非空
void HPAdjustDown(HPDateType* arr, int parent, int n)
{
int child = parent * 2 + 1;
//默认是左子树
//循环条件是左子树不能越界访问
while (child < n)
{
//比较左子树和右子树的大小
//注意右子树不能越界访问
if (child + 1 < n && arr[child] < arr[child + 1])
{
child = child + 1;
}
//向下调整
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
向下调整算法
前提:因出堆操作是把堆顶的数据给删除掉,而在出堆操作之后,剩下的结构会被打乱,这是我们就要用向下调整算法来调整剩下的结构,使其重新调整为大堆或小堆。
向下调整算法(大堆):顾名思义,就是让父结点和子结点进行比较。因为是向下调整,所以我们要从根结点开始,首先让根结点和子结点进行比较,而在和子结点比较之前,要先比较两个子结点,也就是左子树和右子树,让较大的那个子结点和根结点进行比较,这么做是因为较大的子结点如果比根结点要大的话,那么交换两者的值之后,以根结点为父结点的子树就符合大堆的结构,所以没必要两个都比较。比较一次之后,让父结点转移到原较大子结点的位置,再找到新的子结点,再进行比较,而结束的条件是左子树要小于size,不能越界访问。
1.根据父结点的下标,让其*2+1就找到了左子树,而右子树就是左子树的下标。而默认求左子树是因为堆是完全二叉树,结点从左到右是依次排列的,如果一个父结点有右子树,那么这颗树一定有左子树,而有左子树,不一定有右子树,故默认求的是左子树
2.循环条件就是左子树的下标要小于size,进入到循环里面,先比较的就是左右子树的大小,找到大的那个,再与父结点进行比较。如果比父结点小,直接退出循环即可。如果比父结点大,就交换两者的值,然后父结点转移到原较大子结点的位置,再找到新的子结点,再进行比较,以此类推。
注意:在比较左右子树时要考虑没有右子树的情况,所以在判断左右子树的条件中要加上child+1要小于size,不能越界访问。
void HPPop(HP* php)
{
assert(!HPEmpty(php));
//出堆出的是堆顶的数据
//先将堆顶和堆底的数据进行交换
Swap(&php->arr[0], &php->arr[php->size - 1]);
php->size--;
//向下调整算法
HPAdjustDown(php->arr, 0, php->size);
}
出堆
1.判断堆是否为空
2.交换堆顶和堆底的数据
3.size--
4.调用向下调整算法
注意:size--要放在向下调整算法之前,不然的话先调用向下调整算法,会使数组变为原数组。
HPDateType HPTop(HP* php)
{
assert(php);
return php->arr[0];
}
取堆顶的数据
1.判断传入的指针是否为假
2.直接返回数组中下标为0的数据即可
注意:上面的向上调整算法和向下调整算法如果要实现小堆的话,只需要将>改为<即可。
3.堆排序
这篇既然讲到了堆,而后面我们学排序时会学堆排序,故趁热打铁直接把堆排序给讲完。
堆排序:顾名思义,就是利用堆的结构来将数组中的数据进行排序,而堆排序的时间复杂度是要比冒泡排序的时间复杂度要低的,比冒泡排序更高效。
堆排序我依旧以大堆为例。
堆排序思想:将一个随机的数组通过向下调整算法或向上调整算法,将数组的结构转变为堆结构。之后再将堆顶的数据和堆底的数据进行交换,固定好一个值,再将前面的数据调整为堆结构,再次交换堆顶和堆底的数据,一直循环,直到排好序为止。
可能会有人疑惑为什么这么做就能排序?因为堆顶的数据一定是当前数组中最大或最小的数据,再利用堆排序,每次都能把当前数组中最大或最小的数据排在后面,等排之后,数组就会变为有序数组。
void HeapSort(int* arr, int n)
{
//创建堆
//从最后一棵树开始
//利用向下调整算法创建堆
for (int i = (n - 2) / 2; i >= 0; i--)
{
HPAdjustDown1(arr, i, n);
}
//堆排序
int end = n - 1;
for (int i = end; i > 0; i--)
{
Swap1(&arr[0], &arr[end]);
HPAdjustDown1(arr, 0, end);
end--;
}
}
1.将随机数组调整为堆结构。这一步的思想是先找到最后一颗树,先对最后一颗树进行调整,再调整下一棵树,以此类推,当i=0是,也就是到了根结点,在对整棵树进行调整,这样就能将数组调整为堆结构
2.将堆顶和堆底的数据进行交换,这里我是用一个临时变量end来记录最后一个数据的下标,因为一次固定一个数据,故循环次数就是size-1,也就是end,交换之后利用向下调整算法,把前面的数据(也就相当于一个新的数组)调整为堆结构,最后让end--,以此类推,重复上述过程。
这里我用的是向下调整算法来实现堆排序,也可以用向上调整算法来实现,大家可以去试试。
通过排序我们可以发现,如果你建的是大堆,那么排序的结果就是升序,建的是小堆,排序的结果就是降序。
4.总结
堆和堆排序的关键就是要掌握向上调整算法和向下调整算法,把这两个掌握,堆和堆排序实现起来就会轻松很多,最后还是实现一个功能,检验一个功能。
标签:arr,php,堆排序,结点,算法,child,堆及,数据结构,size From: https://blog.csdn.net/2301_80236968/article/details/145267342