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


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

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

 

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM