详谈堆的向上调整法与向下调整法
文章目录
前言
堆有向上和向下两种调整方式,其目的都是为了调整堆,维护其原有的大堆或者小堆状态,但两中方式亦有区别,这篇文章主谈两者的对比和不同使用场景
我们知道无论是向上调整法还是向下调整法,使用的条件都是调整前堆的合法性,即调整前就是一个大堆或者小堆,所以为了保持堆原有的结构,当需要插入数据的时候我们会从堆尾插入数据或者堆顶插入,但这就涉及到场景使用问题了,不同的场景用不同的方法会截然不同
一、向上调整法及其原理
首先我们先谈谈向上调整法的原理,是怎么操作的,我们以小堆为例:
void AdjustUp(HeapDataType* a, int n, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
当我们没有堆的时候,我们就需要建堆,而堆的物理结构实际上是一个数组
在建堆的过程中,本质上我们是将数据尾插进入数组,这里用头插的话会很麻烦,因为数组的空间是连续的,如果我们头插就需要将原本数组中的元素往后移,数据量一大占用的时间复杂度将会极大,所以我们选择尾插
建堆的过程中我们尾插一个数据就向上调整一次,而当堆中没有数据的时候我们插入一个数,这个数无论是大堆还是小堆都是满足堆是合法性这个条件的,我们每次向上调整这个过程就是确保插入数据过后堆还是满足原来的大堆/小堆结构
所以尾插数据建堆的时候,我们用向上调整法
二、向下调整法
我们再来看向下调整法,同样以小堆为例,如图:
void AdjustDown(HeapDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] > a[child + 1])
{
child++;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = child * 2 + 1;
}
else
{
break;
}
}
}
向下调整法主要的主要使用场景是我们已经有一个数组,但数组内的元素并不是堆,这时我们可以用向下调整法进行调整,但我们不能从堆顶开始,而是要从最后一个非子叶到根节点依次遍历向下调整,因为向下调整要满足堆的合法性,还有一种情况就是删除堆顶元素,我们将堆顶元素和最后一个元素对换,然后进行向下调整,达到删除后的堆依然保持原有结构
四、通过建堆来对比两者时间复杂度
如图:
向下调整法的时间复杂度:N
向上调整法的时间复杂度:N*Log2(N)
所以从效率的角度上,向下调整法更优,所以我们在大多数情况下也会优先使用向下调整法