基本思想
- 堆的定義
n個關鍵字序列kl,k2,…,kn稱爲堆,當且僅當該序列滿足如下性質之一(簡稱堆性質):
- ki≤k2i且ki≤k2i+1 或
- ki≥k2i且ki≥k2i+1(1≤i≤FLOOR(n/2))
若將此序列所存儲的向量R[1..n]看做是一棵完全二叉樹的存儲結構,則堆實質上是滿足如下性質的完全二叉樹:樹中任一非葉結點的關鍵字均不大於(或不小於)其左右孩子(若 存在)結點的關鍵字。
- 小根堆:根結點(亦稱爲堆頂)的關鍵字是堆裏所有結點關鍵字中最小的。
- 大根堆:根結點(亦稱爲堆頂)的關鍵字是堆裏所有結點關鍵字中最大的。
我們可以選擇大根堆或者小根堆中的任意一個來進行排序。
- 排序思想
用大根堆排序的基本思想:
- 先將初始文件R[1..n]建成一個大根堆,此堆爲初始的無序區。
- 再將關鍵字最大的記錄R[1](即堆頂)和無序區的最後一個記錄R[n]交換,由此得 到新的無序區R[1..n-1]和有序區R[n],且滿足R[1..n-1].keys≤R[n].key。
- 由於交換後新的根R[1]可能違反堆性質,故應將當前無序區R[1..n-1]調整爲堆。 然後再次將R[1..n-1]中關鍵字最大的記錄R[1]和該區間的最後一個記錄R[n-1]交換,由 此得到新的無序區R[1..n-2]和有序區R[n-1..n],且仍滿足關係 R[1..n-2].keys≤R[n-1..n].keys,同樣要將R[1..n-2]調整爲堆。
算法實現
堆排序算法,Java實現,代碼如下所示:
01 |
public abstract class Sorter
{ |
02 |
public abstract void sort( int []
array); |
03 |
} |
04 |
05 |
public class HeapSorter extends Sorter
{ |
06 |
07 |
public void sort( int []
array) { |
08 |
heapSort(array); |
09 |
} |
10 |
11 |
/** |
12 |
*
<p>堆排序方法 |
13 |
*
<p>基於大根堆的堆排序方法 |
14 |
*/ |
15 |
private void heapSort( int []
array) { |
16 |
Integer
tmp; //
用於交換的暫存單元 |
17 |
buildHeap(array); //
執行初始建堆,並調整 |
18 |
for ( int i
= 0 ;
i < array.length; i++) { |
19 |
//
交換堆頂元素array[0]和堆中最後一個元素array[array.length-1-i] |
20 |
tmp
= array[ 0 ]; |
21 |
array[ 0 ]
= array[array.length - 1 -
i]; |
22 |
array[array.length
- 1 -
i] = tmp; |
23 |
//
每次交換堆頂元素和堆中最後一個元素之後,都要對堆進行調整 |
24 |
adjustHeap(array, 0 ,
array.length - 1 -
i); |
25 |
} |
26 |
} |
27 |
28 |
/** |
29 |
*
<p> |
30 |
*
建堆方法 |
31 |
*
<p> |
32 |
*
調整堆中0~array.length/2個結點,保持堆的性質 |
33 |
* |
34 |
*/ |
35 |
private void buildHeap( int []
array) { |
36 |
//
求出當前堆中最後一個存在孩子結點的索引 |
37 |
int pos
= (array.length - 1 )
/ 2 ; |
38 |
//
從該結點結點開始,執行建堆操作 |
39 |
for ( int i
= pos; i >= 0 ;
i--) { |
40 |
adjustHeap(array,
i, array.length); //
在建堆過程中,及時調整堆中索引爲i的結點 |
41 |
} |
42 |
} |
43 |
44 |
/** |
45 |
*
<p> |
46 |
*
調整堆的方法 |
47 |
* |
48 |
*
@param s 待調整結點的索引 |
49 |
*
@param m 待調整堆的結點的數量(亦即:排除葉子結點) |
50 |
*/ |
51 |
private void adjustHeap( int []
array, int s, int m)
{ |
52 |
Integer
tmp = array[s]; //
當前待調整的結點 |
53 |
int i
= 2 *
s + 1 ; //
當前待調整結點的左孩子結點的索引(i+1爲當前調整結點的右孩子結點的索引) |
54 |
while (i
< m) { |
55 |
if (i
+ 1 <
m && array[i] < array[i + 1 ])
{ //
如果右孩子大於左孩子(找到比當前待調整結點大的孩子結點) |
56 |
i
= i + 1 ; |
57 |
} |
58 |
if (array[s]
< array[i]) { |
59 |
array[s]
= array[i]; //
孩子結點大於當前待調整結點,將孩子結點放到當前待調整結點的位置上 |
60 |
s
= i; //
重新設置待調整的下一個結點的索引 |
61 |
i
= 2 *
s + 1 ; |
62 |
} else { //
如果當前待調整結點大於它的左右孩子,則不需要調整,直接退出 |
63 |
break ; |
64 |
} |
65 |
array[s]
= tmp; //
當前待調整的結點放到比其大的孩子結點位置上 |
66 |
} |
67 |
} |
68 |
} |
堆排序算法,Python實現,代碼如下所示:
01 |
class Sorter: |
02 |
''' |
03 |
Abstract
sorter class, which provides shared methods being used by |
04 |
subclasses. |
05 |
''' |
06 |
__metaclass__ = ABCMeta |
07 |
|
08 |
@abstractmethod |
09 |
def sort( self ,
array): |
10 |
pass |
11 |
12 |
class HeapSorter(Sorter): |
13 |
''' |
14 |
Heap
sorter |
15 |
''' |
16 |
def sort( self ,
array): |
17 |
length = len (array) |
18 |
self .__heapify(array) |
19 |
i = 0 |
20 |
while i<length: |
21 |
array[ 0 ],
array[length - 1 - i] = array[length - 1 - i],
array[ 0 ] |
22 |
self .__sift_down(array, 0 ,
length - 1 - i) |
23 |
i = i + 1 |
24 |
|
25 |
def __heapify( self ,
array): |
26 |
length = len (array) |
27 |
pos = (length - 1 ) / / 2 |
28 |
i = pos |
29 |
while i> = 0 : |
30 |
self .__sift_down(array,
i, length) |
31 |
i = i - 1 |
32 |
|
33 |
def __sift_down( self ,
array, s, m): |
34 |
tmp = array[s] |
35 |
i = 2 * s + 1 |
36 |
while i<m: |
37 |
if i + 1 <m and array[i]<array[i + 1 ]: |
38 |
i = i + 1 |
39 |
if array[s]<array[i]: |
40 |
array[s] = array[i] |
41 |
s = i |
42 |
i = 2 * s + 1 |
43 |
else : |
44 |
break |
45 |
array[s] = tmp |
排序過程
假設待排序數組爲array = {94,12,34,76,26,9,0,37,55,76,37,5,68,83,90,37,12,65,76,49},數組大小爲20。
第一步:初始建堆
首先執行的初始建堆(在建堆的過程中需要調整堆)。過程如下:
- 求出當前堆中最後一個存在孩子結點的索引
這裏,把數組array看做是一棵完全二叉樹,這樣數組每個索引位置上的元素都對應到二叉樹中的結點,如圖所示:
其中需要在這棵樹中找到最後一個有孩子最大的一個結點的索引:
pos = (array.length-1)/2 = (20-1)/2 = 9
也就是索引爲9的array[9] = 76,由後至前層次遍歷,從array[9]一直到array[0],對初始堆進行調整。
- 對初始堆進行調整
-
調整結點array[9] = 76:
先比較array[9] = 76的左右孩子:s = 9,i = 2*s+1 = 2*9 + 1 = 19,而i+1 = 19 + 1 = 20 > m = array.length-1 = 20 -1 = 19(array[9] = 76沒有右孩子),只需要將array[9] = 76與array[i] = array[19] = 49比較,因爲array[9] = 76>array[i] = array[19] = 49,則不需要交換array[9] = 76與array[i] = array[19] = 49,繼續對下一個結點(也就是array[8] = 55)進行調整;
-
調整結點array[8] = 55:
先比較array[8] = 55的左右孩子:s = 8,i = 2*s+1 = 2*8 + 1 = 17,,而i+1 = 17 + 1 = 18 < m = array.length-1 = 20-1 = 19(array[8] = 55存在右孩子),左孩子array[i] = array[17] = 65小於右孩子array[i+1] = array[18] = 76,只需要將array[8] = 76與右孩子array[i+1] = array[18] = 76比較,因爲array[8] = 55<array[i+1] = array[18] = 76,則需要交換array[8] = 55與array[i+1] = array[18] = 76,交換後如圖所示:
繼續對下一個結點(也就是array[8] = 55)進行調整; -
調整結點array[7] = 37:
顯然,不需要交換;
-
調整結點array[6] = 0:
調整結果如圖所示:
-
調整結點array[5] = 9:
調整結果如圖所示:
-
調整結點array[4] = 26:
調整結果如圖所示:
-
調整結點array[3] = 76:
顯然,不需要交換。
-
調整結點array[2] = 34:
調整結果如圖所示:
-
調整結點array[1] = 12:
調整結果如圖所示:
-
調整結點array[0] = 94:
顯然,不需要交換。
至此,對初始堆的調整完成。
第二步:第一次交換
將堆頂元素與最後一個元素交換,即array[0] = 94與最後一個元素array[19] = 49交換,如圖所示:
此時,數組爲:
array = {49,76,90,12,76,68,34,37,76,26,37,5,9,83,0,37,12,65,55,94}
數組中最大的元素被交換到了數組的末尾,也就是array[19] = 94是最終排好序的固定位置。
第三步:調整堆
過程同前面類似。
……
最後經過堆排序得到有序的數組。
算法分析
- 時間複雜度
堆排序的時間,主要由建立初始堆和反覆重建堆這兩部分的時間開銷構成。
堆排序的最壞時間複雜度爲O(nlgn)。堆排序的平均性能較接近於最壞性能。由於建初始堆所需的比較次數較多,所以堆排序不適宜於記錄數較少的文件。
- 空間複雜度
堆排序過程中,需要調整堆,交換待排序記錄需要一個臨時存儲單元,所以空間複雜度爲O(1)。
- 排序穩定性
堆排序是就地排序,它是不穩定的排序方法。
轉載鏈接:http://shiyanjun.cn/archives/802.html