漫談經典排序算法:一、從簡單選擇排序到堆排序的深度解析

原文:http://blog.csdn.net/touch_2011/article/details/6767673

1、序言

這是《漫談經典排序算法系列》第一篇,該篇從最簡單的選擇排序算法談起,由淺入深的詳細解析兩種選擇排序算法的過程及性能比較。逐步揭露選擇排序的本質及其基本思想。

各種排序算法的解析請參考如下:

《漫談經典排序算法:一、從簡單選擇排序到堆排序的深度解析》

《漫談經典排序算法:二、各種插入排序解析及性能比較》

《漫談經典排序算法:三、冒泡排序 && 快速排序》

《漫談經典排序算法:四、歸併排序》

《漫談經典排序算法:五、線性時間排序(計數、基數、桶排序)》

《漫談經典排序算法:六、各種排序算法總結》 

        注:爲了敘述方便,本文以及源代碼中均不考慮A[0],默認下標從1開始。

2、提出問題

     (1)簡單選擇排序和堆排序的基本思想是什麼?

     (2)選擇排序的本質是什麼?

       相信看完這篇文章,讀者一定可以找到答案。 

3、漫談簡單選擇排序

        3.1 從一個簡單問題談起

        給定待排序序列A[ 1.....n ],選擇出A中最小的記錄(也可以理解爲求一個無序數組A中的最小的元素)。下面給出代碼如下:

 

  1. <span style="font-size:18px;">    //選擇待排序序列a中的最小記錄,其下標爲index  
  2.     for(index=i=1;i<=n;i++){  
  3.         if(a[i]<a[index])  
  4.             index=i;  
  5.     }</span>  


         相信這段代碼大家都能看懂,其中A[index]即爲要找的最小記錄。也許有人要問作者是在寫選擇排序,幹嘛要談這個簡單的問題呢?請不要急,看到後面自然就知道了。

        3.2 簡單選擇排序的過程

        描述:給定待排序序列A[ 1......n ] ,選擇出第i小元素,並和A[i]交換,這就是一趟簡單選擇排序。

        代碼:

  1. <span style="font-size:18px;">//簡單選擇排序  
  2. void simpleSelectionSort1(int *a,int n)  
  3. {  
  4.     int i,j,index;  
  5.     //1.進行n-1趟選擇,每次選出第i小記錄  
  6.     for(i=1;i<n;i++){  
  7.         index=i;  
  8.         //2.選擇第i小記錄爲a[index]  
  9.         for(j=i+1;j<=n;j++)  
  10.             if(a[j]<a[index])  
  11.                 index=j;  
  12.         //3.與第i個記錄交換  
  13.         if(index!=i){  
  14.             a[i]=a[i]+a[index];  
  15.             a[index]=a[i]-a[index];  
  16.             a[i]=a[i]-a[index];  
  17.         }  
  18.     }  
  19. }</span>  

        示例:假設給定數組A[1......6]={ 3,5,8,9,1,2 },我們來分析一下A數組進行選擇排序的過程

        第一趟:i=1,index=5, a[1] 和 a[5] 進行交換。得到序列:{ 1,5,8,9,3,2 }

        第二趟:i=2,index=6, a[2] 和 a[6] 進行交換。得到序列:{ 1,2,8,9,3,5 }

        第三趟:i=3,index=5, a[3] 和 a[5] 進行交換。得到序列:{ 1,2,3,9,8,5 }

        第四趟:i=4,index=6, a[3] 和 a[5] 進行交換。得到序列:{ 1,2,3,5,8,9 }

        第五趟:i=5,index=5, 不用交換。得到序列:{ 1,2,3,5,8,9 }

       (6-1)趟選擇結束,得到有序序列:{ 1,2,3,5,8,9 }

        3.3 性能分析

容易看出,簡單選擇排序所需進行記錄移動的操作次數較少,這一點上優於冒泡排序,最佳情況下(待排序序列有序)記錄移動次數爲0,最壞情況下(待排序序列逆序)記錄移動次數n-1。外層循環進行了n-1趟選擇,第i趟選擇要進行n-i次比較。每一趟的時間:n-i次的比較時間+移動記錄的時間(爲一常數0或1,可以忽略)。總共進行了n-1趟。忽略移動記錄的時間,所以總時間爲(n-1)*(n-i)=n^2-(i+1)*n+i。時間複雜度爲O(n^2)。不管是最壞還是最佳情況下,比較次數都是一樣的,所以簡單選擇排序平均時間、最壞情況、最佳情況 時間複雜度都爲O(n^2)。同時簡單選擇排序是一種穩定的原地排序算法。當然穩定性還是要看具體的代碼,在此就不做深究。          

3.4 簡單選擇排序引發的思考    

第一趟排序後:{ 1,5,8,9,3,2 } ,此時A[ 1 ]已經有序,我們可以把待排序序列縮減到A[ 2......6 ]

第二趟排序後:{ 1,2,8,9,3,5 },此時A[ 1...2 ]已經有序,我們可以把待排序序列縮減到A[ 3......6 ]

第三趟排序後:{ 1,2,3,9,8,5 },此時A[ 1...3 ]已經有序,我們可以把待排序序列縮減到A[ 4......6 ]

第四趟排序後:{ 1,2,3,5,8,9 },此時A[ 1...4 ]已經有序,我們可以把待排序序列縮減到A[ 5......6 ]

第五趟排序後:{ 1,2,3,5,8,9 },此時A[ 1...5 ]已經有序,我們可以把待排序序列縮減到A[ 6......6 ]

也就是說第i趟後,A[ 1...i ]已經有序,待排序序列縮減爲A[ (i+1)...n ]。這是一個待排序序列中記錄不斷減少的遞歸過程。也許很多讀者已經發現,我們每次都是從待排序序列中選擇最小的那個記錄然後跟待排序序列的首元素進行交換。於是可以用一個遞歸函數來進行簡單選擇排序,代碼如下:

 

  1. <span style="font-size:18px;">//遞歸函數進行簡單選擇排序  
  2. void simpleSelectionSort2(int *a,int n)  
  3. {  
  4.     int index,i;  
  5.     if(n==1)  
  6.         return;  
  7.     //1.選擇待排序序列a中的最小記錄,其下標爲index  
  8.     for(index=i=1;i<=n;i++){  
  9.         if(a[i]<a[index])  
  10.             index=i;  
  11.     }  
  12.     //2.最小記錄與待排序序列首元素進行交換  
  13.     if(index!=1){  
  14.         a[1]=a[1]+a[index];  
  15.         a[index]=a[1]-a[index];  
  16.         a[1]=a[1]-a[index];  
  17.     }  
  18.     //3.待排序序列元素個數減少,遞歸對剩下的無序序列排序  
  19.     simpleSelectionSort2(a+1,n-1);  
  20. }</span>  

 

看到這裏,不知大家是否還記得上文3.1中談到的簡單的問題。由此可以看出這個簡單的問題正是簡單選擇排序的本質所在。

4、深入堆排序

        4.1 堆排序的引入

       從上文我們知道簡單選擇排序的時間複雜度爲O(n^2),熟悉各種排序算法的朋友都知道,這個時間複雜度是很大的,所以怎樣減小簡單選擇排序的時間複雜度呢?從上文分析中我們知道簡單選擇排序主要操作是進行關鍵字的比較,所以怎樣減少比較次數就是改進的關鍵。 簡單選擇排序中第i趟需要進行n-i次比較,如果我們用到前面已排好

的序列a[1...i-1]是否可以減少比較次數呢?答案是可以的。舉個例子來說吧,A、B、C進行比賽,B戰勝了A,C戰勝了B,那麼顯然C可以戰勝A,C和A就不用比了。正是基於這種思想,有人提出了樹形選擇排序:對n個記錄進行兩兩比較,然後在([n/2]向上取整)個較小者之間在進行兩兩比較,如此重複,直到選出最小記錄。但是這種排序算法需要的輔助空間比較多,所以威洛姆斯在1964年提出了另一種選擇排序,這就是下面要談的堆排序。

        4.2 什麼是堆

       首先堆是一種數據結構,是一棵完全二叉樹且滿足性質:所有非葉子結點的值均不大於或均不小於其左、右孩子結點的值,如下是一個堆得示例:

               

         9>8,9>5;8>3,8>1;5>2   由此發現非葉子結點的值均不小於左右孩子結點的值,所以這是個大頂堆,即堆頂的值是這個堆中最大的一個。

        下面的問題是我們怎麼樣在計算機中存儲這個堆呢?也許有人會想到樹的存儲,確實,剛看這個堆我也是這麼想的。然而事實並非如此,這個堆可以看成是一個一維數組A[6]={9,8,5,3,1,2},那麼相應的這個數組需滿足性質:A[i]<=A[2*i] && A[i]<=A[2*i+1] 。其中A[i]對應堆中的非葉子結點,A[2*i]和A[2*i+1]對應於左右孩子結點。並且最後一非葉子結點下標爲[n/2]向下取整。

         爲什麼是[n/2]向下取整呢?在這裏我簡單的說明一下:設n1表示完全二叉樹中有一個孩子的結點,n2表示表示完全二叉樹中有兩個孩子的結點,d表示非葉子結點的個數,那麼總的結點個數:n=n1+2*n2+1。

        (1)當n爲奇數時,n1=0,n2=(n-1)/2,d=n2+n1=(n-1)/2

        (2)當n爲偶數時,n1=1,n2=n/2-1,d=n2+n1=n/2

          由此可以看出d=[n/2]向下取整.

          注:請大家一定要結合完全二叉樹形式的堆以及堆的數組存儲形式來看下面的內容,這樣才能真正理解堆排序的過程及其本質。

        4.3 篩選法調整堆

        (1)引出:

        現給定一個大頂堆:  即:A[6]={9,8,5,3,1,2},如果我們稍做破壞,把9跟2互換,同時把a[6]這個結點從堆中去掉,於是得到下面這個完全二叉樹:

        A[5]={2,8,5,3,1}

       顯然它不是一個堆,那我們怎麼把它調整爲一個堆呢?首先觀察,我們只是改變了根結點的值,所以根結點左、右子樹均是大頂堆。其次思考,既然是根結點可能破壞了堆的性質,那我們就可以把根結點往下沉,讓最大值網上浮,即比較根結點和左、右孩子的值,若根結點的值不小於孩子結點的值,說明根結點並沒有破壞堆的性質,不用調整;若根結點的值小於左右孩子結點中的任意一個值,則根結點與孩子結點中較大的一個互換,互換之後又可能破壞了左或右子樹的堆性質,於是要對子樹進行上述調整。這樣的一次調整我們稱之爲一次篩選。

      (2)代碼:

       

  1. <span style="font-size:18px;">//調整堆,保持大頂堆的性質,參數i指向根結點  
  2. void maxHeap(int *a,int n,int i)  
  3. {  
  4.     //left、right、largest分別指向  
  5.     //左孩子、右孩子、{a[i],a[left]}中最大的一個  
  6.     int left,right,largest;  
  7.     largest=left=2*i;  
  8.     if(left>n)  
  9.         return;  
  10.     right=2*i+1;  
  11.     if(right<=n && a[right]>a[left]){  
  12.         largest=right;  
  13.     }  
  14.     if(a[i]<a[largest]){//根結點的值不是最大時,交換a[i],a[largest]  
  15.         a[i]=a[i]+a[largest];  
  16.         a[largest]=a[i]-a[largest];  
  17.         a[i]=a[i]-a[largest];  
  18.         //自上而下調整堆  
  19.         maxHeap(a,n,largest);  
  20.     }  
  21. }</span>  

(3)示例

以這個完全二叉樹爲例 :        A[5]={2,8,5,3,1}

第一次篩選:2和8交換

   A[5]={8,2,5,3,1}

第二次篩選:2和3交換

   A[5]={8,3,5,2,1}

篩選完畢,得到大頂堆A[5]={8,3,5,2,1}。

(4)時間代價分析

每一次篩選的過程就是調用一次maxHeap函數,需要的時間是O(1)。那麼要執行多少次篩選呢?從上述中可以看出,每一次篩選根結點都往下沉,所以篩選次數不會超過完全二叉樹的深度:([log2n]向下取整+1),其中n爲結點個數,2爲底數,即時間複雜度爲O(log2n)

 爲什麼n個結點的完全二叉樹的深度是([log2n]向下取整+1)呢?這裏給出簡單的說明:

         深度爲h的完全二叉樹至多有2^h-1個結點,即2^(h-1)<=n<2^h,推出h-1<=log2n<h;由於h是一個整數,所以h=[log2n]向下取整+1 .

        4.4 建堆

        4.3中敘述了堆的篩選過程,但是給定一個待排序的序列,怎樣通過篩選使這個序列滿足堆的性質呢?

        給定待排序序列  A[6]={3,5,8,9,1,2},怎樣使它變成一個堆呢?

        仔細想一想篩選法的前提條件是什麼:根結點的左右子樹已經是堆。那麼這棵樹中哪個結點的左右子樹是堆呢,很自然的發現是最後一個非葉子結點,所以我們在這裏需要自下而上的調整這個完全二叉樹。

      (2)代碼:

  1. <span style="font-size:18px;">//建堆  
  2. void creatHeap(int *a,int n)  
  3. {  
  4.     int i;  
  5.     //自下而上調整堆  
  6.     for(i=n/2;i>=1;i--)  
  7.         maxHeap(a,n,i);  
  8. }</span>  

      (3)示例

         待排序序列:  A[6]={3,5,8,9,1,2},     

         以8爲根結點調整堆後:因爲8>2,此處不進行記錄移動操作

         以5爲根結點調整堆後:5<9,5跟9互換 

             A[6]={3,9,8,5,1,2}

         以3爲根結點調整堆後:3<9,3跟9互換

            A[6]={9,3,8,5,1,2}

         以9爲根的左子樹不滿足大頂堆的性質,所以以3爲跟調整堆,即交換3和5,得A[6]={9,5,8,3,1,2}

        (4)時間代價分析

           從最後一個非葉子結點到第二個結點,總共循環了n/2-1次,每次調用maxHeap函數,4.3中已經分析過maxHeap時間複雜度爲O(log2n)。所以建堆的時間複雜度爲O(n*log2n)

        4.5 堆排序

      (1)堆排序過程

       也許有的朋友想問:不是講堆排序嗎,爲什麼不直接講呢,而是先敘述篩選法和建堆呢?因爲篩選法和建堆就構成了堆排序,講到這裏,堆排序可以說是水到渠成。所以一定要理解篩選法和建堆的過程。

       過程描述:1、建堆  2、將堆頂記錄和堆中最後一個記錄交換  3、篩選法調整堆,堆中記錄個數減少一個,重複第2步。整個過程中堆是在不斷的縮減。

      (2)代碼

  1. <span style="font-size:18px;">//堆排序  
  2. void heapSort(int *a,int n)  
  3. {  
  4.     int i;  
  5.     creatHeap(a,n);//建堆  
  6.     for(i=n;i>=2;i--){  
  7.         //堆頂記錄和最後一個記錄交換  
  8.         a[1]=a[1]+a[i];  
  9.         a[i]=a[1]-a[i];  
  10.         a[1]=a[1]-a[i];  
  11.         //堆中記錄個數減少一個,篩選法調整堆  
  12.         maxHeap(a,i-1,1);  
  13.     }  
  14. }</span>  

      (3)示例
        0.待排序序列:

         A[6]={3,5,8,9,1,2},  

        1.建堆後(建堆過程參見4.4):

        A[6]={9,3,8,5,1,2}

       2.9和2交換,然後把9從堆中去掉後:

         A[6]={2,3,8,5,1,9}

      3.篩選法調整堆A[5]={2,3,8,5,1}後(調整過程參見4.3):

       A[6]={8,3,2,5,1,9}

      4.堆頂記錄與最後一個記錄互換,重複第二步,但是堆頂記錄和最後一個記錄的值變了

    (4)堆排序性能分析

       堆排序時間=建堆時間+調整堆時間。從上文中知道建堆時間複雜度爲O(n*log2n)。篩選法調整堆(maxHeap函數)時間O(log2n),總共循環了n-1次maxHeap函數,所以調整堆時間複雜度爲O(n*log2n)。得出堆排序時間複雜度O(n*log2n)。

       熟悉了堆排序的過程後,可以發現堆排序不存在最佳情況,待排序序列是有序或者逆序時,並不對應於堆排序的最佳或最壞情況。且在最壞情況下時間複雜度也是O(n*log2n)。此外堆排序是不穩定的原地排序算法。

        4.6 反思堆排序 ------ 揭開選擇排序的本質和基本思想

        敘述到這裏,堆排序就敘述完了。不知道大家發現沒有,上文中每一個樹形的堆邊上都有一個數組,其實待排序的整個過程中都是數組元素在不斷的交換移動,樹形的堆只是能形象的表示這個過程。通過觀察這個數組的變化,我們發現了什麼呢?

        仔細回想一下篩選法調整堆的過程我們發現,第i次調整堆,其實就是把A中的第i大元素放到首位置A[1],然後A[1]和A[n-i+1]互換.這樣A[(n-i+1)...n]就已經有序,於是又把A[1...n-i]看成一個堆再進行排序,如此重複。

        還記得3.1中提到那個簡單的問題(選擇出A中最小的記錄)嗎?調整堆不就是選擇出待排序序列中的最大值嗎?所以堆排序的本質和選擇排序的本質是一樣的。選擇一個待排序序列中的最小(大)值,這就是選擇排序的本質。

        敘述到此,相信大家已然知道開篇提出的兩個問題的答案了吧。選擇排序的基本思想是:每一趟從n-i+1個記錄中選擇最小(大)記錄和第i(n-i+1)個記錄交換。

5、附件

        5.1 附件1

        參考文獻:《數據結構》嚴蔚敏版               《算法導論》第二版

        5.2 附件2

        本文涉及的源代碼免積分下載地址:http://download.csdn.net/detail/touch_2011/3594107

        5.3 附件3

        源代碼    

        simple_selection_sort.c

  1. <span style="font-size:18px;">#include<stdio.h>  
  2.   
  3. //簡單選擇排序  
  4. void simpleSelectionSort1(int *a,int n)  
  5. {  
  6.     int i,j,index;  
  7.     //1.進行n-1趟選擇,每次選出第i小記錄  
  8.     for(i=1;i<n;i++){  
  9.         index=i;  
  10.         //2.選擇第i小記錄爲a[index]  
  11.         for(j=i+1;j<=n;j++)  
  12.             if(a[j]<a[index])  
  13.                 index=j;  
  14.         //3.與第i個記錄交換  
  15.         if(index!=i){  
  16.             a[i]=a[i]+a[index];  
  17.             a[index]=a[i]-a[index];  
  18.             a[i]=a[i]-a[index];  
  19.         }  
  20.     }  
  21. }  
  22.   
  23. //遞歸函數進行簡單選擇排序  
  24. void simpleSelectionSort2(int *a,int n)  
  25. {  
  26.     int index,i;  
  27.     if(n==1)  
  28.         return;  
  29.     //1.選擇待排序序列a中的最小記錄,其下標爲index  
  30.     for(index=i=1;i<=n;i++){  
  31.         if(a[i]<a[index])  
  32.             index=i;  
  33.     }  
  34.     //2.最小記錄與待排序序列首元素進行交換  
  35.     if(index!=1){  
  36.         a[1]=a[1]+a[index];  
  37.         a[index]=a[1]-a[index];  
  38.         a[1]=a[1]-a[index];  
  39.     }  
  40.     //3.待排序序列元素個數減少,遞歸對剩下的無序序列排序  
  41.     simpleSelectionSort2(a+1,n-1);  
  42. }  
  43.   
  44.      </span>  

        heap_sort.c

  1. <span style="font-size:18px;">#include<stdio.h>  
  2.   
  3. //調整堆,保持大頂堆的性質,參數i指向根結點  
  4. void maxHeap(int *a,int n,int i)  
  5. {  
  6.     //left、right、largest分別指向  
  7.     //左孩子、右孩子、{a[i],a[left]}中最大的一個  
  8.     int left,right,largest;  
  9.     largest=left=2*i;  
  10.     if(left>n)  
  11.         return;  
  12.     right=2*i+1;  
  13.     if(right<=n && a[right]>a[left]){  
  14.         largest=right;  
  15.     }  
  16.     if(a[i]<a[largest]){//根結點的值不是最大時,交換a[i],a[largest]  
  17.         a[i]=a[i]+a[largest];  
  18.         a[largest]=a[i]-a[largest];  
  19.         a[i]=a[i]-a[largest];  
  20.         //自上而下調整堆  
  21.         maxHeap(a,n,largest);  
  22.     }  
  23. }  
  24.   
  25. //建堆  
  26. void creatHeap(int *a,int n)  
  27. {  
  28.     int i;  
  29.     //自下而上調整堆  
  30.     for(i=n/2;i>=1;i--)  
  31.         maxHeap(a,n,i);  
  32. }  
  33.   
  34. //堆排序  
  35. void heapSort(int *a,int n)  
  36. {  
  37.     int i;  
  38.     creatHeap(a,n);//建堆  
  39.     for(i=n;i>=2;i--){  
  40.         //堆頂記錄和最後一個記錄交換  
  41.         a[1]=a[1]+a[i];  
  42.         a[i]=a[1]-a[i];  
  43.         a[1]=a[1]-a[i];  
  44.         //堆中記錄個數減少一個,篩選法調整堆  
  45.         maxHeap(a,i-1,1);  
  46.     }  
  47. }  
  48.   
  49.  </span>  

        test.c

  1. <span style="font-size:18px;">#include<stdio.h>  
  2. #include<time.h>  
  3. #include"heap_sort.c"  
  4. #include"simple_selection_sort.c"  
  5.   
  6. void main()  
  7. {  
  8.     int i;  
  9.     int a[7]={0,3,5,8,9,1,2};//不考慮a[0]  
  10.     simpleSelectionSort1(a,6);  
  11.     simpleSelectionSort1(a,6);  
  12.     heapSort(a,6);  
  13.     for(i=1;i<=6;i++)  
  14.         printf("%-4d",a[i]);  
  15.     printf("\n");  
  16. }  
  17. </span>  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章