排序算法總結

排序算法總覽

分類:

  • 插入類:直接插入排序、折半插入排序、希爾排序
  • 交換類:冒泡排序、快速排序
  • 選擇類:直接選擇排序、堆排序
  • 歸併類:二路歸併排序

特徵:

  • 平均時間複雜度:快、希、歸、堆排序爲:O(nlog2 n);其餘排序爲O(n2 )(記憶口訣:快些歸隊)
  • 穩定性:快、希、選、堆排序爲:不穩定;其他排序是穩定的(記憶口訣:快些選一堆)
  • 經過一趟排序就能保證一個元素到達最終位置:(交換類)冒泡排序、快速排序、(選擇類)直接選擇排序、堆排序
  • 元素比較次數與初始序列無關:直接選擇排序、折半插入排序
  • 排序的趟數與初始序列有關:交換類
    • 冒泡排序:越有序,需要的趟數越少
    • 快速排序:越有序,需要的趟數越多
    • 注意插入排序需要的趟數是固定n-1的,只是越有序每趟元素比較的次數越少

——>下面具體分析每種排序:

  • 注:默認序列下標從0開始

一、插入排序

基本思想:將待排序表看做兩個部分:無序區、有序區。整個排序過程就是將無序區的元素逐個插入到有序區中(注意①如何尋找插入點,②插入時如何移動有序區的元素是關鍵),構成新的有序區。

  • 直接插入排序
  • 折半插入排序
  • 希爾排序

1、直接插入排序

基本思想

將待排序表分爲左右兩個部分,無序區在左邊,有序區在右邊。一邊尋找插入點一邊後移。

-------○■
----------○■
-------------○■
  • 雙層循環。
  • 外層:由小到大(從左到右)。由■代表i:待插入值的序號,從i = 1開始
  • 內層:從右到左。由○代表j=i-1:被比較值的序號

代碼

/**
  * 直接插入排序
  * @param R[0..N-1] 待排序表
  * @param n 序列個數
  */
void insertSort(int[] R, int n) {
    int i, j;
    int temp;
    //外層循環:由小到大
    for (i = 1; i < n; i++) {
        temp = R[i];
        j = i-1;
        //內層循環:從右到左(首先要進行數組越界判斷)
        while (j >= 0 && temp < R[j]) {
            //一邊尋找插入點一邊後移
            R[j+1] = R[j];
            --j;
        }
        //插入位置
        R[j+1] = temp;
    }   
}

方法改進:監視哨

對於待排序表R[1..N],由於R[0]不設置元素,在R[0]處設置“監視哨”,作用:

  • 相當於越界判斷
  • 相當於temp

——>改進代碼:

/**
  *  帶監視哨的直接插入排序
  * @param R[1..N] 待排序表
  * @param n 序列個數
  */
void insertSort1(int[] R, int n) {
    int i, j;
    // 外層循環
    for (i = 1; i < n; i++) {
        R[0] = R[i];
        j = i-1;
        //內層循環(不需要j>=1)
        while (R[0] < R[j]) {
            R[j+1] = R[j];
            --j;
        }
        // 插入位置
        R[j+1] = R[0];
    }   
}

性能分析

適用在序列基本有序的情況中。

  • 最好的情況:整個序列已經有序,時間複雜度O(n)
  • 最壞的情況:整個序列逆序,基本操作需要執行i=2n(i1) =n(n-1)/2,時間複雜度O(n2 )
  • 平均時間複雜度:O(n2 )
  • 空間複雜度:O(1),額外空間只有一個temp

2、折半插入排序

基本思想

其排序的思想與直接插入排序一樣,只不過在尋找插入點時不一樣:先通過二分法找到插入點,即low處,然後在後移元素(不能像直接插入排序一邊尋找一邊後移)

-------○■
----------○■
-------------○■
  • 雙層循環。
  • 外層:由小到大(從左到右)。由■代表i:待插入值的序號
  • 內層:從右到左。由○代表high=i-1(這裏high相當於直接插入排序中的j)
  • 比較的方式不同:先二分查找,在整體後移

代碼

/**
  * 折半插入排序
  * @param R[0..N-1] 待排序表
  * @param n 序列個數
  */
void binaryInsertSort(int[] R, int n) {
    //外層循環
    for (int i = 1; i < n; i++) {
        int temp = R[i];
        int low = 0;
        int high = i-1;
        //內層循環
        //①先尋找插入點
        while (low <= high) {
            int middle = (low + high) / 2;
            if (temp < R[middle]) {
                high = middle -1;
            }else {
                low = middle + 1;
            }   
        }
        //②插入點的索引就是low,然後後移元素
        for (int j = i; j > low; j--) {
            R[j] = R[j-1];
        }
        //插入
        R[low] = temp;  
    }
}

性能分析

折半插入排序適合序列數較多的場景。與直接插入排序相比,折半插入排序在尋找插入點所花費的時間將大大減少(比較次數與初始序列無關),但是在移動次數方面和直接插入排序一樣的。所以時間複雜度與直接插入排序是一樣的。

  • 最好的情況:整個序列已經有序,時間複雜度O(n)
  • 最壞的情況:整個序列逆序,時間複雜度O(n2 )
  • 平均時間複雜度:O(n2 )
  • 空間複雜度:O(1)

3、希爾排序

基本思想

將待排序表劃分爲若干組(步長d),在每組中進行直接插入排序,通過縮小步長使序列逐漸有序。

  • 注意是三層循環:最外層循環用於縮量步長,後兩次循環按照直接插入排序的過程

——>爲啥需要希爾排序?

  • 我們知道直接插入排序適合序列基本有序的情況,希爾排序在每次迭代(最外層循環)中通過縮小增量步長的方式來使整個序列逐漸基本有序
  • 元素個數越少,直接插入排序的效率越高

——>步長的取值

依次逐漸縮小:d1 = n/2,d2 = d1 /2,…,dk = 1
注意,最後一趟的dk 一定要爲1

——>例子:

這裏寫圖片描述

代碼

/**
 * 希爾排序
 * @param R[0..N-1] 待排序表
 * @param n 元素個數
 */
void shellSort(int R[], int n) {
    //增加步長d
    for (int d = n/2; d >= 1 ; d /= 2) {
        //以下按照直接插入排序的過程
        for (int i = 0 + d; i < n; i++) {
            int temp = R[i];
            //相當於直接插入排序中的j = i-1
            int j = i-d;
            while (j > 0 && temp < R[j]) {
                //後移d位
                R[j+d] = R[j];
                j = j-d;
            }
            R[j+d] = temp;
        }
    }
}

性能分析

  • 時間複雜度O(nlog2 n):由於每一趟的序列都認爲基本有序,則各趟的時間複雜度爲O(n);總共需要的趟數爲log2 n
  • 空間複雜度O(1)

二、交換排序

基本思想:兩兩比較待排序表的元素,發現倒序就交換。比較/交換的位置不同出現不同的方法。

  • 冒泡排序:相鄰位置比較
  • 快速排序:與選出的中間元素比較

1、冒泡排序

基本思想

冒泡一趟,一定能將該趟序列中的最大(最小)元素交換到最終的位置。

------------
----------
--------
  • 雙層循環
  • 外層:由大到小(從右到左)。由■代表i:每趟逐漸減小i。從i=n-1開始,到i=1結束
  • 內層:從左到右。由○代表j:一趟比較序號從j=1開始,到j=i結束(下標0基址)

代碼

/**
 * 冒泡排序
 * @param R[0..N-1] 待排序表
 * @param n 元素個數
 */
void bubbleSort(int[] R, int n) {
    //外層循環
    for (int i = n-1; i >= 1; i--) {
        //用來標記該趟排序是否發生了交換
        boolean flag = false;
        //內層循環
        for (int j = 1; j <= i; j++) {
            if (R[j-1] > R[j]) {
                //交換
                int temp = R[j];
                R[j] = R[j-1];
                R[j-1] = temp;

                flag = true;
            }
        }
        //一趟排序過程中沒有發生交換,說明序列已經有序
        if (!flag) {
            return;
        }
    }
}

性能分析

  • 最好的情況:整個序列已經有序,僅需要一趟排序,執行n-1次,時間複雜度O(n)
  • 最壞的情況:整個序列逆序,基本操作需要執行n(n-1)/2,時間複雜度O(n2 )
  • 平均時間複雜度:O(n2 )
  • 空間複雜度:O(1),額外空間只有一個temp

2、快速排序

基本思想

分治法的思想(在分階段同時進行排序):首先選定一個元素作爲中間元素,然後將表中所有元素與該元素比較,比它小的調到表的前面,比它大的調到表的後面。一趟排序完後以中間元素爲分裂點將表分爲左右兩個子表繼續排序。

通用的快速排序思想:首先選擇表頭作爲中間元素temp。然後,從j開始掃描,遇到小於temp的停止掃描,將A[i](此時的i在中間元素位置,並保存在temp中)與A[j]交換,然後i++。接着,從i開始掃描,遇到大於temp的停止掃描,將A[j]與A[i]交換,然後j- -。以此類推,直到i與j交叉或相遇,將temp賦值到A[i]中。

具體分析詳見:分治法中的合併排序和快速排序

代碼

/**
 * 快速排序
 * @param R[0..N-1] 待排序表
 * @param n 元素個數
 */
void quickSort(int[] R,int l,int r){
     if (l < r) {   
          int i = l;
          int j = r;
          int temp = R[l];  
          while (i < j) {  
              //i<j爲越界限制
              while(i < j && R[j] > temp) // 從右向左找第一個小於x的數  
                  j--;    
              if(i < j)   
                  R[i++] = R[j];  

              while(i < j && R[i] < temp) // 從左向右找第一個大於等於x的數  
                  i++;    
                  if(i < j)   
                      R[j--] = R[i];  
           }  
           R[i] = temp;  
           quickSort(R, l, i-1);
           quickSort(R, i+1, r);
      }
}

性能分析

從平均時間性能來說,快速排序目前被認爲是最好的一種內部排序方法。

  • 一趟劃分比較的時間複雜度固定在O(n)
  • 最好的情況:每趟都將子表等分成兩部分,需要log2 n趟,理想的時間複雜度爲O(nlog2 n)
  • 最壞的情況:序列基本有序,每次選取的中間元素要麼最大要麼最小,劃分成一個空表一個n-1的子表,則需要n-1趟,最壞的時間複雜度O(n2 )
  • 平均時間複雜度:趨向於最好的情況,O(nlog2 n)
  • 空間複雜度:O(log2 n),遞歸需要棧的輔助

三、選擇排序

基本思想:在每一趟排序中,在待排序表中都選出最大或最小的元素放到最終的位置。選擇的方式不同,出現不同的方法。

  • 直接選擇排序
  • 堆排序

1、直接選擇排序

基本思想

每趟選擇完,都將待排序表中的最大(小)元素放到表後(前)。這裏每次選的是最小元素。

■○----------N-1
  ■○--------N-1
    ■○------N-1
  • 雙層循環
  • 外層:由大到小(但從左到右)。由■代表i:每趟也逐漸減小i。但從i=0開始,到i=n-1結束
  • 內層:從左到右。由○代表j:一趟比較序號從j=i+1開始,到j=n-1結束

代碼

/**
 * 直接選擇排序
 * @param R R[0..N-1] 待排序表
 * @param n 元素個數
 */
void selcetSort(int[] R, int n) {
    int i, j;
    int minIndex;
    //外層循環
    for (i = 0; i < n; i++) {
        minIndex = i;
        //內層循環
        for (j = i+1; j < n; j++) {
            //選出最小元素索引
            if (R[j] < R[minIndex]) {
                minIndex = j;
            }
        }
        if (minIndex != i) {
            //交換
            int temp = R[i];
            R[i] = R[minIndex];
            R[minIndex] = temp;
        }   
    }
}

性能分析

  • 時間複雜度:由於比較次數固定:(n-1 + 1)(n-1)/2 = n(n-1)/2,且與初始序列無關。所以時間複雜度爲O(n2 )
  • 空間複雜度:O(1),額外空間只有一個temp

2、堆排序

基本思想

可以把堆看成一顆完全二叉樹。必須滿足任何一個非葉子節點的值不大於(小根堆)或不小於(大根堆)左右孩子節點的值。

堆排序的過程:初始建堆——>n-1調整——>n-1-1調整——>…

建立完一趟的大根堆,並不意味着排序完成。雖然可以得到最大的元素,但是無法知道左右孩子的正確序列。所以需要將根刪除繼續n-1建堆,直到堆規模只剩下一個元素。

建堆的過程:先將待排序錶轉變成完全二叉樹形式,然後建堆,可以參考:圖解排序算法(三)之堆排序

但是要記住:從第一個非葉子節點開始,從右到左,從下到上,對每個節點進行調整,最終得到一顆大根堆。

代碼

  • 注:序列的下標從1開始。則2i爲節點i的左孩子,2i+1爲節點i的右孩子
/**
 * 堆排序 
 * @param R[1..N] 待排序表
 * @param n 元素個數
 */
void heapSort(int[] R, int n) {
    //初始建堆
    //從n/2表示最後一個非葉子節點開始,從右到左,從下到上
    for (int i = n/2; i >= 1; i--) {
        sift(R, i, n);
    }
    for (int i = n; i >= 2 ; i--) {
        //交換,將根節點放到序列最終位置
        int temp = R[1];
        R[1] = R[i];
        R[i] = temp;
        //往後的調整,每次只需要從頭結點開始就可以了
        sift(R, 1, i-1);
    }       
}

/**
 * 篩選
 * @param R
 * @param low
 * @param high
 */
void sift(int[] R, int low, int high) {
    //節點i,和其左孩子2i
    int i = low, j = 2*i;
    int temp = R[i];
    while (j <= high) {
        //如果右孩子大,則j指向有孩子
        if (j < high && R[j] < R[j+1]) {
            ++j;
        }
        //如果父節點小於孩子,賦值父節點
        if (temp < R[j]) {
            R[i] = R[j];
            i = j;
            //繼續比較下去,孩子的孩子節點是否大於temp
            j = 2*i;
        }else {
            break;
        }
    }
    R[i] = temp;    
}

性能分析

  • 時間複雜度爲:O(nlog2 n)
    • 篩選操作的時間:堆排序算法花費的時間最多用在初始建堆和調整時所進行的篩選上。每個節點篩選的時間複雜度爲O(log2 n)(解釋:完全二叉樹的高度k=log2 n+1,最多需要比較2(k-1)次)。
    • 初次建堆的時間:初始建堆需要n/2次篩選操作
    • 調整操作的時間:剩下需要n-1調整操作,每次只需要篩選頭節點
    • 因此堆排序算法的整個基本操作次數爲O(log2 n)x(n/2) + O(log2 n)x(n-1)。簡化後時間複雜度爲O(nlog2 n)
  • 空間複雜度:O(1),額外空間只有一個temp
  • 堆排序適合的場景是記錄數很多的情況,比如從10000個記錄中選出前10個最小的。

四、歸併排序

基本思想

所謂歸併是指將兩個或兩個以上的有序表合併成一個新的有序表。歸併排序也可以歸納爲分治法的思想,但是重點在合併階段。

具體分析及代碼詳見:分治法中的合併排序和快速排序

性能分析

  • 時間複雜度爲:O(nlog2 n)。其中,一趟排序的時間複雜度爲O(n),需要 log2 n趟遞歸
  • 空間複雜度:O(n),需要存儲整個待排序表

參考

  • 《數據結構(C++描述)》 胡學剛 張晶 主編 人民郵電出版社
  • 《2015版數據結構高分筆記》
  • 圖解排序算法
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章