选择排序
选择排序的基本思路是:每次都从原序列中顺序查找出最小的元素,放入新的序列的下一个位置(在具体实现中,一般是还是放在原序列中,采用依次交换位置的方法)。这种最简单实现的选择排序时间复杂度为。有没有效率更高的基于选择的排序算法呢?堆排序就是一种,与选择排序一样,堆排序是每次从原序列中取出最大(或最小)的元素,不同的是堆排序使用了堆这一数据结构,每次取出最大(最小)元素仅需O(log(n))
,效率有所提高。
堆排序
排序方法非常多,这里的堆排序很好理解,就是指利用堆这种数据结构所设计的一种排序算法。具体排序思路如下:假定是升序排序,利用堆的性质,先由待排序数组构造最大堆,满足堆性质后,因为根节点是最大值,所以每次弹出根节点,即为有序数组,但这样描述不是十分严谨,比较严谨的描述如下:
- 构造最大堆。
- 交换数组中第一个元素(堆中根节点元素)和数组中最后一个元素,数组长度减一。
- 执行“下移”操作,将第一个元素“下移”到满足堆性质。
- 不断执行步骤2,直到最后仅剩一个元素。
时间复杂度分析:构造堆O(n)
,依次弹出根节点,共n
次,每次的时间复杂度O(log(n))
,所以有O(n+nlog(n))=O(nlog(n))
。空间复杂度O(1)
。
具体实现
算法描述如下:
// a为带排序数组, count元素个数
procedure heapsort(a, count)
heapify(a, count); // 构造堆
end = count - 1;
while end > 0
swap(a[end], a[0]);
end = end - 1;
siftDown(a, 0, end);
// 将数组堆化, a为待排序数组, count元素个数
procedure heapify(a, count)
// 这里元素在数组中的位置从0开始, iParent(count-1)指的是最后一个元素的父节点
start = iParent(count-1)
while start >= 0 do
siftDown(a, start, count-1)
start = start - 1 //下一个非叶子节点
// 堆根节点为start的堆进行堆化(保证父节点大于等于子节点)
procedure siftDown(a, start, end)
root = start
while iLeftChild(root) <= end
// 其实这块主要是选左右子节点中最大的一个,与之交换,代码实现有很多方法,这里只是其中一种
child = iLeftChild(root)
swap = root
if a[swap] < a[child]
swap = child
if child+1 <= end and a[swap] < a[child+1] // 如果右子节点存在且大于左子节点大于父节点,就设置swap为右子节点
swap = child + 1
if swap = root
return // 已满足堆性质,返回
else
swap(a[root], a[swap])
root = swap
具体实现代码见heapsort.cpp。
与其他排序算法的比较
堆排序经常与快排进行比较,他们的平均时间复杂度都为O(nlog(n))
,但一般情况下,快排是较堆排序快一些的,分析时比较容易忽视的一点是局部性原理,这也是cache设计的依据,堆排序相比快排对“局部性”不友好,这个从siftDown
的过程就可以看出来,其总是需要父子节点之间进行比较,当元素非常多时,父子节点在数组中的位置会相距非常大。
Reference:
Heapsort