堆的概念
堆是一颗顺序存储的完全二叉树
每个结点的关键字都不大于其孩子结点的关键字,这样的堆称为小根堆
每个结点的关键字都不小于其孩子结点的关键字,这样的堆称为大根堆
对于n个元素的序列{R0, R1,R2,.......,Rn}当且仅当下列关系之一的时候,称之为堆
(1) Ri <= R2i + 1 且 Ri <= R2i + 2 (小根堆)
(2) Ri >= R2i + 1 且 Ri >= R2i + 2 (大根堆)
上图,序列R{3,8,15,31,25}是个典型的小根堆。
堆中的两个父结点3和8.
元素3在数组中以R[0]表示,它的左孩子结点是R[1],右孩子结点是R[2]。
元素8在数组中以R[1]表示,它的左孩子结点是R[3], 右孩子结点是R[4],父结点是R[0]。他们满足一下规律
设当前元素在数组中以R[i]表示,那么
(1) 它的左孩子是R[2*i+1]
(2) 它的右孩子是R[2*i+2]
(3)它的父结点是R[(i-1)/2]
重要的点:
首先,按堆的定义将数组R[0..n]调整为堆(这个过程称为创建初始堆),交换R[0]和R[n];
然后,将R[0..n-1]调整为堆,交换R[0]和R[n-1];
如此反复,直到交换了R[0]和R[1]为止。
以上思想可归纳为两个操作:
(1)根据初始数组去构造初始堆(构建一个完全二叉树,保证所有的父结点都比它的孩子结点数值大)。
(2)每次交换第一个元素跟最后一个元素,输出最后一个元素(最大值),然后把剩下元素重新调整为大根堆。
当输出完最后一个元素后,这个数组已经是按照从小到大的顺序排列了。
先通过详细的实例图来看一下,如何构建初始堆。
设有一个无序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 }。
构造了初始堆后,我们来看一下完整的堆排序处理:
还是针对前面提到的无序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 } 来加以说明。
代码:
void HeapAdjust(std::vector &a, int parent, int length)
{
int temp = a[parent];
int child = 2 * parent + 1;
//如果有有孩子结点,且有孩子结点大于左孩子结点,则选取右孩子结点
while (child < length) {
if (child + 1 < length && a[child] < a[child + 1]) {
child++;
}
//如果父结点的值已经大于孩子结点的值,则直接结束
if (temp >= a[child]) {
break;
}
a[parent] = a[child];
parent = child;
child = child * 2 + 1;
}
a[parent] = temp;
}
void Heapsort(std::vector &a)
{
for (int i = a.size()/2; i >= 0; i--) {
HeapAdjust(a, i, a.size());
}
for (int i = a.size() - 1; i > 0; i--) {
int temp = a[i];
a[i] = a[0];
a[0] = temp;
HeapAdjust(a, 0, i);
}
}
int main()
{
std::vector a;
a.push_back(1);
a.push_back(3);
a.push_back(4);
a.push_back(5);
a.push_back(2);
a.push_back(6);
a.push_back(9);
a.push_back(7);
a.push_back(8);
a.push_back(0);
Heapsort(a);
return 0;
}
时间复杂度:
堆的存储表示是顺序的。因为堆所对应的二叉树为完全二叉树,而完全二叉树通常采用顺序存储方式。
当想得到一个序列中第k个最小的元素之前的部分排序序列,最好采用堆排序。
堆排序的时间复杂度是O(n*logn)
算法稳定性:
堆排序是一种不稳定的排序方法。
因为在堆的调整过程中,关键字进行比较和交换所走的是该结点到叶子结点的一条路径,
因此对于相同的关键字就可能出现排在后面的关键字被交换到前面来的情况