選擇排序
選擇排序的基本思路是:每次都從原序列中順序查找出最小的元素,放入新的序列的下一個位置(在具體實現中,一般是還是放在原序列中,採用依次交換位置的方法)。這種最簡單實現的選擇排序時間複雜度爲。有沒有效率更高的基於選擇的排序算法呢?堆排序就是一種,與選擇排序一樣,堆排序是每次從原序列中取出最大(或最小)的元素,不同的是堆排序使用了堆這一數據結構,每次取出最大(最小)元素僅需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