堆排序(heapsort)的运行时间为O(n logn),是一种原地排序算法,是不稳定的排序算法。
先直观感受一下,下面就是一个堆:
20 17 8 7 16 3
什么??上面不就一个数组吗……?!
没错,(二叉)堆数据结构是一种数组对象。
不过,让我们用另外一种方式来看这个数组:
对于表示堆的数组arr[0…n-1],我们以arr[0]为根,给定某个节点下标i,令其父节点和左右后代节点的下标为:
parent(i) = (i-1)/2;
left(i) = 2*i+1;
right(i) = 2*i+2;
(具体实现时,可用移位来实现乘以2和除以2)
于是,它可以看作一棵完全二叉树:
可是,这也只是一棵完全二叉树,有啥特别之处呢?
特点就是:除根节点以外的每个节点i,都有arr[ parent(i) ] >= arr[i]。
堆分为最大堆和最小堆,上面就是最大堆,最小堆的特点则是:除根节点以外的每个节点i,都有arr[ parent(i) ] <= arr[i]。
堆排序一般使用最大堆,最大堆中的最大元素位于根节点。
因为具有n个元素的堆是基于一颗完全二叉树的,所以其高度为O(log n)。
在清楚什么是最大堆之后,我们来谈一谈如何保持堆的性质,也就是说,如果堆中有节点不满足堆的性质,我们如何进行调整。
首先,我们假定以节点i的左右儿子为根的两棵二叉树都是最大堆,而以节点i为根的二叉树可能不是最大堆,则调整的过程如下:
基于以上思想,我们用代码实现如下:
//arr[0...size-1]
void maxHeapify(int arr[], int i, int size)
{
int l = left(i);
int r = right(i);
int largest = i;
if (l < size && arr[l] > arr[largest])
largest = l;
if (r < size && arr[r] > arr[largest])
largest = r;
if (largest != i)
{
exchange(arr[i], arr[largest]);
maxHeapify(arr, largest, size);
}
}
再强调一遍,本函数的调用前提是:i的左右子树都是最大堆。
上面保持堆的性质是一个铺垫,它也是堆算法中的核心部分,后面我们将利用它完成建堆和堆排序。
我们先看看如何使用maxHeapify()来将一个数组arr[0…size-1]变成一个最大堆。
对于每一片树叶,我们都可以看作是一个只含一个元素的堆。于是对于叶子节点的父亲节点(左右子树都是最大堆),我们可以调用maxHeapify()来进行调整。调整之后,我们得到更大的堆,对于这些堆的父节点,我们又可以调用maxHeapify()来进行调整。
为保证maxHeapify()的调用前提,我们只需从最下面的非叶子节点开始调整,一直到根节点,整个堆建立完毕。
那么,最下面的非叶子节点的下标是多少?
在这里我只给出结论,有兴趣的读者可以尝试证明一下:
当用数组表示存储了n个元素的堆时,叶子节点的下标为:n/2, n/2+1, … , n-1。 (n/2表示向下取整)
于是我们的调整顺序为n/2-1, … , 0:
buildMaxHeap(int arr[], int size)
{
for (int i = size/2-1; i >= 0; --i)
maxHeapify(arr, i, size);
}
千辛万苦,终于铺垫完了,下面进行堆排序就得心应手了!
为进行原地排序,我们引入另一个变量:heap_size,它用来表示堆的大小,而用size来表示数组的大小。
于是数组arr[0…size-1]中,arr[0…heap_size-1]为堆,arr[heap_size, size-1]为排好序的元素。
由最大堆的性质可知道,arr[0]存放着堆中最大的元素,于是可以利用该性质如下排序:
代码实现:
void heapSort(int arr[], int size)
{
if (NULL == arr || size <= 0)
return ;
int heap_size = size;
buildMaxHeap(arr, heap_size);
for (int i = size-1; i >= 1; --i)
{
exchange(arr[0], arr[i]);
--heap_size;
maxHeapify(arr, 0, heap_size);
}
}
堆排序是一个很漂亮的算法,但是实际应用中,由于快排隐含的常数因子较小,往往优于堆排序,所以堆排序用的没那么多。
但是堆这个数据结构的用处很大,特别是用来实现优先队列。
这里我们只讨论基于最大堆实现的最大优先队列。
优先队列是一种用来维护由一组元素构成的集合S的数据结构,每个元素都有一个key。最大优先队列支持以下操作:
下面看看具体实现(伪代码):
//O(1)
heapMaximum(A)
{
return A[0];
}
去掉S中具有最大key的元素类似于堆排序:
//O(log n)
heapExtractMax(A)
{
if (heap_size < 1)
error "heap underflow";
max = A[0]; //用于返回
A[0] = A[heap_size-1]; //将最大值交换到尾部
--heap_size; //抛弃交换到尾部的最大值
maxHeapify(A, 0, heap_size); //O(log n)
return max;
}
将元素x的key值增加到k之后,可能违反堆的性质(这里的违反与之前的并不一样,此处key值的增加并不影响以该元素为根的堆的性质,而是影响了以其祖先节点为根的堆的性质),我们需要将该元素不断地与其父母比较,如果该元素的key较大,则交换key并继续移动,当该元素的key小于其父母时,最大堆性质成立。
//O(log n)
heapIncreaseKey(A, i, key)
{
if (key < A[i])
error "new key is smaller than current key";
A[i] = key;
while (i > 0 && A[parent(i)] < A[i])
{
exchange(A[i], A[parent(i)]);
i = parent(i);
}
}
插入一个新的元素,只需要在末端先添加一个key为负无穷的元素,然后调用heapIncreaseKey来增加其key值至x即可:
maxHeapInsert(A, x)
{
++heap_size;
A[heap_size-1] = -∞;
heapIncreaseKey(A, i, x);
}
以上为优先队列的内容~优先队列的应用这里就不多说了,操作系统中用的很多(比如作业调度)~
参考资料:《算法导论》 第六章 堆排序
每天进步一点点,Come on!
(●’◡’●)
本人水平有限,如文章内容有错漏之处,敬请各位读者指出,谢谢!