堆是數據結構中的一種重要結構,瞭解了「堆」的概念和操做,能夠快速掌握堆排序。
html
堆的概念java
堆是一種特殊的徹底二叉樹(complete binary tree)。若是一棵徹底二叉樹的全部節點的值都不小於其子節點,稱之爲大根堆(或大頂堆);全部節點的值都不大於其子節點,稱之爲小根堆(或小頂堆)。算法
在數組(在0號下標存儲根節點)中,容易獲得下面的式子(這兩個式子很重要):數組
1.下標爲i的節點,父節點座標爲(i-1)/2;數據結構
2.下標爲i的節點,左子節點座標爲2*i+1,右子節點爲2*i+2。ide
堆的創建和維護oop
堆能夠支持多種操做,但如今咱們關心的只有兩個問題:post
1.給定一個無序數組,如何創建爲堆?性能
2.刪除堆頂元素後,如何調整數組成爲新堆?this
先看第二個問題。假定咱們已經有一個現成的大根堆。如今咱們刪除了根元素,但並無移動別的元素。想一想發生了什麼:根元素空了,但其它元素還保持着堆的性質。咱們能夠把最後一個元素(代號A)移動到根元素的位置。若是不是特殊狀況,則堆的性質被破壞。但這僅僅是因爲A小於其某個子元素。因而,咱們能夠把A和這個子元素調換位置。若是A大於其全部子元素,則堆調整好了;不然,重複上述過程,A元素在樹形結構中不斷「下沉」,直到合適的位置,數組從新恢復堆的性質。上述過程通常稱爲「篩選」,方向顯然是自上而下。
刪除一個元素是如此,插入一個新元素也是如此。不一樣的是,咱們把新元素放在末尾,而後和其父節點作比較,即自下而上篩選。
那麼,第一個問題怎麼解決呢?
我看過的數據結構的書不少都是從第一個非葉子結點向下篩選,直到根元素篩選完畢。這個方法叫「篩選法」,須要循環篩選n/2個元素。
但咱們還能夠借鑑「無中生有」的思路。咱們能夠視第一個元素爲一個堆,而後不斷向其中添加新元素。這個方法叫作「插入法」,須要循環插入(n-1)個元素。
因爲篩選法和插入法的方式不一樣,因此,相同的數據,它們創建的堆通常不一樣。
大體瞭解堆以後,堆排序就是水到渠成的事情了。
算法概述/思路
咱們須要一個升序的序列,怎麼辦呢?咱們能夠創建一個最小堆,而後每次輸出根元素。可是,這個方法須要額外的空間(不然將形成大量的元素移動,其複雜度會飆升到O(n^2))。若是咱們須要就地排序(即不容許有O(n)空間複雜度),怎麼辦?
有辦法。咱們能夠創建最大堆,而後咱們倒着輸出,在最後一個位置輸出最大值,次末位置輸出次大值……因爲每次輸出的最大元素會騰出第一個空間,所以,咱們剛好能夠放置這樣的元素而不須要額外空間。很漂亮的想法,是否是?
下面是堆排序的示意圖(圖片來自維基百科):
代碼實現
因爲堆是一種數據結構,所以,咱們能夠封裝它爲一個類。固然,也能夠不這麼作。下面的代碼使用篩選法創建了一個堆。
package flyingcat.sort; /** * * @author FlyingCat * Date: 2013-8-26 * */ public class ArrayHeap { private int[] array; public ArrayHeap(int[] arr) { this.array = arr; } private int getParentIndex(int child) { return (child - 1) / 2; } private int getLeftChildIndex(int parent) { return 2 * parent + 1; } /** * 初始化一個大根堆。 */ private void initHeap() { int last = array.length - 1; for (int i = getParentIndex(last); i >= 0; --i) { // 從最後一個非葉子結點開始篩選 int k = i; int j = getLeftChildIndex(k); while (j <= last) { if (j < last) { if (array[j] <= array[j + 1]) { // 右子節點更大 j++; } } if (array[k] > array[j]) { //父節點大於子節點中較大者,已經找到最終位置 break; // 中止篩選 } else { swap(k, j); k = j; // 繼續篩選 } j = getLeftChildIndex(k); }// loop while }// loop i } /** * 調整堆。 */ private void adjustHeap(int lastIndex) { int k = 0; while (k <= getParentIndex(lastIndex)) { int j = getLeftChildIndex(k); if (j < lastIndex) { if (array[j] < array[j + 1]) { j++; } } if (array[k] < array[j]) { swap(k, j); k = j; // 繼續篩選 } else { break; // 中止篩選 } } } /** * 堆排序。 * */ public void sort() { initHeap(); int last = array.length - 1; while (last > 0) { swap(0, last); last--; if (last > 0) { // 這裏若是不判斷,將形成最終前兩個元素逆序。 adjustHeap(last); } } } private void swap(int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } }
算法性能/複雜度
堆排序的時間複雜度很是穩定(咱們能夠看到,對輸入數據不敏感),爲O(n㏒n)複雜度,最好狀況與最壞狀況同樣。
可是,其空間複雜度依實現不一樣而不一樣。上面即討論了兩種常見的複雜度:O(n)與O(1)。本着節約空間的原則,我推薦O(1)複雜度的方法。
算法穩定性
堆排序存在大量的篩選和移動過程,屬於不穩定的排序算法。
算法適用場景
堆排序在創建堆和調整堆的過程當中會產生比較大的開銷,在元素少的時候並不適用。可是,在元素比較多的狀況下,仍是不錯的一個選擇。尤爲是在解決諸如「前n大的數」一類問題時,幾乎是首選算法。
參考資料
1.維基百科 http://zh.wikipedia.org/wiki/%E5%A0%86%E6%8E%92%E5%BA%8F#java.E8.AF.AD.E8.A8.80
2.堆排序-chenlb的博客 http://blog.chenlb.com/2008/12/heap-sort-for-java.html