淺析七種經典排序算法

本文分析冒泡、選擇、插入、希爾、快速、歸并和堆排序,為不影響閱讀體驗,將關于時間、空間復雜度和穩定性的概念放在博文后半部分,建議在閱讀每種算法指標分析前先理解這些概念。為了對以下各個算法進行方便的測試,測試主方法體如下(Java實現):

public class Sort {
   public static void main(String[] args) {
        int[] input = {5, 4, 7, 1, 6, 2, 8, 9, 10};
        //此處調用方法,以調用冒泡排序為例
        bubbleSort(input);
        for (int i = 1; i <= input.length; i++) {
            System.out.println(input[i - 1]);
        }
    }
}

本篇博文所有排序實現均默認從小到大

冒泡排序(Bubble Sort)

每一次通過兩兩比較找到最大(?。┑脑胤旁谖磁判蛐蛄械淖詈?,直至有序。

步驟

  1. 比較兩個相鄰的元素。如果前面比后面個大(?。?,就交換順序。
  2. 從第1個數與第2個數開始比較,直到第n-1個數與第n個數結束。到最后,最大(小)的數就"浮"在了最上面。
  3. 持續每次對前面越來越少的未排序元素重復上面的步驟,直到所有元素有序。

排序演示

image

源代碼(Java實現)

public static void bubbleSort(int[] input) {
        for (int i = 1; i <= input.length; i++) {
            for (int j = 1; j <= input.length - i; j++) {
                if (input[j - 1] > input[j]) {
                    int temp = input[j - 1];
                    input[j - 1] = input[j];
                    input[j] = temp;
                }
            }
        }
}

算法指標分析

優化1的最好時間復雜度:當序列有序時。第一個for循環中,第2/3/11/12行語句執行1次,即頻度為1;第二個for循環中,第4、5、9行語句執行n-1次,即頻度為n-1。T(n) = 3*(n-1)+4 = 3n+1 = O(n)。所以最好時間復雜度為O(n)。

最壞時間復雜度:當序列為逆序時。第一個for循環的頻度為n;第二個for循環中,j可以取 1,2,...,n-1,所以第3/4/6/7/8語句,頻度均為(1+n-1)(n-1)/2。T(n) = n(n-1)/2+n = O(n2)。所以最壞時間復雜度為O(n2)。

我們可以看出在嵌套層數多的循環語句中,由最內層語句的頻度f(n)決定時間復雜度。

平均時間復雜度:O((n+n^2)/2) = O(n^2)。

輔助空間:不需要額臨時的存儲空間來運行算法,所以為O(1)。

穩定性:因為排序方式是相鄰數比較后交換,如果序列中有相等的兩個數,待兩數相鄰時,不會交換兩者的位置,所以穩定。

冒泡排序的兩種優化方式

優化1:某一趟遍歷如果沒有元素交換,flag標記依舊為true。說明已經排好序了,結束迭代。

public static void bubbleSort2(int[] input) {
        for (int i = 1; i <= input.length; i++) {
            boolean flag = true;
            for (int j = 1; j <= input.length - i; j++) {
                if (input[j - 1] >  input[j]) {
                    int temp = input[j - 1];
                    input[j - 1] = input[j];
                    input[j] = temp;
                }
                flag = false;
            }
            if (flag)
                break;
        }
}

優化2:記錄某次遍歷時最后發生元素交換的位置(LastExchange),這個位置之后的數據顯然已經有序,不用再排序了。因此通過記錄最后發生元素交換的位置就可以確定下次循環的范圍。

public static void bubbleSort3(int[] input) {
        int LastExchange = input.length;
        boolean flag = true;
        for (int i = 1; i <= input.length; i++) {
            int k = LastExchange;
            for (int j = 1; j <= k - 1; j++) {
                if (input[j - 1] > input[j]) {
                    int temp = input[j - 1];
                    input[j - 1] = input[j];
                    input[j] = temp;
                }
                LastExchange = j;
                flag = false;
            }
            if (flag)
                break;
        }
}

選擇排序(Selection Sort)

每一次通過選擇找到未排序序列的最?。ù螅┰胤旁谝雅判蛐蛄械哪┪玻ń粨Q位置),直至有序。

步驟

  1. 未排序序列中找到最?。ù螅┰?,存放到序列的起始位置。
  2. 再從剩余未排序元素中繼續找到最?。ù螅┰?,放到已排序序列的末尾。
  3. 以此類推,直到所有元素均排序完畢。

排序演示

image

源代碼(Java實現)

public static void selectionSort(int[] input) {
        for (int i = 1; i <= input.length; i++) {
            int minIndex = i - 1;
            for (int j = i; j < input.length; j++) {
                if (input[minIndex] > input[j])
                    minIndex = j;
            }
            if (minIndex != i - 1) {
                int temp = input[minIndex];
                input[minIndex] = input[i - 1];
                input[i - 1] = temp;
            }
        }
}

算法指標分析

不管序列有序還是逆序,第二個for循環的頻度為n*(n-1)/2。所以最壞時間復雜度和最好時間復雜度均為O(n^2)。

平均時間復雜度:O(n^2)

輔助空間:不需要額臨時的存儲空間來運行算法,所以為O(1)。

穩定性:不穩定。舉例,5、7、5、2、9,找到最小2時,2與5交換,此時兩個5的相對位置發生改變。

插入排序(Insertion Sort)

對于每個未排序元素,在已排序序列中從后向前掃描,找到相應位置并插入。

步驟

  1. 第1個元素視為已經被排序
  2. 取未排序的第1個元素,在已排序序列中從后向前掃描
  3. 如果被掃描的元素大于新元素,將該元素后移一位
  4. 重復步驟3,直到找到已排序的元素小于或者等于新元素的位置
  5. 將新元素插入到該位置后
  6. 重復步驟2~5
  7. 如果全部有序,結束迭代

排序演示

image

源代碼(Java實現)

public static void insertionSort(int[] input) {
        for (int i = 1; i < input.length; i++) {
            for (int j = i; j > 0; j--) {
                if (input[j] < input[j - 1]) {
                    int temp = input[j];
                    input[j] = input[j - 1];
                    input[j - 1] = temp;
                }
                else break;
            }
        }
}

算法指標分析

最好時間復雜度:當序列有序時。當第一個for循環運行時,在第二個for循環中,因為序列有序,所以只會相鄰比較1次,就跳出循環。所以兩個循環的頻度均為n-1,所以最好時間復雜度均為O(n)。

最壞時間復雜度:當序列逆序時。第二個for循環的頻度為n(n-1)/2。所以最壞時間復雜度均為O(n^2)。

平均時間復雜度:O((n+n^2)/2) = O(n^2)。

輔助空間:不需要額臨時的存儲空間來運行算法,所以為O(1)。

穩定性:因為排序方式是兩兩比較,序列中如果相鄰兩個數相等,不會交換兩者的位置,所以穩定。

希爾排序(Shell Sort)

希爾排序,也稱遞減增量排序算法,實質上是一種分組插入排序算法。是插入排序的一種更高效的改進版本。
希爾排序是基于插入排序的以下兩點性質而提出改進方法的:

  • 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率
  • 但插入排序一般來說是低效的,因為插入排序每次只能將數據移動一位

步驟

  1. 假設數組為{5, 4, 7, 1, 6, 2, 8, 9, 10},如果我們以步長為4開始進行排序,我們可以通過將這列表放在有4列的表中來更好地描述算法,這樣他們就應該看起來是這樣:

5, 4, 7, 1
6, 2, 8, 9
10

  1. 將數組列在一個表中并對列分別進行插入排序

5, 2, 7, 1
6, 4, 8, 9
10

  1. 重復這過程,不過每次用更長的列(步長更小,列數更少)來進行,如下步長為2。

5, 2
7, 1
6, 4
8, 9
10

  1. 最后整個表就只有一列了( 再進行插入排序)

5, 1, 6, 2, 7, 4, 8, 9, 10

源代碼(Java實現)

public static void shellSort(int[] input) {
        int len = input.length, j;
        int gap = Math.round(len / 2);
        for (; gap > 0; gap /= 2) //步長每次就減少一倍
            for (int i = gap; i < len; i++) {
                int temp = input[i];
                for (j = i - gap; j >= 0 && temp < input[j]; j -= gap) {//列排序
                    input[j + gap] = input[j];
                    input[j] = temp;
                }
            }
}

算法指標分析

最好時間復雜度:當序列有序時。最好時間復雜度均為O(n^s),1<s<2,跟算法步長有關。

最壞時間復雜度:當序列逆序時。最壞時間復雜度均為O(n^2)。

平均時間復雜度:O(n log n)~O(n^2)。

輔助空間:不需要額臨時的存儲空間來運行算法,所以為O(1)。

穩定性:不穩定。解釋如下:

假設,序列為5,5,7,3,2,取步長為3分組為
5,5,7
3,6
3和5交換位置后,兩個5的相對順序發生改變,所以不穩定

堆排序(HeapSort)

使用到二叉堆這種數據結構,二叉堆是平衡二叉樹。它具有以下性質:

  1. 父節點的值大于等于左右節點的值,為最大堆(大頂堆);父節點的值小于于等于左右節點的值,為最小堆(大頂堆)
  2. 每個父節點的左右節點都是二叉堆(大頂堆或小頂堆)

本質上堆排序是由數組實現。

步驟

  1. 調用構造大頂堆方法,將數組構造成大頂堆。葉子節點相當于大頂堆,假設數組下標范圍為0~n-1,則從下標n/2開始的元素(葉子節點)均為大頂堆。所以從下標n/2-1開始的元素(父節點)開始,向前依次(下標減1)構造大頂堆。
  2. 堆排序:由于堆是用數組模擬的。構造的大頂堆相當于在數組中無序。因此需要將數組有序化。思想是循環將根節點與最后一個未排序元素交換,再調用構造大頂堆方法。循環結束后,整個數組就是有序的了。
  3. 構造大頂堆方法:調整節點,使得左右子節點小于父節點,保證根節點是當前堆中最大的元素。

排序演示:

第一次將數組構造成大頂堆時,跟此演示稍有不同,默認按數組順序放入二叉堆,沒有邊放邊構造。

image

源代碼(Java實現)

public static void heapSort(int[] input) {
        int i = input.length / 2 - 1;//最后一個非葉子節點
        //將數組構造成大頂堆
        for (; i >= 0; i--)
            maxHeapify(input, i, input.length - 1);
        //堆排,將大頂堆轉換成有序數組
        for (i = input.length - 1; i >= 0; i--) {
            int temp = input[0];
            input[0] = input[i];
            input[i] = temp;
            maxHeapify(input, 0, i - 1);
        }
}
 //構造大頂堆
public static void maxHeapify(int[] input, int start, int end) {
        int child;
        int root = start;
        //父節點不是葉子節點時循環;只有一個根節點時不再比較
        for (; root <= (end - 1) / 2 && end > 0; ) {
            child = root * 2 + 1;//調整子節點
            //取較大的子節點
            if (child + 1 <= end && input[child] < input[child + 1])
                child += 1;

            if (input[root] < input[child]) {
                //交換較小父節點和較大子節點的位置
                int temp = input[root];
                input[root] = input[child];
                input[child] = temp;
                root = child;//較大的子節點成為父節點
            } else
                break;
        }
}

算法指標分析

最好時間復雜度:當序列有序時。最好時間復雜度均為O(n log n),跟算法步長有關。

最壞時間復雜度:當序列逆序時。最壞時間復雜度均為O(n log n)。

平均時間復雜度:O(n log n)。

輔助空間:不需要額臨時的存儲空間來運行算法,所以為O(1)。

穩定性:不穩定。

二分歸并排序(MergeSort)

先遞歸分解序列,再合并序列。也采用了分治法的思想。

步驟

  1. 合并:合并的條件是兩個序列都有序,當序列中只有1個元素時我們認為也是有序的;合并的方法是通過比較將較小元素先放入臨時數組,再將左、右序列剩余元素放入。
  2. 分解:先將源序列從中位數(中數)分開為左右序列,遞歸分解左序列,中數不斷減小,直到為1。此時左、右序列中只有一個元素,通過比較將左、右序列合并為左序列后,再遞歸分解右序列(也分解到只有一個元素)。

排序演示

此圖主要思想一致,但代碼實現過程中,6531合并完成時,8724還沒分解。

image

源代碼(Java實現)

public static void mergeSort(int[] input) {
        merge_sort(input, 0, input.length - 1);
    }
public static void merge_sort(int[] input, int start, int end) {
        int middle;
        //當序列中只有一個元素時,結束當前遞歸
        if (start < end) {
            middle = (start + end) / 2;//找出中位數(中數),中數越來越小
            merge_sort(input, start, middle);//中數左側序列二分
            merge_sort(input, middle + 1, end);//中數右側序列二分
            merge(input, start, middle, end);//合并成源序列
        }
}
//合并成源序列
public static void merge(int[] input, int left, int middle, int right) {
        int[] temp = new int[right - left + 1];//用于存放新排好序的序列
        int i = left;//左序列的起始下標
        int j = middle + 1;//右序列的起始下標
        int n = 0;//temp[]數組的起始下標
        //通過比較將較小元素先放入temp數組
        while (i <= middle && j <= right) {
            if (input[i] < input[j]) {
                temp[n] = input[i];
                i++;
            } else {
                temp[n] = input[j];
                j++;
            }
            n++;
        }
        //將第一個序列的剩余元素放入temp[]
        while (i <= middle) {
            temp[n] = input[i];
            i++;
            n++;
        }
        //將第二個序列的剩余元素放入temp[]
        while (j <= right) {
            temp[n] = input[j];
            j++;
            n++;
        }
        //將num[]中的元素復制到數組input
        for (int x = 0, y = left; x <= n && y <= right; x++, y++)
            input[y] = temp[x];
}

算法指標分析

最好時間復雜度:當序列有序時。最好時間復雜度均為O(n log n)。

最壞時間復雜度:當序列逆序時。最壞時間復雜度均為O(n log n)。

平均時間復雜度:O(n log n)。

輔助空間:需要新建額外臨時的存儲空間來存儲新排好序的序列,每一次歸并都需要重新新建,新建頻度為n,所以輔助空間為O(n)。

穩定性:穩定。因為兩兩比較。

快速排序(Quick Sort)

又稱劃分交換排序(partition-exchange sort)。采用了分治法的思想,通常明顯比同為Ο(n log n)的其他算法更快。

步驟

  1. 將數組區塊的第n個素設為中數(中位值);第1個元素設為左數;第n-1個元素設為右數。
  2. 左數下標遞增,右數下標遞減。將大于等于中數的左數和小于等于中數的右數交換,直到左數和右數下標相等。如果左(右)數大于等于中數,交換位置,否則加左數下標1。
  3. 對中數的左右數組區間遞歸執行第1,2步,直至各數組區間只有一個數。

排序演示

此演示的第1次選擇3作為中數,與題意稍有出入,其他均相同。

image

源代碼(Java實現)

public static void quickSort(int[] input) {
        quick_sort(input, 0, input.length - 1);
}

private static void quick_sort(int[] input, int start, int end) {
        if (start >= end) {
            return;
        }
        int left = start, right = end - 1;
        int mid = input[end];
        while (left < right) {
            while (input[left] <= mid && left < right) {
                left++;
            }
            while (input[right] >= mid && left < right) {
                right--;
            }
            int temp = input[left];
            input[left] = input[right];
            input[right] = temp;
        }
        if (input[left] >= input[end]) {
            int temp = input[left];
            input[left] = input[end];
            input[end] = temp;
        } else
            left++;
        quick_sort(input, start, left - 1);
        quick_sort(input, left, end);
}

算法指標分析

最好時間復雜度:當序列有序時。所以,最好時間復雜度均為O(n log n)。

最壞時間復雜度:當序列逆序時。所以,最壞時間復雜度均為O(n^2)。

平均時間復雜度:O(n log n)。

輔助空間:需要額臨時的存儲空間來運行算法,所以為O(n log n)~O(n)。

穩定性:不穩定。

時間復雜度

時間頻度:一個算法花費的時間與算法中語句的執行次數成正比例,哪個算法中語句執行次數多,它花費時間就多。一個算法中的語句執行次數稱為語句頻度或時間頻度。記為T(n)。

時間復雜度:在剛才提到的時間頻度中,n稱為問題的規模,當n不斷變化時,時間頻度T(n)也會不斷變化。但有時我們想知道它變化時呈現什么規律。為此,我們引入時間復雜度概念。 一般情況下,算法中基本操作重復執行的次數是問題規模n的某個函數,用T(n)表示,若有某個輔助函數f(n),使得當n趨近于無窮大時,T(n)/f(n)的極限值為不等于零的常數C,則稱f(n)是T(n)的同數量級函數。記作T(n)=O(f(n)),稱O(f(n)) 為算法的漸進時間復雜度,簡稱時間復雜度。

雖然對f(n)沒有規定,但是一般都是取盡可能簡單的函數。例如,O(2n^2+n+1) = O(3n^2+n+3) = O(7n^2 + n) = O(n^2) ,一般都只用O(n^2)表示就可以了。注意到大O符號里隱藏著一個常數C,所以f(n)里一般不加系數。如果把T(n)當做一棵樹,那么O(f(n))所表達的就是樹干,只關心其中的主干,其他的細枝末節全都拋棄不管。

image

由上圖可見,常見的算法時間復雜度由小到大依次為:Ο(1)<Ο(log n)<Ο(n)<Ο(nlog n)<Ο(n2)<Ο(n3)<…<Ο(2^n)<Ο(n!)。

分析以下代碼的時間復雜度。

i=1;     
while (i<=n)  
  i=i*2;  

第1行語句的頻度為1
設第3行語句的時間頻度為f(n)k,2f(n) = n; -->f(n) = log n
第2行語句跟第2三行的頻度一樣,為log n
以上代碼的T(n) = 2
log n+1 = O(log n)

由此可見,T(n)由最大的f(n)決定。在嵌套層數多的循環語句中,由最內層語句的頻度f(n)決定T(n)。
注:log n = log?n = lg n;復雜度中以2為底,不是數學中以10為底。

空間復雜度

一個算法的空間復雜度(Space Complexity)定義為該算法所耗費的存儲空間,它也是問題規模n的函數。漸近空間復雜度也常常簡稱為空間復雜度。

一個算法在計算機存儲器上所占用的存儲空間,包括:

  • 存儲算法本身所占用的存儲空間
  • 算法的輸入輸出數據所占用的存儲空間
  • 算法在運行過程中臨時占用的存儲空間

我們將算法在運行過程中臨時占用的存儲空間稱為輔助空間,它隨算法的不同而異,另外兩種存儲空間與算法本身無關。

如當一個算法的空間復雜度為一個常量,即不隨被處理數據量n的大小而改變時,輔助空間可表示為O(1);當一個算法的空間復雜度與以2為底的n的對數成正比時,輔助空間可表示為O(1og?n);當一個算法的空間復雜度與n成線性比例關系時,輔助空間可表示為O(n)。

穩定性

在排序過程中,具有相同數值的對象的相對順序被不被打亂。如果可以保證不被打亂就是穩定的,如果不能保證就是不穩定的。

總結

根據每個排序算法關于穩定性的指標分析,可以得出以下結論:

  1. 如果每次變換都只是交換相鄰的兩個元素,那么就是穩定的。
  2. 如果每次都有某個元素和比較遠的元素的交換操作,那么就是不穩定的。

以上算法博主親測都能正確運行。以下是上述七種算法的性能指標分析對比情況。

排序方式 平均時間復雜度 最好時間復雜度 最壞時間復雜度 空間復雜度
(輔助空間)
穩定性
冒泡排序 O(n^2) O(n) O(n^2) O(1) 穩定
選擇排序 O(n^2) O(n^2) O(n^2) O(1) 不穩定
插入排序 O(n^2) O(n) O(n^2) O(1) 穩定
希爾排序 O(n log n)~O(n^2) O(n^s)
(0<s<1,跟步長有關)
O(n^2) O(1) 不穩定
堆排序 O(n log n) O(n log n) O(n log n) O(1) 不穩定
二分歸并排序 O(n log n) O(n log n) O(n log n) O(n) 穩定
快速排序 O(n log n) O(n log n) O(n^2) O(log n)~O(n) 不穩定

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 概述 排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部...
    蟻前閱讀 5,222評論 0 52
  • 概述:排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部...
    每天刷兩次牙閱讀 3,746評論 0 15
  • 該系列文章主要是記錄下自己暑假這段時間的學習筆記,暑期也在實習,抽空學了很多,每個方面的知識我都會另起一篇博客去記...
    Yanci516閱讀 12,282評論 6 19
  • 風景很美 用心去發現噢??
    花生花二三說閱讀 329評論 2 4
  • 1996年初次見到你,之后的每逢盛夏,我都為你著迷。 科比,是我們一代人的青春記憶,不管科黑還是科蜜,對你有多少愛...
    浮生幻想閱讀 273評論 0 0