堆是數(shù)據(jù)結(jié)構(gòu)中的一種重要結(jié)構(gòu),了解了“堆”的概念和操作,可以快速掌握堆排序。
堆的概念
堆是一種特殊的完全二叉樹(complete binary tree)。如果一棵完全二叉樹的所有節(jié)點的值都不小于其子節(jié)點,稱之為大根堆(或大頂堆);所有節(jié)點的值都不大于其子節(jié)點,稱之為小根堆(或小頂堆)。
在數(shù)組(在0號下標存儲根節(jié)點)中,容易得到下面的式子(這兩個式子很重要):
1.下標為i的節(jié)點,父節(jié)點坐標為(i-1)/2;
2.下標為i的節(jié)點,左子節(jié)點坐標為2*i+1,右子節(jié)點為2*i+2。
堆的建立和維護
堆可以支持多種操作,但現(xiàn)在我們關心的只有兩個問題:
1.給定一個無序數(shù)組,如何建立為堆?
2.刪除堆頂元素后,如何調(diào)整數(shù)組成為新堆?
先看第二個問題。假定我們已經(jīng)有一個現(xiàn)成的大根堆。現(xiàn)在我們刪除了根元素,但并沒有移動別的元素。想想發(fā)生了什么:根元素空了,但其它元素還保持著堆的性質(zhì)。我們可以把最后一個元素(代號A)移動到根元素的位置。如果不是特殊情況,則堆的性質(zhì)被破壞。但這僅僅是由于A小于其某個子元素。于是,我們可以把A和這個子元素調(diào)換位置。如果A大于其所有子元素,則堆調(diào)整好了;否則,重復上述過程,A元素在樹形結(jié)構(gòu)中不斷“下沉”,直到合適的位置,數(shù)組重新恢復堆的性質(zhì)。上述過程一般稱為“篩選”,方向顯然是自上而下。
刪除一個元素是如此,插入一個新元素也是如此。不同的是,我們把新元素放在末尾,然后和其父節(jié)點做比較,即自下而上篩選。
那么,第一個問題怎么解決呢?
我看過的數(shù)據(jù)結(jié)構(gòu)的書很多都是從第一個非葉子結(jié)點向下篩選,直到根元素篩選完畢。這個方法叫“篩選法”,需要循環(huán)篩選n/2個元素。
但我們還可以借鑒“無中生有”的思路。我們可以視第一個元素為一個堆,然后不斷向其中添加新元素。這個方法叫做“插入法”,需要循環(huán)插入(n-1)個元素。
由于篩選法和插入法的方式不同,所以,相同的數(shù)據(jù),它們建立的堆一般不同。
大致了解堆之后,堆排序就是水到渠成的事情了。
算法概述/思路
我們需要一個升序的序列,怎么辦呢?我們可以建立一個最小堆,然后每次輸出根元素。但是,這個方法需要額外的空間(否則將造成大量的元素移動,其復雜度會飆升到O(n^2))。如果我們需要就地排序(即不允許有O(n)空間復雜度),怎么辦?
有辦法。我們可以建立最大堆,然后我們倒著輸出,在最后一個位置輸出最大值,次末位置輸出次大值……由于每次輸出的最大元素會騰出第一個空間,因此,我們恰好可以放置這樣的元素而不需要額外空間。很漂亮的想法,是不是?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
public class HeapSort { public static void main(String[] args) { int [] arr = { 50 , 10 , 90 , 30 , 70 , 40 , 80 , 60 , 20 }; System.out.println( "排序之前:" ); for ( int i = 0 ; i < arr.length; i++) { System.out.print(arr[i] + " " ); } // 堆排序 heapSort(arr); System.out.println(); System.out.println( "排序之后:" ); for ( int i = 0 ; i < arr.length; i++) { System.out.print(arr[i] + " " ); } } /** * 堆排序 */ private static void heapSort( int [] arr) { // 將待排序的序列構(gòu)建成一個大頂堆 for ( int i = arr.length / 2 ; i >= 0 ; i--){ heapAdjust(arr, i, arr.length); } // 逐步將每個最大值的根節(jié)點與末尾元素交換,并且再調(diào)整二叉樹,使其成為大頂堆 for ( int i = arr.length - 1 ; i > 0 ; i--) { swap(arr, 0 , i); // 將堆頂記錄和當前未經(jīng)排序子序列的最后一個記錄交換 heapAdjust(arr, 0 , i); // 交換之后,需要重新檢查堆是否符合大頂堆,不符合則要調(diào)整 } } /** * 構(gòu)建堆的過程 * @param arr 需要排序的數(shù)組 * @param i 需要構(gòu)建堆的根節(jié)點的序號 * @param n 數(shù)組的長度 */ private static void heapAdjust( int [] arr, int i, int n) { int child; int father; for (father = arr[i]; leftChild(i) < n; i = child) { child = leftChild(i); // 如果左子樹小于右子樹,則需要比較右子樹和父節(jié)點 if (child != n - 1 && arr[child] < arr[child + 1 ]) { child++; // 序號增1,指向右子樹 } // 如果父節(jié)點小于孩子結(jié)點,則需要交換 if (father < arr[child]) { arr[i] = arr[child]; } else { break ; // 大頂堆結(jié)構(gòu)未被破壞,不需要調(diào)整 } } arr[i] = father; } // 獲取到左孩子結(jié)點 private static int leftChild( int i) { return 2 * i + 1 ; } // 交換元素位置 private static void swap( int [] arr, int index1, int index2) { int tmp = arr[index1]; arr[index1] = arr[index2]; arr[index2] = tmp; } } |