本文分析冒泡、選擇、插入、希爾、快速、歸并和堆排序,為不影響閱讀體驗,將關于時間、空間復雜度和穩定性的概念放在博文后半部分,建議在閱讀每種算法指標分析前先理解這些概念。為了對以下各個算法進行方便的測試,測試主方法體如下(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個數開始比較,直到第n-1個數與第n個數結束。到最后,最大(小)的數就"浮"在了最上面。
- 持續每次對前面越來越少的未排序元素重復上面的步驟,直到所有元素有序。
排序演示
源代碼(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位置),直至有序。
步驟
- 未排序序列中找到最?。ù螅┰?,存放到序列的起始位置。
- 再從剩余未排序元素中繼續找到最?。ù螅┰?,放到已排序序列的末尾。
- 以此類推,直到所有元素均排序完畢。
排序演示
源代碼(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個元素,在已排序序列中從后向前掃描
- 如果被掃描的元素大于新元素,將該元素后移一位
- 重復步驟3,直到找到已排序的元素小于或者等于新元素的位置
- 將新元素插入到該位置后
- 重復步驟2~5
- 如果全部有序,結束迭代
排序演示
源代碼(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)
希爾排序,也稱遞減增量排序算法,實質上是一種分組插入排序算法。是插入排序的一種更高效的改進版本。
希爾排序是基于插入排序的以下兩點性質而提出改進方法的:
- 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率
- 但插入排序一般來說是低效的,因為插入排序每次只能將數據移動一位
步驟
- 假設數組為
{5, 4, 7, 1, 6, 2, 8, 9, 10}
,如果我們以步長為4開始進行排序,我們可以通過將這列表放在有4列的表中來更好地描述算法,這樣他們就應該看起來是這樣:
5, 4, 7, 1
6, 2, 8, 9
10
- 將數組列在一個表中并對列分別進行插入排序
5, 2, 7, 1
6, 4, 8, 9
10
- 重復這過程,不過每次用更長的列(步長更小,列數更少)來進行,如下步長為2。
5, 2
7, 1
6, 4
8, 9
10
- 最后整個表就只有一列了( 再進行插入排序)
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)
使用到二叉堆這種數據結構,二叉堆是平衡二叉樹。它具有以下性質:
- 父節點的值大于等于左右節點的值,為最大堆(大頂堆);父節點的值小于于等于左右節點的值,為最小堆(大頂堆)
- 每個父節點的左右節點都是二叉堆(大頂堆或小頂堆)
本質上堆排序是由數組實現。
步驟
- 調用構造大頂堆方法,將數組構造成大頂堆。葉子節點相當于大頂堆,假設數組下標范圍為0~n-1,則從下標n/2開始的元素(葉子節點)均為大頂堆。所以從下標n/2-1開始的元素(父節點)開始,向前依次(下標減1)構造大頂堆。
- 堆排序:由于堆是用數組模擬的。構造的大頂堆相當于在數組中無序。因此需要將數組有序化。思想是循環將根節點與最后一個未排序元素交換,再調用構造大頂堆方法。循環結束后,整個數組就是有序的了。
- 構造大頂堆方法:調整節點,使得左右子節點小于父節點,保證根節點是當前堆中最大的元素。
排序演示:
第一次將數組構造成大頂堆時,跟此演示稍有不同,默認按數組順序放入二叉堆,沒有邊放邊構造。
源代碼(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。此時左、右序列中只有一個元素,通過比較將左、右序列合并為左序列后,再遞歸分解右序列(也分解到只有一個元素)。
排序演示
此圖主要思想一致,但代碼實現過程中,6531合并完成時,8724還沒分解。
源代碼(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)的其他算法更快。
步驟
- 將數組區塊的第n個素設為中數(中位值);第1個元素設為左數;第n-1個元素設為右數。
- 左數下標遞增,右數下標遞減。將大于等于中數的左數和小于等于中數的右數交換,直到左數和右數下標相等。如果左(右)數大于等于中數,交換位置,否則加左數下標1。
- 對中數的左右數組區間遞歸執行第1,2步,直至各數組區間只有一個數。
排序演示
此演示的第1次選擇3作為中數,與題意稍有出入,其他均相同。
源代碼(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))所表達的就是樹干,只關心其中的主干,其他的細枝末節全都拋棄不管。
由上圖可見,常見的算法時間復雜度由小到大依次為:Ο(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) = 2log 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)。
穩定性
在排序過程中,具有相同數值的對象的相對順序被不被打亂。如果可以保證不被打亂就是穩定的,如果不能保證就是不穩定的。
總結
根據每個排序算法關于穩定性的指標分析,可以得出以下結論:
- 如果每次變換都只是交換相鄰的兩個元素,那么就是穩定的。
- 如果每次都有某個元素和比較遠的元素的交換操作,那么就是不穩定的。
以上算法博主親測都能正確運行。以下是上述七種算法的性能指標分析對比情況。
排序方式 | 平均時間復雜度 | 最好時間復雜度 | 最壞時間復雜度 | 空間復雜度 (輔助空間) |
穩定性 |
---|---|---|---|---|---|
冒泡排序 | 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) | 不穩定 |
參考資料
- 維基百科:希爾排序 、快速排序
- 經典排序算法總結與實現 ---Python實現
- 白話經典算法系列之一 冒泡排序的三種實現
- 幾種經典排序算法
- 算法的時間復雜度和空間復雜度-總結
- 排序算法的穩定性