提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
带领大家简单了解一下堆,二叉树的部分有简单了解就可以看的懂
提示:以下是本篇文章正文内容,下面案例可供参考
一、堆
堆这种数据结构其实就是“更有规矩”一点的完全二叉树
如果在一个堆中,任意一个根节点都大于等于它的子节点,则称作这个堆为大堆。
如果在一个堆中,任意一个根节点都小于等于它的子节点,则称作这个堆为小堆。
一般做题的时候为了方便在本地调试,我们可能会一个一个的开辟出节点,然后手动把它们连接在一起,但实际上我们可以写出一个函数,让它根据数组创建堆
虽然堆中的数据是以“二叉树”的逻辑顺序存储的,但是实际上堆是以数组这样的物理顺序依次存储的
这样其实可以发现,设父节点的下标为parent,左子节点的下标为child,那么child=parent*2+1,这在后面是非常重要的一个点
下面是我自己写的堆中头文件的内容:
#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 HeapSort(int* a, int n);
//输入一个拥有n个元素的数组,实现堆的构建
void HeapCreate(Heap* hp, HpDataType* a, int n);
//堆的初始化
HpDataType* HeapInit(Heap* hp);
//堆的销毁
void HeapDestory(Heap* hp);
//打印堆中元素
void HeapPrintf(Heap* hp);
//插入新元素
void HeapPush(Heap* hp,HpDataType x);
//push过程中维持小堆形态
void AdjustUp(Heap* hp,int child);
//删除堆顶元素
void HeapPop(Heap* hp);
//删除堆顶元素过程中维持大堆
void AdjustDown(Heap* hp, int n,int parent);
//返回堆顶元素
HpDataType HeapTop(Heap* hp);
//返回堆大小
int HeapSize(Heap* hp);
//堆的判空
int HeapEmpty(Heap* hp);
下面建堆算法中,我以大堆来举例,对应的是函数“AdjustDown”
1、向上/下调整
先来看一些无关紧要的函数,对堆先有一点小了解
//堆的初始化
HpDataType* HeapInit(Heap* hp)
{
assert(hp);
hp->a = NULL;
hp->size = 0;
hp->capacity = 0;
}
//堆的销毁
void HeapDestory(Heap* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
void HeapPrintf(Heap* hp)//方便调试
{
assert(hp);
int i = 0;
for (i = 0; i < hp->size; i++)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
然后是一些小例子,这是一个大堆:
直接删除堆顶数据,再按原来的顺序构建二叉树,你会发现它已经不再是堆了
按照正常规则,删除堆顶元素的以后的二叉树长这样:
所以在堆的增删过程中我们需要用函数来对整个堆进行调整
堆的删除,指的是堆顶元素的删除,也就是将数组中的数据用建堆算法排好序以后,删除堆这个二叉树“祖先节点”-也就是下标为0的数据,然后,我们需要将堆底的数据置于堆顶,(实现起来的话先”交换“后“删除”是一样的)最后需要用函数AdjustDown对堆进行调整:
//删除堆顶元素,并将堆底元素置于堆顶
void HeapPop(Heap* hp)
{
assert(hp);
int tmp = hp->a[0];
hp->a[0] = hp->a[hp->size - 1];
hp->size--;
AdjustDown(hp, hp->size, 0);
}
向下调整指的是把刚刚被挪到堆顶的数据向下调整
由于是从堆顶往堆底调整,所以首先要注意child不要超过数组的容量n;
parent这个参数主要是要在建(大)堆算法中复用,所以特意设置的
//删除堆顶元素过程中维持大堆
void AdjustDown(Heap* hp, int n, int parent)//参数parent是为了在建堆算法中复用
{
int child = 2 * parent + 1;
while (child < n)
{
保证child指向大的那个孩子
if ((child+1 < n) && (hp->a[child] < hp->a[child + 1]))
{
child++;
}
// 1、孩子大于父亲,交换,继续向下调整
// 2、孩子小于父亲,则调整结束
if (hp->a[parent] < hp->a[child])
{
int tmp = hp->a[parent];
hp->a[parent] = hp->a[child];
hp->a[child] = tmp;
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
插入数据后要进行调整,也是相似的道理
在插入新元素的时候我们选择直接在数组的后面直接插入这个数据,但如果我们想把它安放在逻辑结构的堆中的正确顺序,我们还需要把这个数组重新用函数AdjustUp调整
//插入新元素
void HeapPush(Heap* hp, HpDataType x)
{
assert(hp);
if (hp->capacity == hp->size)
{
hp->a = (HpDataType*)realloc(hp->a, 2 * (hp->capacity) * sizeof(HpDataType));
}
hp->a[hp->size] = x;
hp->size++;
AdjustUp(hp->a, hp->size, hp->size - 1);
}
向上调整指的是把新插入的元素向上调整到合适的位置
主要思想是从新插入的节点开始,如果它比父节点大,那么就交换两个节点的值,因为这个过程需要把节点和父节点“从上往下,从堆顶到堆底”进行比较,所以边界条件是child大于0
//push过程中维持小堆形态
void AdjustUp(Heap* hp, int child)
{
assert(hp);
int parent = (child - 1) / 2;
while (child > 0)
{
if (hp->a[child] > hp->a[parent])
{
int tmp = hp->a[child];
hp->a[child] = hp->a[parent];
hp->a[parent] = tmp;
child = parent;//确保循环持续运行
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
2、建堆算法
以建造大堆为例
建堆算法的规则就是:一个一个的把所有parent节点用adjustdown按照从堆底向堆顶的方向挨个跑一次
堆的构建(复杂度高)
//void HeapCreate(Heap* hp, HpDataType* arr, int n)
//{
// assert(hp);
// HeapInit(hp);
// int i = 0;
// Heap* tmp = (HpDataType*)malloc(n * sizeof(HpDataType));
// if (!tmp)
// {
// perror("HeapInit:malloc\n");
// exit(-1);
// }
// hp->a = tmp;
// hp->capacity = n;
// for (i = 0; i < n; i++)
// {
// HeapPush(hp, arr[i]);
// AdjustUp(hp, (hp->size) - 1);
// }
//}
//堆的构建
void HeapCreate(Heap* hp, HpDataType* arr, int n)
{
assert(hp);
HeapInit(hp);
int i = 0;
Heap* tmp = (HpDataType*)malloc(n * sizeof(HpDataType));
if (!tmp)
{
perror("HeapInit:malloc\n");
exit(-1);
}
hp->a = tmp;
hp->capacity = hp->size = n;//注意n就是size,所以后面求父节点时要减2才是下标
memcpy(hp->a, arr, n * sizeof(HpDataType));
for (int i = (n - 2) / 2; i >=0; i--)
{
AdjustDown(hp, n, i);
}
3.Topk问题
举个具体的例子就是“从N个数里面寻找最大的K个”这样的问题,我们既可以建大堆,取一次堆顶数据然后pop一次,
但真正高效的做法是:
建立K个数小堆,然后遍历这N个数的数组,如果遍历到的数据比堆顶的数据大,那么就把这个数据push进去,然后向下调整,遍历结束以后,留在这个小堆中的数据就是前k大的数据