七大排序的我的總結(三)

堆排序(Heap:算法

要講堆排序以前先要來複習一下徹底二叉樹的知識。數組

定義:數據結構

對一棵具備n個結點的二叉樹按層序編號,若是編號爲i(0 <= i <= n)的結點與一樣深度的滿二叉樹編號爲i的結點在二叉樹中位置徹底相同,則這棵二叉樹稱爲徹底二叉樹。性能

 

如上面就是一棵徹底二叉樹。測試

咱們主要會使用的的性質是父結點與子結點的關係:spa

標號爲n的結點的左孩子爲2 * n + 1(若是有的話),右孩子爲2 * n + 2(若是有的話)操作系統

 

因爲徹底二叉樹的結點的編號是鏈接的,因此咱們能夠用一個數組來保存這種數據結構。結點之間的關係能夠經過上面的公式進行計算獲得。3d

 

那什麼是堆呢?code

堆是具備下列性質的徹底二叉樹:blog

每一個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆(或大根堆);或者每一個結點的值都小於或等於其左右孩子結點的值。稱爲小頂堆(小根堆)。

 

如圖:就是一大根堆。將它轉化爲數組就是這樣的:

{ 9,7,5,6,1,4,2,0,3 }

 

能夠看到一個大概的狀況是:0個元素是最大的,前面的元素廣泛比後面的大,但這不是絕對的好比例子中的1就跑到4前邊去了。

 

建堆:

那接下來就是第一個問題了,怎麼建立一個大根堆呢?也就是解決怎麼將給定的一個數組調整成大根堆

假如咱們給定一個比較極端的例子{ 10,20,30,40,50,60,70,80 },加個0是爲了方便不與結點的編號產生混淆。

 

對於這樣的一個堆,咱們應該怎麼進行調整呢?

對於堆排序而言,一個比較直觀的想法就是從下面開始,把值比較大的元素往上推。這樣進行到根位置時,就能夠獲得一個一個最大的根了

因此,咱們應該從最後一個非葉子結點開始調整。

那麼怎麼肯定哪個是最後一個非葉子結點呢?

其實這徹底是能夠從徹底二叉樹的性質中獲得的。還記得嗎?

左孩子爲2 * n + 1

右孩子爲2 * n + 2

因此最後一個非葉子結點的編號爲array.length / 2 – 1。array就是給定的數組。

 

因此咱們第一個要調整的結點是編號爲3的結點,拿它的值跟兩個孩子的值作比較(它只有一個孩子)。顯然,40和80這兩個要交換位置了。

 

接下來就輪到編號爲2的結點了,進行比較後顯然是70比較大一點,也進行交換:

 

 

一樣的道理,編號爲1的結點也進行調節:

 

請注意,這個時候問題就來了。結點1是符合條件了,能夠對於以結點3這根的這棵子樹就不符合大根堆的要求了,因此咱們要從新對編號爲3的結點再作一次調整。獲得:

 

咱們以一樣的方法對編號爲0的結點也進行一樣的調整。最後就能夠獲得第一個大根堆了。

 

這一個過程咱們能夠稱爲建堆。咱們將數據展開成數組:

{ 80,50,70,40,10,60,30,20 }

不難發現這一個過程當中,咱們已經把不少值比較大的數字也放到了比較靠前的位置。這一點至關重要,也能夠說是堆排序的精華所在。

 

獲得了大根堆以後,咱們是能夠獲得一個最大值了,接下來要作的,就是不斷的移除這個堆頂值,與堆尾的值進行交換,堆的長度減少1,而後進行從新的調整

 

顯然,每次都是在堆頂刪除,在堆頂開始調整。

 

以後就是一直重複這個過程直到只剩下一個元素時,就能夠完成排序工做了。

相信只要跟着這個思路和這幾張圖,本身模擬幾回仍是很好理解的。

接下來看看代碼是怎麼實現的:

public static void sort(int[] array) {

    init(array);

    // 這個過程就是不斷的從堆頂移除,調整

    for (int i = 1; i < array.length; i++) {

       int temp = array[0];

       int end = array.length - i;

       array[0] = array[end];

       array[end] = temp;

       adjust(array, 0, end);

    }

}

 

private static void init(int[] array) {

    for (int i = array.length / 2 - 1; i >= 0; i--) {

       adjust(array, i, array.length);

    }

}

 

private static void adjust(int[] array, int n, int size) {

    int temp = array[n]; // 先拿出數據

    int child = n * 2 + 1; // 這個是左孩子

    while (child < size) { // 這個保證還有左孩子

       // 若是右孩子也存在的話,而且右孩子的值比左孩子的大

       if (child + 1 < size && array[child + 1] > array[child]) {

           child++;

       }

       if (array[child] > temp) {

           array[n] = array[child];

           n = child; // n須要從新計算

           child = n * 2 + 1; // 從新計算左孩子

       } else {

           // 這種狀況說明左右孩子的值都比父結點的值小

           break;

       }

    }

    array[n] = temp;

}

堆排序的代碼量比較多,主要的工做實際上是在adjust上。

在adjust這個過程當中有幾個要注意的:

一個是要注意數組的邊界,由於咱們每次是把最大值放在最後,而後它就不能再參與調整了。

其次,是最後一個非葉子結點可能只有一個孩子,這也是須要注意的。

 

堆排序到底快在哪呢?

仍是來看一個極端的例子:

{ 1,2,3,4,5,6,7 }

在建堆的時候第一次比較以後的結果應該是這樣的:(7和3交換了位置)

{ 1,2,7,4,5,6,3

第二次調整以後是:

{ 1,5,7,4,2,6,3 }(5和2交換了位置)

而後是:

7,5,1,4,2,6,3 }(7和1交換了位置,1的位置不對,須要再調整)

{ 7,5,6,4,2,1,3 }(6和1交換了位置)

能夠看到,僅僅用了4次比較和4次交換就已經把數組給調整成「比較有序」了。

 

這個實際上是由徹底二叉樹的性質決定的,由於子結點的編號和父結點的編號存在着兩倍(粗略)的差距。

也就說父結點與子結點的數據進行一次交換移動的距離是比較大的(相對於步進)。這個與冒泡和直接插入的「步進」是有明顯的區別的。能夠說,堆排序的優點在於它具備高效的元素移動效率(這是我的總結,不嚴謹)

其次,咱們在調整堆的時候,能夠發現有一半的數據是咱們不用動到的。這就使比較次數大大地減小。這個就是很好地利用在建堆的時候保存下來的狀態。仍是那句話「讓上一次的操做結果爲下一次操做服務」。

 

最後回顧一下七個排序:

冒泡排序:好吧,它是中槍次數最多的,最大的優勢應該是陪襯其餘算法的高效。

 

選擇排序:我我的認爲它是最符合人的思惟習慣的,缺點在於比較次數太多了,但其實它在對少許數據,或者是對於只排序一部分(好比只選出前十名之類的),這種狀況下,選擇排序就很不錯了,由於它能夠「部分排序」。

 

直接插入排序:其實它還不算太差,在應對一些平時的使用時,性能仍是能夠的。直接插入排序是希爾排序的基礎。

 

希爾排序:這個曾經把我糾結好久的算法,它的外表很難讓人看出它的強大。它在幾個比較高效的排序算法中代碼是最少的,也很容易一次性寫出。但理解有點困難。我以爲主要是那個步長序列太難讓人一眼看出它到底作了些什麼。我的以爲要理解希爾排序首先要弄清楚「基本有序」這個有什麼用和希爾排序的前n-1個步長作的就是這些事。先讓整個數組變得基本有序,基於一個事實,就是對於基本有序的數組而言,直接插入排序的效率是很高的

 

歸併排序:分治和遞歸的經典使用,勝就勝在元素的比較次數比較少(貌似說是最少的)。缺點是須要比較大的輔助空間,這個有時會成爲限制條件(由於過大的空間消耗有時是不容許的)。

 

快速排序:如其名,雖存在必定的不穩定性,理論上在最差的狀況下,快速排序會退化成選擇排序,但能夠經過一些手段來使這種狀況發生的機率至關的小。

 

堆排序:我的以爲是最難一口氣寫出來的排序算法,特別是調整結點的算法每次都要寫得當心翼翼(固然,多是平時寫得少)。但它確實是一個很優秀的排序算法,堆排序在元素的移動效率和比較次數上都是比較優秀的。操做系統中堆但是一個重要的數據結構。我記得當時第一次寫出堆排序的感嘆是「原來數組還能夠這麼用」。

 

最後讓這幾大高手進行一次PK吧,測試的數據是3000000個範圍在0 ~ 30000000的隨機數。

獲得的結果大概是這樣的:

    

差距並不算太大,能夠看到,最快的仍是Java類庫提供的方法,它爲何能比快速排序還快呢?

由於它是綜合了其餘幾個算法的特色,好比說在元素不多的時候,直接插入排序可能會快一點,數據量大一點的時候歸併可能會快一點,當數據很大的時候,用快速排序能夠把數組分紅小部分。因此它不是一我的在戰鬥!

 

好了,至此,七個排序算法也算是複習了一次,仍是那句話,本人菜鳥一個,對這幾個算法理解有限,出錯之處還請各位指出。

一點我的感覺,算法這東西有時覺得本身弄懂了,其實還差得遠了,有時候看十次書不如本身寫一次代碼,寫了十次代碼不如跟別人講一次。由於這個過程會遇到不少本身之前從沒想過的事。這就是我寫博客的初衷。

相關文章
相關標籤/搜索