Heapsort (堆排序)是最經典的排序算法之一,在google或者百度中搜一下能夠搜到不少很是詳細的解析。一樣好的排序算法還有quicksort(快速排序)和merge sort(歸併排序),選擇對這個算法進行分析主要是由於它用到了一個很是有意思的算法技巧:數據結構 - 堆。並且堆排實際上是一個看起來複雜其實並不複雜的排序算法,我的認爲heapsort在機器學習中也有重要做用。這裏從新詳解下關於Heapsort的方方面面,也是爲了本身鞏固一下這方面知識,有可能和其餘的文章有不一樣的入手點,若有錯誤,還請指出。文中引用的referecne會再結尾標註。java
p.s. 我的認爲所謂詳解是你在看相關wiki或者算法書看不懂的時候看通俗易懂的解釋,不過最佳方案仍是去看教授們的講解,推薦reference[1]中的heapsort章節。python
以上是廢話,能夠不看算法
Section 1 - 簡介api
Heapsort是一個comparison-based的排序算法(快排,歸併,插入都是;counting sort不是),也是一種選擇排序算法(selection sort),一個選擇算法(selection algorithm)的定義是找到一個序列的k-th order statistic(統計學中的術語),直白的說就是找到一個list中第k-th小的元素。以上均可以大不用懂,heapsort都理解了回來看一下是這回事就是了。一樣,插值排序也是一種選擇排序算法。數組
Heapsort的時間複雜度在worst-case是\(O(nlgn)\),average-case是\(O(nlgn)\);空間複雜度在worst-case是\(O(1)\),也就是說heapsort能夠in-place實現;heapsort不穩定。數據結構
如下順便附上幾種排序算法的時間複雜度比較(\(\Theta-notation\)比\(O-notation\)更準確的定義了漸進分析(asymptotic analysis)的上下界限,詳細瞭解能夠自行google):dom
Algorithm | Worst-case機器學習 |
Average-case/expected ide |
Insertion sort(插值排序)函數 |
\(\Theta (n^2)\) | \(\Theta (n^2)\) |
Merge sort(歸併排序) |
\(\Theta (nlgn)\) | \(\Theta (nlgn)\) |
Heapsort(堆排序) | \(O(nlgn)\) | \(O(nlgn)\) |
Quicksort(快速排序) | \(\Theta (n^2)\) | \(\Theta (n^2)\) (expected) |
*Additional Part - KNN
heapsort在實踐中的表現常常不如quicksort(儘管quicksort最差表現爲 \(\Theta (n^2)\),但quicksort 99%狀況下的runtime complexity爲 \(\Theta (nlgn)\)),但heapsort的\(O(nlgn)\)的上限以及固定的空間使用常常被運做在嵌入式系統。在搜索或機器學習中常常也有重要的做用,它能夠只返回k個排序須要的值而無論其餘元素的值。例如KNN(K-nearest-neighbour)中只需返回K個最小值便可知足需求而並不用對全局進行排序。固然,也可使用divide-and-conquer的思想找最大/小的K個值,這是一個題外話,之後有機會作一個專題比較下。
如下程序爲一個簡單的在python中調用heapq進行heapsort取得k個最小值,能夠大概體現上面所述的特性:
1 ''' 2 Created On 15-09-2014 3 4 @author: Jetpie 5 6 ''' 7 8 9 import heapq, time 10 import scipy.spatial.distance as spd 11 import numpy as np 12 13 pool_size = 100000 14 15 #generate an 3-d random array of size 10,000 16 # data = np.array([[2,3,2],[3,2,1],[2,1,3],[2,3,2]]) 17 data = np.random.random_sample((pool_size,3)) 18 #generate a random input 19 input = np.random.random_sample() 20 #calculate the distance list 21 dist_list = [spd.euclidean(input,datum) for datum in data] 22 23 #find k nearest neighbours 24 k = 10 25 26 #use heapsort 27 start = time.time() 28 heap_sorted = heapq.nsmallest(k, range(len(dist_list)), key = lambda x: dist_list[x]) 29 print('Elasped time for heapsort to return %s smallest: %s'%(k,(time.time() - start))) 30 31 #find k nearest neighbours 32 k = 10000 33 34 #use heapsort 35 start = time.time() 36 heap_sorted = heapq.nsmallest(k, range(len(dist_list)), key = lambda x: dist_list[x]) 37 print('Elasped time for heapsort to return %s smallest: %s'%(k,(time.time() - start)))
運行結果爲:
Elasped time for heapsort to return 10 smallest: 0.0350000858307 Elasped time for heapsort to return 10000 smallest: 0.0899999141693
Section 2 - 算法過程理解
2.1 二叉堆
在「堆排序」中的「堆」一般指「二叉堆(binary heap)」,許多不正規的說法說「二叉堆」其實就是一個徹底二叉樹(complete binary tree),這個說法正確但不許確。但在這基礎上理解「二叉堆」就很是的容易了,二叉堆主要知足如下兩項屬性(properties):
#1 - Shape Property: 它是一個徹底二叉樹。
#2 - Heap Property: 主要分爲max-heap property和min-heap property(這就是我之前說過的術語,很重要)
|--max-heap property :對於全部除了根節點(root)的節點 i,\(A[Parent] \geq A[i]\)
|--min-heap property :對於全部除了根節點(root)的節點 i,\(A[Parent] \leq A[i]\)
上圖中的兩個二叉樹結構均是徹底二叉樹,但右邊的纔是知足max-heap property的二叉堆。
在如下的描述中,爲了方便,咱們仍是用堆來講heapsort中用到的二叉堆。
2.2 一個初步的構想
有了這樣一個看似簡單的結構,咱們能夠產生如下初步構想來對數組A作排序:
1.將A構建成一個最大堆(符合max-heap property,也就是根節點最大);
2.取出根節點(how?);
3.將剩下的數組元素在建成一個最大二叉堆,返回第2步,直到全部元素都被取光。
若是已經想到了以上這些,那麼就差很少把heapsort完成了,剩下的就是怎麼術語以及有邏輯、程序化的表達這個算法了。
2.3 有邏輯、程序化的表達
一般,heapsort使用的是最大堆(max-heap)。給一個數組A(咱們使用 Java序列[0...n]),咱們按順序將它初始化成一個堆:
Input:
Initialization:
*堆的根節點(root)爲A[0];
對這個堆中index爲\(i\)的節點,咱們能夠獲得它的parent, left child and right child,有如下操做:
Parent(i): \(parent(i)\gets A[floor((i-1)/2)]\)
Left(i): \(left(i)\gets A[2*i + 1]\)
Right(i): \(right(i)\gets A[2*i + 2]\)
經過以上操做,咱們能夠在任意index-\(i\)獲得與其相關的其餘節點(parent/child)。
在heapsort中,還有三個很是重要的基礎操做(basic procedures):
Max-Heapify(A , i): 維持堆的#2 - Heap Property,別忘了在heapsort中咱們指的是max-heap property(min-heap property一般是用來實現priority heap的,咱們稍後說起)。
Build-Max-Heap(A): 顧名思義,構建一個最大堆(max-heap)。
Heapsort(A): 在Build-Max-Heap(A)的基礎上實現咱們2.2構想中得第2-3步。
其實這三個操做每個都是後面操做的一部分。
下面咱們對這三個很是關鍵的步驟進行詳細的解釋。
Max-Heapify(A , i)
+Max-Heapify的輸入是當前的堆A和index-\(i\),在實際的in-place實現中,每每須要一個heapsize也就是當前在堆中的元素個數。
+Max-Heapify有一個重要的假設:以Left(\(i\))和Right(\(i\))爲根節點的subtree都是最大堆(若是樹的知識很好這裏就很好理解了,但爲何這麼假設呢?在Build-Max-Heap的部分會解釋)。
+有了以上的輸入以及假設,那麼只要對A[i], A[Left(i)]和A[Right(i)]進行比較,那麼會產生兩種狀況:
-第一種,最大值(\(largest\))是A[i],那麼基於以前的重要假設,以\(i\)爲根節點的樹就已經符合#2 - Heap Property了。
-第二種,最大值(\(largest\))是A[Left(i)]或A[Right(i)],那麼交換A[i]與A[\(largest\)],這樣的結果是以\(largest\)爲根節點的subtree有可能打破了#2 - Heap Property,那麼對以\(largest\)爲根節點的樹進行Max-Heapify(A, largest)的操做。
+以上所述的操做有一個形象的描述叫作A[i] 「float down", 使以\(i\)爲根節點的樹是符合#2 - Heap Property的,如下的圖例爲A[0] 」float down"的過程(注意,以A[1]和A[2]爲根節點的樹均是最大堆)。
如下附上reference[1]中的Psudocode:
1 MAX-HEAPIFY(A, i) 2 l = LEFT(i) 3 r = RIGHT(i) 4 if <= heapsize and A[l] > A[i] 5 largest = l 6 else largest = i 7 if r <= heapsize and A[r] > A[largest] 8 largest = r 9 if not largest = i 10 exchange A[i] with a[largest] 11 MAX-HEAPIFY(A, largest)
Build-Max-Heap(A)
先附上reference[1]中的Psudocode(作了部分修改,這樣更明白),由於很是簡單:
1 BUILD-MAX-HEAP(A) 2 heapsize = A.length 3 for i = PARENT(A.length-1) downto 0 4 MAX-HEAPIFY(A , i)
+Build-Max-Heap首先找到最後一個有子節點的節點 \(i = PARENT(A.length -1)\) 做爲初始化(Initialization),由於比 i 大的其餘節點都沒有子節點了因此都是最大堆。
+對 i 進行降序loop並對每一個 i 都進行Max-Heapify的操做。因爲比 i 大的節點都進行過Max-Heapify操做並且 i 的子節點必定比 i 大, 所以符合了Max-Heapify的假設(以Left(\(i\))和Right(\(i\))爲根節點的subtree都是最大堆)。
下圖爲對咱們的輸入進行Build-Max-Heap的過程:
Heapsort(A)
到如今爲止咱們已經完成了2.2中構想的第一步,A[0]也就是root節點是數組中的最大值。若是直接將root節點取出,會破壞堆的結構,heapsort算法使用了一種很是聰明的方法。
+將root節點A[0]和堆中最後一個葉節點(leaf)進行交換,而後取出葉節點。這樣,堆中除了以A[0]爲root的樹破壞了#2 - Heap Property,其餘subtree仍然是最大堆。只需對A[0]進行Max-Heapify的操做。
+這個過程當中將root節點取出的方法也很簡單,只需將\(heapsize\gets heapsize -1\)。
下面是reference[1]中的Psudocode:
1 HEAPSORT(A): 2 BUILD-MAX-HEAP(A) 3 for i = A.length downto 1 4 exchange A[0] with A[i] 5 heapsize = heapsize -1 6 MAX-HEAPIFY(A , 0)
到此爲止就是整個heapsort算法的流程了。注意,若是你是要閉眼睛也能寫出一個堆排,最好的方法就是理解以上六個重要的操做。
Section 3 - runtime複雜度分析
這一個section,咱們對heapsort算法過程當中的操做進行復雜度分析。
首先一個總結:
而後咱們分析一下爲何是這樣的。在如下的分析中,咱們所指的全部節點\(i\)都是從1開始的。
Max-Heapify
這個不難推導,堆中任意節點 i 到葉節點的高度(height)是\(lgn\)。要專業的推導,能夠參考使用master theorem。
Build-Max-Heap
在分析heapsort複雜度的時候,最有趣的就是這一步了。
若是堆的大小爲\(n\),那麼堆的高度爲\(\lfloor lgn\rfloor\);
對於任意節點\(i\),\(i\)到葉節點的高度是\(h\),那麼高度爲\(h\)的的節點最多有\(\lceil n /2^{h+1}\rceil\)個,下面是一個大概的直觀證實:
-首先,一個大小爲\(n\)的堆的葉節點(leaf)個數爲\(\lceil n/2\rceil\):
--還記不記得最後一個有子節點的節點parent(length - 1)是第\(\lfloor n/2\rfloor\)(注意這裏不是java序號,是第幾個),由此可證葉節點的個數爲n - \(\lfloor n/2\rfloor\);
-那麼若是去掉葉節點,剩下的堆的節點個數爲\(n - \lceil n/2\rceil = \lfloor n/2\rfloor\),這個新樹去掉葉節點後節點個數爲\(\lfloor \lfloor n/2\rfloor /2\rfloor\) ;
-(這須要好好想想)以此類推,最後一個樹的葉節點個數即爲高度爲\(h\)的節點的個數,必定小於\(\lceil (n/2)/2^h\rceil\),也就是\(\lceil n/2^{h+1}\rceil\)。
對於任意節點\(i\),\(i\)到葉節點的高度是\(h\),運行Max-Heapify所須要的時間爲\(O(h)\),上面證實過。
那麼Build-Max-Heap的上限時間爲(參考reference[1]):
$\sum_{h=0}^{\lfloor lgn\rfloor } \lceil \frac{n}{2^{h+1}}\rceil O(h) = O\left(n\sum_{h=0}^{\lfloor lgn\rfloor }\frac{h}{2^h}\right)$
根據如下定理:
$\sum_{k=0}^{\infty } kx^k = \frac{x}{(1-x)^2} for \quad |x| < 1$
咱們用$x = \frac{1}{2}$替換求和的部分獲得:
$\sum_{h=0}^{\infty } \frac{h}{2^h} = \frac{1/2}{(1-1/2)^2} = 2$
綜上所述,咱們能夠求得:
$O\left(n\sum_{h=0}^{\lfloor lgn\rfloor }\frac{h}{2^h}\right) = O\left(n\sum_{h=0}^{\infty}\frac{h}{2^h}\right) = O(2n) = O(n)$
Heapsort
因爲Build-Max-Heap複雜度爲$O(n)$,有n-1次調用Max-Heapify(複雜度爲$O(lgn)$),全部總的複雜度爲$O(nlgn)$
到此爲止,全部functions的運行復雜度都分析完了,下面的章節就是使用Java的實現了。
Section 4 - Java Implementation
這個Section一共有兩個內容,一個簡單的Java實現(只有對key排序功能)和一個Priority Queue。
Parameters & Constructors:
1 protected double A[]; 2 protected int heapsize; 3 4 //constructors 5 public MaxHeap(){} 6 public MaxHeap(double A[]){ 7 buildMaxHeap(A); 8 }
求parent/left child/right child:
1 protected int parent(int i) {return (i - 1) / 2;} 2 protected int left(int i) {return 2 * i + 1;} 3 protected int right(int i) {return 2 * i + 2;}
保持最大堆特性:
protected void maxHeapify(int i){ int l = left(i); int r = right(i); int largest = i; if (l <= heapsize - 1 && A[l] > A[i]) largest = l; if (r <= heapsize - 1 && A[r] > A[largest]) largest = r; if (largest != i) { double temp = A[i]; // swap A[i] = A[largest]; A[largest] = temp; this.maxHeapify(largest); } }
構造一個「最大堆」:
1 public void buildMaxHeap(double [] A){ 2 this.A = A; 3 this.heapsize = A.length; 4 5 for (int i = parent(heapsize - 1); i >= 0; i--) 6 maxHeapify(i); 7 }
對一個array使用heapsort:
1 public void heapsort(double [] A){ 2 buildMaxHeap(A); 3 4 int step = 1; 5 for (int i = A.length - 1; i > 0; i--) { 6 double temp = A[i]; 7 A[i] = A[0]; 8 A[0] = temp; 9 heapsize--; 10 System.out.println("Step: " + (step++) + Arrays.toString(A)); 11 maxHeapify(0); 12 } 13 }
main函數:
1 public static void main(String[] args) { 2 //a sample input 3 double [] A = {3, 7, 2, 11, 3, 4, 9, 2, 18, 0}; 4 System.out.println("Input: " + Arrays.toString(A)); 5 MaxHeap maxhp = new MaxHeap(); 6 maxhp.heapsort(A); 7 System.out.println("Output: " + Arrays.toString(A)); 8 9 }
運行結果:
Input: [3.0, 7.0, 2.0, 11.0, 3.0, 4.0, 9.0, 2.0, 18.0, 0.0] Step: 1[0.0, 11.0, 9.0, 7.0, 3.0, 4.0, 2.0, 2.0, 3.0, 18.0] Step: 2[0.0, 7.0, 9.0, 3.0, 3.0, 4.0, 2.0, 2.0, 11.0, 18.0] Step: 3[2.0, 7.0, 4.0, 3.0, 3.0, 0.0, 2.0, 9.0, 11.0, 18.0] Step: 4[2.0, 3.0, 4.0, 2.0, 3.0, 0.0, 7.0, 9.0, 11.0, 18.0] Step: 5[0.0, 3.0, 2.0, 2.0, 3.0, 4.0, 7.0, 9.0, 11.0, 18.0] Step: 6[0.0, 3.0, 2.0, 2.0, 3.0, 4.0, 7.0, 9.0, 11.0, 18.0] Step: 7[0.0, 2.0, 2.0, 3.0, 3.0, 4.0, 7.0, 9.0, 11.0, 18.0] Step: 8[2.0, 0.0, 2.0, 3.0, 3.0, 4.0, 7.0, 9.0, 11.0, 18.0] Step: 9[0.0, 2.0, 2.0, 3.0, 3.0, 4.0, 7.0, 9.0, 11.0, 18.0] Step: 10[0.0, 2.0, 2.0, 3.0, 3.0, 4.0, 7.0, 9.0, 11.0, 18.0] Output: [0.0, 2.0, 2.0, 3.0, 3.0, 4.0, 7.0, 9.0, 11.0, 18.0]
heapsort在實踐中常常被一個實現的很好的快排戰勝,但heap有另一個重要的應用,就是Priority Queue。這篇文章只作拓展內容說起,簡單得說,一個priority queue就是一組帶key的element,經過key來構造堆結構。一般,priority queue使用的是min-heap,例如按時間順序處理某些應用中的objects。
爲了方便,我用Inheritance實現一個priority queue:
1 package heapsort; 2 3 import java.util.Arrays; 4 5 public class PriorityQueue extends MaxHeap{ 6 7 public PriorityQueue(){super();} 8 public PriorityQueue(double [] A){super(A);} 9 10 public double maximum(){ 11 return A[0]; 12 } 13 14 public double extractMax(){ 15 if(heapsize<1) 16 System.err.println("no element in the heap"); 17 double max = A[0]; 18 A[0] = A[heapsize-1]; 19 heapsize--; 20 this.maxHeapify(0); 21 return max; 22 } 23 24 public void increaseKey(int i,double key){ 25 if(key < A[i]) 26 System.err.println("new key should be greater than old one"); 27 28 A[i] = key; 29 while(i>0 && A[parent(i)] <A[i]){ 30 double temp = A[i]; 31 A[i] = A[parent(i)]; 32 A[parent(i)] = temp; 33 i = parent(i); 34 } 35 } 36 37 public void insert(double key){ 38 heapsize++; 39 A[heapsize - 1] = Double.MIN_VALUE; 40 increaseKey(heapsize - 1, key); 41 } 42 43 public static void main(String[] args) { 44 //a sample input 45 double [] A = {3, 7, 2, 11, 3, 4, 9, 2, 18, 0}; 46 System.out.println("Input: " + Arrays.toString(A)); 47 PriorityQueue pq = new PriorityQueue(); 48 pq.buildMaxHeap(A); 49 System.out.println("Output: " + Arrays.toString(A)); 50 pq.increaseKey(2, 100); 51 System.out.println("Output: " + Arrays.toString(A)); 52 System.out.println("maximum extracted: " + pq.extractMax()); 53 pq.insert(33); 54 System.out.println("Output: " + Arrays.toString(A)); 55 56 } 57 }
運行結果:
Input: [3.0, 7.0, 2.0, 11.0, 3.0, 4.0, 9.0, 2.0, 18.0, 0.0] Output: [18.0, 11.0, 9.0, 7.0, 3.0, 4.0, 2.0, 2.0, 3.0, 0.0] Output: [100.0, 11.0, 18.0, 7.0, 3.0, 4.0, 2.0, 2.0, 3.0, 0.0] maximum extracted: 100.0 Output: [33.0, 18.0, 4.0, 7.0, 11.0, 0.0, 2.0, 2.0, 3.0, 3.0]
Section 5 - 小結
首先要說本文所有是原創,如須要使用,只須要引用一下並不須要通知我。
寫到最後發現有不少寫得很冗餘,也有囉嗦的地方。感受表達出來對本身的知識鞏固頗有幫助。Heapsort真是一個很是有意思的排序方法,是一個通用而不算複雜的算法,這是決定開始寫blogs後的第一篇文章,必定有不少不足,歡迎討論!以後打算寫一些機器學習和計算機視覺方面的來拋磚引玉。但願經過博客園這個平臺能夠交到更多有鑽研精神的朋友。
Bibliography
[1] Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein.
Introduction to Algorithms. MIT Press, 3rd edition, 2009.
[2] Wikipedia. Heapsort — wikipedia, the free encyclopedia, 2014. [Online; accessed15-September-2014].