目录
堆的实现:——以小堆为例
typedef int HPDataType;
typedef struct Heap
{
HPDataType* val;
int size;
int capacity;
}Heap;
// 堆的构建
void HeapCreate(Heap* hp);
// 堆的销毁
void HeapDestory(Heap* hp);
void Swap(HPDataType* p1, HPDataType* p2);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
void AdjustUp(HPDataType*val,int child);
// 堆的删除
void HeapPop(Heap* hp);
void AdjustDown(HPDataType*val,int size);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
堆的定义:
typedef struct Heap
{
HPDataType* val;
int size;
int capacity;
}Heap;
因为堆的底层是数组所以定义一个数组的结构体,且看似一个顺序表,但本质上并不是顺序表,因为逻辑结构是一个完全二叉树,所以插入的要求要完全按照完全二叉树的要求来看,这里就需要用到之前完全二叉树存储到数组中后,父节点和子节点再数组中的下标关系。
堆的初始化和销毁:
注意:size是表示结点的个数,而不是数组的下标,结点的个数始终是比数组的下标大1 ,所以之后的插入操作中 size 是表示需要插入的位置下标。
// 堆的构建
void HeapCreate(Heap* hp)
{
assert(hp);
hp->capacity = hp->size = 0;
hp->val = NULL;
}
// 堆的销毁
void HeapDestory(Heap* hp)
{
assert(hp);
free(hp->val);hp->val = NULL;
hp->capacity = hp->size = 0;
}
堆的插入:
- 堆的插入和删除本质上是需要对堆的状态进行一种保持,例如,再插入前是小堆类型,那么插入后就必须是小堆类型,删除也是一样
- 所以怎么样使得堆保持再小堆状态或者是大堆状态是插入和删除需要考虑的问题。
而之前说过,堆的底层是一种数组,而数组的另一种令人熟知的结构是顺序表,那么是否能够模仿顺序表的尾插进行堆的插入操作呢?
答案是可以!
但,需要对这种操作进行一种改造
因为尾插的数据不确定,而根据小堆的特点,父节点始终需要比子节点要小,所以需要进行比较,比较后进行子节点和父节点之间的交换
但仅仅一次的交换是不够的!
因为父节点之上,还有父节点的父节点,所以为了保持堆的结构,需要自下而上的进行一次判断交换操作!
自下而上的交换操作:
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//这里的孩子指数组下标
void AdjustUp(HPDataType* val,int child)
{
int parent = (child - 1) / 2;
while (child>0)
{
if (val[parent] > val[child])
{
Swap(&val[parent],&val[child]);
child = parent;
parent= (child - 1) / 2;
}
else
{
break;
}
}
}
尾插操作: ——与顺序表的尾插一样
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
//先按小堆来插入
//判断空间是否足够
assert(hp);
if (hp->size == hp->capacity)
{
int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(hp->val, sizeof(HPDataType) * newcapacity);
assert(tmp);
hp->val = tmp;
hp->capacity = newcapacity;
}
hp->val[hp->size] = x;
hp->size++;
AdjustUp(hp->val,hp->size-1);
}
堆的删除:
删除数据,分为两种,一种是删除根结点,一种是删除最后的结点。
但是删除最后的结点其实没有任何的意义,对于堆的结构并不会发生改变,所以通常而言删除堆其实就是删除堆的根节点后,堆依旧保持大堆状态或者小堆状态。
其次,删除堆的根节点有一个误区,就是会沿用顺序表的头删操作,这样做虽然删除了根本节点,但是对于堆的结构却发生了不可逆转的变化,那就是父亲不是父亲,孩子不是孩子,兄弟不是兄弟。
要知道顺序表是一种连续的线性表结构,而堆是一个完全二叉树结构,二者的结构本质上是不一样的!
而正确的做法,是将尾的结点的元素和根结点元素进行交换,交换后再进行尾删,这样既不会使得堆的父子节点关系错乱,保证了左右子树的各个节点关系正常。
而在交换后进行尾删,达到删除的目的,之后在进行自下而上的交换,和自己的子节点自上而下的进行数值的交换,从而保证堆还是原来的小堆或者大堆。
进行交换要符合条件,也就是小堆或者大堆的父子关系特点
例如父节点要不子节点小——例子以小堆为例,因为要换小的,所以要换最小的那个孩子,所以需要左右孩子进行比较,交换的条件是判断孩子的结点下标是不是超出了数组长度的范围,超了就是叶节点表示交换停止了。
- 注意:如果不是交换左右孩子中最小的那个,那么就不是堆
自上而下的交换操作:
void AdjustDown(HPDataType* val,int size)
{
//最后child可能越界!!!
int parent = 0;
//假设法选出较小的孩子
int child = parent * 2 + 1;
while (child < size)
{
if (val[child] > val[child + 1]&&(child+1)<size)//这一步至关重要
{
child++;
}
if (val[parent] > val[child])
{
Swap(&val[parent], &val[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
尾删操作:——和顺序表的尾删一样
// 堆的删除
void HeapPop(Heap* hp)
{
assert(hp);
//默认删根节点
Swap(&hp->val[0], &hp->val[hp->size - 1]);
hp->size--;
//向下调整
AdjustDown(hp->val,hp->size);
}
获取堆顶元素:——获取根结点元素
判断堆是否存在:——判断堆的结点个数是否是零
获取堆的结点个数:
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
assert(hp);
return hp->val[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
return hp->size;
}
// 堆的判空
int HeapEmpty(Heap* hp)
{
return hp->size == 0;
}
堆的意义:
第一是堆的排序,第二是堆的top k 排行问题
堆的 top k 排行问题:
堆的top k 排行问题主要是利用了 大堆或者小堆的堆顶一定是最大或者最小值的特点,再利用了堆的删除操作,从而进行堆的一种大小排序,从而形成一种升序或者逆序的top榜单排满。
再面对大量的数据时,如果要进行排列前k个最小的数值时,可以先创建一个拥有K个结点大小的小堆,随后再进行插入,将插入的数据和栈顶元素进行比较,如果比栈顶元素小,那么替换栈顶元素,随后再和栈顶下的各个结点进行比较和交换。
这种做法到最后,形成了一种栈顶是最小的元素的结果,这种方法也相当于是一种末位淘汰制。