堆的实现及堆排序
前两天刷笔试题,判断一个数组的序列可以构成堆。仔细想了想,脑海里几乎已经遗忘了堆的知识,今天又重新去看书,把堆的知识总结一下。
首先堆是一种数组对象,它可以被看成一个完全二叉树。在我们常见的堆中有大堆和小堆。对大堆来说,每个父节点都大于孩子结点;小堆恰好相反。而且,大堆/小堆的每个子树也是一个大堆/小堆。
一般,我们都是用数组来存储堆的,当然根据情况,我们也可以选择用vector来存储。所以堆的结构如下。也可以很清楚看到父子节点的关系。
在学习一个新的数据结构,我们一定要很清楚怎样去创建这个数据结构。下面,我们说说如何创建一个堆(这里以大堆为例)。如上图,可以很明显看出来堆的整体形状,然后我们一般是用一个数组去构建一个堆,当然你也可以采用插入的方式去构建堆。在这里,我们用数组构建,然后讲堆的插入。
1.堆的构建
在给定的数组里,我们可以先将所有的叶子结点认为是已经符合要求的子堆,那么我们需要调整的第一个堆肯定是最后一个拥有叶子结点的父节点。如下图,即是a[4]=13这个结点。在这里也可以看出父节点和子节点的关系,child=parent*2-1。
我们建的是一个大堆,在这里已经是一个堆的大概形状了。所以我们需要调整父子关系。如果孩子大于父亲,那么交换父子。然后依次向下调整。原理比较简单,就不画图说明了 。
void _AdjustDown(int root)
{
int parent = root;
int child = parent * 2 + 1;
while (child < _a.size())
{
if (child + 1 < _a.size() && _a[child + 1] > _a[child]) //在左右孩子中找最小的
++child;
if (_a[child]>_a[parent])
{
swap(_a[child], _a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
堆化数组就是将所有的数据按照如上所示的方式一个个调整,直到所有的数据都符合大堆的规则。在这里,我所建的堆的底层是一个vector。
Heap(T* a, size_t n) //堆化数组
{
_a.reserve(n);
for (size_t i = 0; i < n; i++) //将所有的结点push进去
{
_a.push_back(a[i]);
}
//可以把所有的叶子结点看成是 一个合法的堆
//所以需要从最后一个结点的父节点开始向下调整
//child=parent*2+1;
for (int i = (_a.size() - 2 / 2); i >= 0; i--)
{
_AdjustDown(i);
}
}
2.堆的插入
上面给出的是对一个数组的堆化,那么如果我们想插入完成一个堆的创建也是可以的。当我们在一个堆中插入一个数据,毫无疑问这个数据是插入在最后的,然后我们通过调整使得堆继续满足条件。
如图,我们插入了11,这个堆已经不满足条件了,然后我们通过对堆的调整,向上调整。直到把11调整到合适的位置。
void push(const T& x) //时间复杂度为log(N)
{
_a.push_back(x);
_AdjustUp(_a.size() - 1);
}
void _AdjustUp(int child)
{
int parent = (child - 1) >> 1;
while (child > 0)
{
if (_a[child] > _a[parent])
{
swap(_a[child], _a[parent]);
child = parent;
parent = (child - 1) >> 1;
}
else
break;
}
}
3.堆的删除
既然有插入,那么必不可少的也得有删除。但是堆的删除,我们需要考虑一下怎么删。按照堆的定义,我们只能删除堆中第0个数据。如果我们直接删除堆的这个数据,这个堆的结构都发生了改变,那么我们该怎么删除呢?
在我们学习的过程中,有种方法叫做替代法,也就是说将a[0]和a[last]的数据进行交换,然后删除掉最后一个数据。删除最后一个叶子,对这个堆的结构并不会发生什么影响,紧接着我们在通过对堆进行一个调整,保证这个堆仍是一个大堆/小堆。
void pop() //删除的时间复杂度为log(N)
{
assert(_a.empty);
swap(_a[0], _a[_a.size() - 1]);
_a.pop_back();
_AdjustDown(0); //删除后需要堆化,将堆重新调整
}
4.堆排序
说了这么多堆的基础知识,那么堆在我们数据结构中的应用呢?最常见的一个就是堆排。堆排在我们平常用的概率也是蛮大的。堆排采用的思想是:先对一个数组进行建堆,如果这是要升序,我们就建一个大堆,然后将堆中最大的数据和最后一个数据进行交换,将剩下的数据继续调整为一个大堆,然后在将最大的数和剩下的数的最后一个进行交换……依次类推。
void AdjustDown(int* a, size_t n, size_t parent)
{
size_t child = parent * 2 + 1;
while (child < n)
{
if (child+1<n && a[child + 1]>a[child]) //child+1 防止越界
child++;
if (a[child] > a[parent])
{
swap(a[child], a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
void HeapSort(int* a, size_t n)
{
assert(a);
for (int i = (n - 2) / 2; i>= 0; i--)
AdjustDown(a, n, i);
int end = n - 1;
while (end>0)
{
swap(a[0], a[end]);
AdjustDown(a, end, 0);
--end;
}
}
整理堆排的整个思路,我们可以计算出堆排的时间复杂度为N*log(N)。恢复堆的时间复杂度为log(N),对数组的N个数来说,每次都需要这样操作一遍,所以时间复杂度为N * log(N)。
所有源码地址:堆的实现及堆排序