目录
什么是堆
堆是一种特树的二叉树,如果一颗二叉树满足以下条件,那么它就是一个堆:
- 堆是一颗完全二叉树。
- 堆中的所有父亲节点都大于(包括等于)或小于(包括等于)自己的孩子节点。
其中前一种堆结构我们称为大堆,后一种堆结构称为小堆。大堆的根节点称为大堆顶,小堆的根节点称为小堆顶。堆顶的数据总是堆中最大或最小的。
堆详解
堆为啥非得是完全二叉树呢?
堆一般用数组来存储,根据层序遍历的方式来存储。如果不是完全二叉树在存储时就会有空间浪费,因此堆才是完全二叉树。
你说堆一般用数组来表示,那我就非常不理解,如果用数组来表示一颗二叉树那树的节点关系那不就全乱了吗?我怎么知道谁是谁的父亲,谁是谁的孩子?
堆的存储结构在物理上是一个数组,在逻辑上是一颗二叉树。它是按照二叉树层序的方式来存储节点的,所以节点间的关系不会乱。而我们可以通过下面的公式来计算一个节点的父亲和左右孩子是谁:
- 父亲节点:(i-1)/ 2
- 左孩子节点:i*2 + 1
- 右孩子节点:i*2 + 2
注:i表示当前节点
那什么又是层序遍历呢?
如下图,就是字面意思就是一层一层的来存储节点,上一层节点是下一层节点的父亲。
堆的实现
你肯定很好奇如何将一颗普通的完全二叉树,建成一个堆,当然这句话有两种意思:建一个堆和将数组调整成一个堆。
建堆
typedef int HPDataType;
//大堆
typedef struct Heap
{
HPDataType* a; //堆
int size; //当前元素
int capacity; //当前容量
}Heap;
堆的物理结构是一个数组,size表示堆的元素个数、 capacity表示堆的容量大小
关于堆的接口
//初始化
void HeapInit(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的判空
bool HeapEmpty(Heap* hp);
//初始化
void HeapInit (Heap* hp);
//初始化
void HeapInit(Heap* hp)
{
assert(hp);
hp->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
if (NULL == hp->a)
{
perror("malloc fail");
return;
}
hp->capacity = 4;
hp->size = 0;
}
// 堆的插入
void HeapPush (Heap* hp, HPDataType x);
//向上调整
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//如果孩子不是根且孩子大于父亲就调整
while (child != 0 && a[child] > a[parent])
{
//交换
Swap(&a[child], &a[parent]);
//孩子成为父亲
child = parent;
//新孩子的父亲
parent = (child - 1) / 2;
}
}
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
//检查增容
if (hp->size == hp->capacity)
{
HPDataType* tmp = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * hp->capacity * 2);
if (NULL == tmp)
{
perror("realloc fail");
return;
}
hp->a = tmp;
}
hp->a[hp->size++] = x;
//向上调整
AdjustUp(hp->a,hp->size-1);
}
// 堆的删除
堆的插入是在数组的尾部插入的,但如果就是这样简单插入的话会破坏堆的结构。所以在插入元素后还要进行向上调整:孩子节点与父亲节点进行比较,如果孩子节点大于父亲节点就交换位置在与它的父亲节点比较,直到孩子节点不大于父亲节点或成为堆顶。
// 堆的删除
void HeapPop (Heap* hp);
//向下调整 -- 大
void AdjustLargeDown(HPDataType* a, int size, int parent)
{
//默认是左孩子
int child = (parent + 1) * 2 -1;
while (size > child)
{
//右孩子是否存在并大于左孩子
if (size > child + 1 && a[child + 1] > a[child])
{
child++;
}
//孩子大于父亲
if (a[child] > a[parent])
{
Swap(&a[child],&a[parent]);
//让父亲成为孩子
parent = child;
//让它成为它孩子
child = (parent + 1) * 2 - 1;
}
else
{
break;
}
}
}
// 堆的删除
void HeapPop(Heap* hp)
{
//hp不能为空并且堆不能为空
assert(hp && !HeapEmpty(hp));
//删除堆顶元素
Swap(&hp->a[0],&hp->a[--hp->size]);
AdjustLargeDown(hp->a,hp->size-1,0);
}
对于堆来说堆底的删除毫无意义,而堆顶的数据又是最大或最小的,所以堆删除删的是堆顶数据。但直接删除会改变堆的结构,所以是先和数组的尾部交换在删除之后进行向下调整。
向下调整:左右孩子中选出一个较大的,在于其进行比较如果小于较大的那个还孩子就进行交换,然后再去调整下一层,直到没有孩子节点为止。
其他接口
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
assert(hp);
return hp->a[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->size;
}
// 堆的判空
bool HeapEmpty(Heap* hp)
{
assert(hp);
return hp->size == 0;
}
// 堆的销毁
void HeapDestory(Heap* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->capacity = 0;
hp->size = 0;
}
堆的构建
这里的建堆的意思是将数组调整成一个堆,将数组调整成堆一般是用于堆排序。
堆的构建:使用向下调整算法,先对最后一个父亲节点进行调整,让后在对该节点减1对其下一个父亲节点进行调整。
//向下调整 -- 小
void AdjustMinorDown(int* a, int size, int parent)
{
//默认是左孩子
int child = (parent + 1) * 2 -1;
while (size > child)
{
//右孩子是否存在并小于左孩子
if (size > child + 1 && a[child + 1] < a[child])
{
child++;
}
//孩子小于父亲
if (a[child] < a[parent])
{
Swap(&a[child],&a[parent]);
//让父亲成为孩子
parent = child;
//让它成为它孩子
child = (parent + 1) * 2 - 1;
}
else
{
break;
}
}
}
// 堆的构建 --- 小
void HeapCreate(int* a, int n)
{
assert(a);
//建小堆---向下调整
for (i = (n - 2) / 2; i >= 0; i--)
{
//从最后一个父亲节点开始调整
AdjustMinorDown(a n, i);
}
}