1. 程式人生 > >七大排序演算法的個人總結(三)

七大排序演算法的個人總結(三)

堆排序(Heap:

要講堆排序之前先要來複習一下完全二叉樹的知識。

定義:

對一棵具有n個結點的二叉樹按層序編號,如果編號為i(0 <= i <= n)的結點與同樣深度的滿二叉樹編號為i的結點在二叉樹中位置完全相同,則這棵二叉樹稱為完全二叉樹。

 

如上面就是一棵完全二叉樹。

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

標號為n的結點的左孩子為2 * n + 1(如果有的話),右孩子為2 * n + 2(如果有的話)

 

由於完全二叉樹的結點的編號是連線的,所以我們可以用一個數組來儲存這種資料結構。結點之間的關係可以通過上面的公式進行計算得到。

 

那什麼是堆呢?

堆是具有下列性質的完全二叉樹:

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

 

如圖:就是一大根堆。將它轉化為陣列就是這樣的:

{ 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類庫提供的方法,它為什麼能比快速排序還快呢?

因為它是綜合了其他幾個演算法的特點,比如說在元素很少的時候,直接插入排序可能會快一點,資料量大一點的時候歸併可能會快一點,當資料很大的時候,用快速排序可以把陣列分成小部分。所以它不是一個人在戰鬥!

 

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

一點個人感受,演算法這東西有時以為自己弄懂了,其實還差得遠了,有時候看十次書不如自己寫一次程式碼,寫了十次程式碼不如跟別人講一次。因為這個過程會遇到很多自己以前從沒想過的事。這就是我寫部落格的初衷。

from: https://www.cnblogs.com/yjiyjige/p/3258849.html