相關文章
演算法(一)時間複雜度
前言
排序是演算法的基礎,排序有很多種方法,有些方法實現起來很簡單,但是效率較差,我們可以將這些排序的方法稱之為初等排序。這篇文章我們就來學習初等排序中的插入排序和氣泡排序。
1.插入排序
插入排序比較容易想到,思路與打撲克時排列牌的順序是類似的。比如我們左手拿牌,然後用右手將牌從左到右,從小到大來排序,這就需要我們把需要進行排列的牌抽出來放到合適的位置,並且不斷的重複,直到牌的順序排好,這個過程就可以理解為插入排序。
圖解插入排序
插入排序過程中會將需要排序的陣列,分為兩個部分:已排序部分和未排序部分,如下圖所示。
從圖中可以看出這個陣列分為兩個部分,其中下標為0、1、2的元素為已排列部分,其餘的則為未排列部分。
插入的排序規則:
將開頭元素視為以排序部分。接著執行如下的處理,直到沒有未排序部分。
- 取出未排序部分的開頭元素賦值給臨時儲存資料的變數v。
- 在已排列的部分將所有比v大的元素向後移動一個位置。
- 將取出的元素v插入空位。
按照這個規則,我們來舉一個簡單的例子。我們對陣列 a={8,3,1,5,2,1} 進行從小到大排序,陣列a如下圖所示。
我們對陣列a進行排序,共需要5個步驟:
1.接著我們將a[0]=8視為已排序,我們從a[1]開始操作,將a[1]的值3取出,3要小於a[0]的值8,因此將a[0]的值8移動到a[1],再把3插入到a[0],如下圖所示。
2.a[2]的值1要比a[0]和a[1]的值要小,則將a[0]和a[1]順次向後移一個位置,然後將1插入a[0],如下圖所示。
3.將a[3]中的5拿出來,比它大的是a[2]的8,因此8向後移,將5插入a[3]。如下圖所示。
4.將a[4]中2拿出來,發現a[1]、a[2]、a[3]中的值都比2大,因此將它們依次向後移,將2插入到a[1]中,如下圖所示。
5.最後將a[5]中的1移到合適的位置,過程和上面一樣,最後的排序結果如下圖所示。
實現插入排序
接下來要實現插入排序,針對下圖來定義變數。
如上圖所示,i代表未排序部分的開頭元素,v是臨時儲存a[i]值的變數, j代表已排序部分v要插入的位置。
根據定義的這三個變數,插入排序的實現思路就是:外層迴圈i從1開始自增,並在每次迴圈開始時將a[i]的值儲存在v中;內層迴圈則是j從i-1開始向前自減,並將比v大的元素從a[j]移動到a[j+1],並將v插入到當前j+1的位置(內層迴圈後,j會先自減1,因此插入的地方則是j+1的位置),當j等於-1或者a[j]小於等於v則內層迴圈結束。
接下來我們用程式碼來實現插入排序,如下所示。
public class InsertSort {
public static void main(String[] args) {
int a[] = {8, 3, 1, 5, 2, 1};
ArrayUtils.printArray(a);
int b[] = insert(a);
ArrayUtils.printArray(b);
}
public static int[] insert(int[] a) {
int i, j, v;
int n = a.length;
for (i = 1; i < n; i++) {
v = a[i];
j = i - 1;
while (j >= 0 && a[j] > v) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = v;
}
return a;
}
}複製程式碼
其中負責列印陣列的ArrayUtils類如下所示。
public class ArrayUtils {
public static void printArray(int[] array) {
System.out.print("{");
int len=array.length;
for (int i = 0; i < len; i++) {
System.out.print(array[i]);
if (i < len - 1) {
System.out.print(", ");
}
}
System.out.println("}");
}
}複製程式碼
輸出結果為:
{8, 3, 1, 5, 2, 1}
{1, 1, 2, 3, 5, 8}
插入排序的複雜度
根據演算法(一)時間複雜度所講的,我們來算一下插入排序的時間複雜度。在最壞的情況下,每個i迴圈都需要執行i次移動,總共需要1+2+......+n-1=n²/2-n/2,根據此前講過的推導大O階的規則的我們得出插入排序的時間複雜度為O(n²)。
2.希爾排序
插入排序對於大規模的亂序陣列,插入排序會很慢,因為它只會交換相鄰的元素,元素只能一點一點的從陣列的一端移動到另一端。如果最小的元素在陣列的末尾,則要將它移動到陣列的開頭則需要進行n-1次移動。
希爾排序原理
希爾排序改進了插入排序這一問題,它交換不相鄰的元素對陣列進行區域性排序,並最終用插入排序將區域性有序的陣列進行排序。
希爾排序的思想就是使得陣列中任意間隔h的元素都是有序的,這樣的陣列可以成為h有序陣列。這裡拿陣列a={4,8,9,1,10,6,2,5}為例,當h為4時,會將這個陣列分為h個子陣列。
從上圖可以看到,我們根據h=4,將陣列分為了四個子陣列,分別是{4,10}、{8,2}、{1,5}、{10,3}。我們分別對這四個子陣列進行區域性排序,接下來對h進行遞減操作,直到h為1,這樣最後一次迴圈就是一個典型的插入排序。
實現希爾排序
我們將陣列分為h個陣列,我們將子陣列的每個元素交換到比他大的元素前面去,只需要將插入排序的將移動元素的距離1改為h即可。這樣希爾排序的實現就轉換為了一個類似於插入排序但使用的增量不同的過程。
程式碼實現如下所示。
public class ShellSort {
public static void main(String[] args) {
int a[] = {4, 8, 9, 1, 10, 6, 2, 5};
ArrayUtils.printArray(a);
int b[] = shellSort(a);
ArrayUtils.printArray(b);
}
public static int[] shellSort(int[] a) {
int h = 1;
int n = a.length;
while (h < n / 3) //1
h = 3 * h + 1;
while (h >= 1) {
//增量為h的插入排序
for (int i = h; i < n; i++) {
int v = a[i];
int j = i - h;
while (j >= 0 && a[j] > v) {
a[j + h] = a[j];
j -= h;
}
a[j + h] = v;
}
h = h / 3;
}
return a;
}
}複製程式碼
註釋1處的程式碼是為了得到h值,關於h選什麼樣的值是最好的,至今還未有定論,這裡我們給出比較常用的h值為h = 3 * h + 1,也就是1、4、13、40、121、346、1093......,這些h值會根據陣列的大小而改變。接著往下看,下面的程式碼則是一個增量為h的插入排序。
輸出結果為:
{4, 8, 9, 1, 10, 6, 2, 5}
{1, 2, 4, 5, 6, 8, 9, 10}
希爾排序的複雜度
希爾排序的複雜度要根據h的值來進行計算,不同的h值會導致不同的複雜度,一般情況下,當h = 3 * h + 1時,希爾排序的複雜度基本維持在O(n^1.25)。
歡迎關注我的微信公眾號,第一時間獲得部落格更新提醒,以及更多成體系的Android相關原創技術乾貨。
掃一掃下方二維碼或者長按識別二維碼,即可關注。