1. 堆的概念及结构
堆的性质:
堆中某个节点的值总是不大于其父节点的值(大根堆)或不小于其父节点的值(小根堆);
堆总是一棵 完全二叉树。
通过上面的内容我们知道堆是一种顺序存储的二叉树,而且堆还总是一棵完全二叉树。
堆分为小根堆和大根堆:
2. 堆的实现
2.1两种算法
堆的实现有两种算法,一种是向下调整,另一种是向上调整,以实现大根堆为例。
对于向下调整,现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个大堆。向下调整算法有一个前提:左右子树必须是一个堆(此例中左右子树必须是一个大堆),才能调整。
对于向上调整,现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们只要将要插入堆的数据通过向上调整就可以把它调整成一个大堆。向上调整算法有一个前提:除了要插入的数据,其它的数据已经构成了一个大堆,这样才能调整。
虽然这俩种算法都可以实现堆的结构,但实际中却选择了向下调整的算法,这是因为向下调整算法的时间复杂度更优,那接下来让我们一起来看看他们的时间复杂度。
2.2堆的创建
2.2.1向下调整创建大堆
下面我们给出一个数组,这个数组逻辑上可以看做一棵完全二叉树,但是还不是一个大堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点(也就是最后一个结点的父节点)的子树开始调整,这样它的左右子树一定已经满足堆的条件了,这样一直调整到二叉树的根节点,就可以调整成堆。
建堆时间复杂度:
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果)
代码实现:
//交换两个元素
void Swap(int* a, int* b)
{
int tmp = 0;
tmp = *a;
*a = *b;
*b = tmp;
}
//向下调整
void AdjustDown(int* a, int n, int parent)
{
//先假设当前待调整结点的左孩子结点存在
//并且是待调整结点的左右孩子结点(不管右孩子结点存不存在,都这样假设)中值最大的
int child = parent * 2 + 1;
child < n 说明左孩子结点确实存在。
while (child < n)
{
//child+1 < n 说明右孩子结点确实存在
//如果a[child] < a[child+1]也成立,那说明左右孩子结点中值最大的是右孩子结点
if ((child + 1 < n) && a[child] < a[child + 1])
{
child = child + 1;
}
//如果a[child]>a[parent],则说明父节点比比左右孩子节点的值都要小,要置换
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = child * 2 + 1;
}
//如果a[child] <= a[parent],那就不需要进行调整
else
{
break;
}
}
}
//建大堆
//a接收的是数组的地址,n是数组的长度
void HeapCreat(int* a, int n)
{
//向下调整是要将该结点去和它的左右子树进行比较,从而移动到适当的位置
//所以当前结点相当于父节点。
int parent = (n - 1 - 1) / 2;
for (int i = parent; i >= 0; i--)
{
//i是待调整的当前结点
AdjustDown(a, n, i);
}
}
2.2.2向上调整创建大堆
下面我们给出一个数组,这个数组逻辑上可以看做一棵完全二叉树,但是还不是一个大堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从第二个节点开始向上调整,这样一直调整到二叉树的最后一个结点,就可以调整成堆。
建堆时间复杂度:
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果)
代码实现:
//交换
void Swap(int* a, int* b)
{
int tmp = 0;
tmp = *a;
*a = *b;
*b = tmp;
}
//向上调整
void AdjustUp(int* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0) //用parent>=0取判断,虽然结果是一样的,但逻辑有问题
{
//如果a[child] > a[parent],则需要进行调整
if (a[child] > a[parent])
{
Swap(&a[parent], &a[child]);
child = parent;
parent = (parent - 1) / 2;
}
//如果a[child] <= a[parent],则不需要进行调整
else
{
break;
}
}
}
//建大堆
//a接收的是数组的地址,n是数组的长度
void HeapCreat(int* a, int n)
{
//向上调整时从第二个结点开始的
int child = 1 ;
for (int i = 1 ; i < n ; i++)
{
AdjustUp(a, n, i);
}
}
2.2.3最终算法选择
通过上面这种算法的比较,我们发现向下调整的时间复杂度优于向上调整的时间复杂度,所以我们选择通过向下调整算法来创建堆。
2. 总结
只要我们掌握了堆的创建,那对于很多和堆有关的知识学起来就更容易了。后面还会学到关于堆的应用,就会用到很多这一节的知识。