文章目錄
- 預備:最簡單的排序
- 1.標準冒泡排序
- 2.改進冒泡排序
- 最佳化方案
排序:對一序列對象根據某個關鍵字進行排序;
穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面;
不穩定:如果a原本在b的前面,而a=b,排序之後a可能會出現在b的後面;
內排序:所有排序操作都在記憶體中完成;
外排序:由於資料太大,因此把資料放在磁碟中,而排序通過磁碟和記憶體的資料轉送才能進行;
排序耗時的操作:比較、移動;
排序分類:
(1)交換類:冒泡排序、快速排序;此類的特點是通過不斷的比較和交換進行排序;
(2)插入類:簡單插入排序、希爾排序;此類的特點是通過插入的手段進行排序;
(3)選擇類:簡單選擇排序、堆排序;此類的特點是看準了再移動;
(4)歸併類:歸併排序;此類的特點是先分割後合并;
曆史進程:一開始排序演算法的複雜度都在O(n^2),希爾排序的出現打破了這個僵局;
以下視頻是Sapientia University創作的,用跳舞的形式示範排序步驟,這些視頻就可以當作複習排序的資料~
冒泡排序視頻:http://v.youku.com/v_show/id_XMzMyOTAyMzQ0.html
選擇排序視頻:http://v.youku.com/v_show/id_XMzMyODk5MDI0.html
插入排序視頻:http://v.youku.com/v_show/id_XMzMyODk3NjI4.html
希爾排序視頻:http://v.youku.com/v_show/id_XMzMyODk5MzI4.html
歸併排序視頻:http://v.youku.com/v_show/id_XMzMyODk5Njg4.html
快速排序視頻:http://v.youku.com/v_show/id_XMzMyODk4NTQ4.html
上面介紹的排序演算法都是基於排序的,還有一類演算法不是基於比較的排序演算法,即計數排序、基數排序;
預備:最簡單的排序
此種實現方法是最簡單的排序實現;
缺點是每次找最小值都是單純的找,而沒有為下一次尋找做出鋪墊;
演算法如下:
public static int[] simple_sort(int[] arr) {for (int i = 0; i < arr.length; i++) {for (int j = i + 1; j < arr.length; j++) {if (arr[i] > arr[j]) {swap(arr, i, j);}}}return arr;}
一、冒泡排序
冒泡排序相對於最簡單的排序有了改進,即每次交換都是對後續有協助的,大數將會越來越大,小的數將會越來越小;
冒泡排序思想:兩兩相鄰元素之間的比較,如果前者大於後者,則交換;
因此此排序屬於交換排序一類,同類的還有現在最常用的排序方法:快速排序;
1.標準冒泡排序
此種方法是最一般的冒泡排序實現,思想就是兩兩相鄰比較並交換;
演算法實現如下:
public static int[] bubble_sort2(int[] arr) {for (int i = 0; i < arr.length; i++) {for (int j = arr.length - 1; j > i; j--) {if (arr[j] < arr[j - 1]) {swap(arr, j, j - 1);}}}return arr;}
2.改進冒泡排序
改進在於如果出現一個序列,此序列基本是排好序的,如果是標準的冒泡排序,則還是需要進行不斷的比較;
改進方法:通過一個boolean isChanged,如果一次迴圈中沒有交換過元素,則說明已經排好序;
演算法實現如下:
// 最好:n-1次比較,不移動,因此時間複雜度為O(n),不佔用輔助空間// 最壞:n(n-1)/2次比較和移動,因此O(n^2),佔用交換的臨時空間,大小為1;public static int[] bubble_sort3(int[] arr) {boolean isChanged = true;for (int i = 0; i < arr.length && isChanged; i++) {isChanged = false;for (int j = i + 1; j < arr.length; j++) {if (arr[i] > arr[j]) {swap(arr, i, j);isChanged = true;}}}return arr;}
二、簡單選擇排序
簡單選擇排序特點:每次迴圈找到最小值,並交換,因此交換次數始終為n-1次;
相對於最簡單的排序,對於很多不必要的交換做了改進,每個迴圈不斷比較後記錄最小值,只做了一次交換(當然也可能不交換,當最小值已經在正確位置)
演算法如下:
//最差:n(n-1)/2次比較,n-1次交換,因此時間複雜度為O(n^2)//最好:n(n-1)/2次比較,不交換,因此時間複雜度為O(n^2)//好於冒泡排序public static int[] selection_sort(int[] arr) {for (int i = 0; i < arr.length - 1; i++) {int min = i;for (int j = i + 1; j < arr.length; j++) {if (arr[min] > arr[j]) {min = j;}}if (min != i)swap(arr, min, i);}return arr;}
三、簡單插入排序
思想: 給定序列,存在一個分界線,分界線的左邊被認為是有序的,分界線的右邊還沒被排序,每次取沒被排序的最左邊一個和已排序的做比較,並插入到正確位置;我們預設索引0的子數組有序;每次迴圈將分界線右邊的一個元素插入有序數組中,並將分界線向右移一位;
演算法如下:
// 最好:n-1次比較,0次移動 ,時間複雜度為O(n)// 最差:(n+2)(n-1)/2次比較,(n+4)(n-1)/2次移動,時間複雜度為 O(n^2)public static int[] insertion_sort(int[] arr) {int j;for (int i = 1; i < arr.length; i++) {if (arr[i] < arr[i - 1]) {int tmp = arr[i];for (j = i - 1; j >= 0 && arr[j] > tmp; j--) {arr[j + 1] = arr[j];}arr[j + 1] = tmp;}}return arr;}
簡單插入排序比選擇排序和冒泡排序好!
四、希爾排序
1959年Shell發明;
第一個突破O(n^2)的排序演算法;是簡單插入排序的改進版;
思想:由於簡單插入排序對於記錄較少或基本有序時很有效,因此我們可以通過將序列進行分組排序使得每組容量變小,再進行分組排序,然後進行一次簡單插入排序即可;
這裡的分組是跳躍分組,即第1,4,7位置為一組,第2,5,8位置為一組,第3,6,9位置為一組;
此時,如果increment=3,則i%3相等的索引為一組,比如索引1,1+3,1+3*2
一般增量公式為:increment = increment/3+1;
演算法實現如下:
// O(n^(3/2))//不穩定排序演算法public static int[] shell_sort(int[] arr) {int j;int increment = arr.length;do {increment = increment / 3 + 1;for (int i = increment; i < arr.length; i++) { //i=increment 因為插入排序預設每組的第一個記錄都是已排序的if (arr[i] < arr[i - increment]) {int tmp = arr[i];for (j = i - increment; j >= 0 && arr[j] > tmp; j -= increment) {arr[j + increment] = arr[j];}arr[j + increment] = tmp;}}} while (increment > 1);return arr;}
五、堆排序
Floyd和Williams在1964年發明;
大根堆:任意父節點都比子節點大;
小根堆:任意父節點都比子節點小;
不穩定排序演算法,是簡單選擇排序的改進版;
思想:構建一棵完全二叉樹,首先構建大根堆,然後每次都把根節點即最大值移除,並用編號最後的節點替代,這時數組長度減一,然後重新構建大根堆,以此類推;
注意:此排序方法不適用於個數少的序列,因為初始構建堆需要時間;
演算法實現如下:
// 時間複雜度為O(nlogn) //不穩定排序演算法//輔助空間為1//不適合排序個數較少的序列public static int[] heap_sort(int[] arr) {int tmp[] = new int[arr.length + 1];tmp[0] = -1;for (int i = 0; i < arr.length; i++) {tmp[i + 1] = arr[i];}// 構建大根堆:O(n)for (int i = arr.length / 2; i >= 1; i--) {makeMaxRootHeap(tmp, i, arr.length);}// 重建:O(nlogn)for (int i = arr.length; i > 1; i--) {swap(tmp, 1, i);makeMaxRootHeap(tmp, 1, i - 1);}for (int i = 1; i < tmp.length; i++) {arr[i - 1] = tmp[i];}return arr;}private static void makeMaxRootHeap(int[] arr, int low, int high) {int tmp = arr[low];int j;for (j = 2 * low; j <= high; j*=2) {if (j < high && arr[j] < arr[j + 1]) {j++;}if (tmp >= arr[j]) {break;}arr[low] = arr[j];low = j;}arr[low] = tmp;}
六、歸併排序
穩定排序演算法;
思想:利用遞迴進行分割和合并,分割直到長度為1為止,並在合并前保證兩序列原本各自有序,合并後也有序;
實現代碼如下:
// 穩定排序;// 時間複雜度O(nlogn)// 空間複雜度:O(n+logn)public static int[] merge_sort(int[] arr) {Msort(arr, arr, 0, arr.length - 1);return arr;}private static void Msort(int[] sr, int[] tr, int s, int t) {int tr2[] = new int[sr.length];int m;if (s == t) {tr[s] = sr[s];} else {m = (s + t) / 2;Msort(sr, tr2, s, m);Msort(sr, tr2, m + 1, t);Merge(tr2, tr, s, m, t);}}private static void Merge(int[] tr2, int[] tr, int i, int m, int t) {int j, k;for (j = i, k = m + 1; i <= m && k <= t; j++) {if (tr2[i] < tr2[k]) {tr[j] = tr2[i++];} else {tr[j] = tr2[k++];}}while (i <= m) {tr[j++] = tr2[i++];}while (k <= t) {tr[j++] = tr2[k++];}}
七、快速排序
冒泡排序的升級版;現在用的最多的排序方法;
思想:選取pivot,將pivot調整到一個合理的位置,使得左邊全部小於他,右邊全部大於他;
注意:如果序列基本有序或序列個數較少,則可以採用簡單插入排序,因為快速排序對於這些情況效率不高;
實現代碼如下:
// 不穩定排序演算法// 時間複雜度:最好:O(nlogn) 最壞:O(n^2)// 空間複雜度:O(logn)public static int[] quick_sort(int[] arr) {qsort(arr, 0, arr.length - 1);return arr;}private static void qsort(int[] arr, int low, int high) {int pivot;if (low < high) {pivot = partition(arr, low, high);qsort(arr, low, pivot);qsort(arr, pivot + 1, high);}}private static int partition(int[] arr, int low, int high) {int pivotkey;pivotkey = arr[low];//選擇pivot,此處可以最佳化while (low < high) {while (low < high && arr[high] >= pivotkey) {high--;}swap(arr, low, high);//交換,此處可以最佳化while (low < high && arr[low] <= pivotkey) {low++;}swap(arr, low, high);}return low;}
最佳化方案
(1)選取pivot:選取pivot的值對於快速排序至關重要,理想情況,pivot應該是序列的中間數;
而前面我們只是簡單的取第一個數作為pivot,這點可以進行最佳化;
最佳化方法:抽多個數後取中位元作為pivot;
(2)對於小數組使用插入排序:因為快速排序適合大數組排序,如果是小數組,則效果可能沒有簡單插入排序來得好;
如果想進行最佳化,則可以使用以下代碼:
public static int[] quick_sort(int[] arr) {if(arr.length>10){qsort(arr, 0, arr.length - 1);}else{insertion_sort(arr);}return arr;}
八、計數排序
計數排序是典型的不是基於比較的排序演算法,基於比較的排序演算法最少也要O(nlogn),有沒有可能創造線性時間的排序演算法呢?那就是不基於比較的排序演算法;
如果數組的資料範圍為0~100,則很適合此演算法;
複雜度: O(n+k), n為原數組長度,k為資料範圍;
思想:
(1)首先找出數組中的最大值,然後建立一個計數數組(用來記錄每個元素的數量),長度為max,比如數組為{1,1,2,3,4,5},則建立一個長度為6的數組count[],count[1]存放數值1出現的次數,即2;
(2)填充count數組,即遍曆原數組,並且count[arr[i]-1]++;
(3)對count數組進行累加,即count[i] = count[i] + count[i-1];
(4)反向填充result數組,result[count[arr[i]]-1] = arr[i];
代碼如下:
import java.util.ArrayList;import java.util.Scanner;/** * 計數排序適用於: * (1)資料範圍較小,建議在小於1000 * (2)每個數值都要大於等於0 * @author xiazdong * */public class Count_Sort {public static void main(String[] args) {int[] array = readArray();System.out.print("排序前數組為:");print(array);int result[] = count_sort(array);System.out.print("排序後數組為:");print(result);}//讀取數組函數private static int[] readArray() {Scanner in = new Scanner(System.in);ArrayList<Integer> list = new ArrayList<Integer>();while(true){System.out.print("輸入數字:");int element = in.nextInt();if(element==-1){break;}else{list.add(element);}}Integer[] arr = list.toArray(new Integer[0]);int[]array = new int[arr.length];for(int i=0;i<arr.length;i++){array[i] = arr[i];}return array;}//計數排序public static int[] count_sort(int arr[]){int gap = findGap(arr);int[] count = new int[gap];int[] result = new int[arr.length];for(int i=0;i<arr.length;i++){count[arr[i]]++;}for(int i=1;i<count.length;i++){count[i] = count[i] + count[i-1];}//反向填充結果數組for(int i=arr.length-1;i>=0;i--){result[count[arr[i]]-1] = arr[i]; count[arr[i]]--;}return result;}public static void print(int result[]){for(int a:result){System.out.print(a+" ");}System.out.println();}/** * 找出數組的資料範圍,即最大數的值 * @param arr * @return */private static int findGap(int[] arr) {int max = arr[0];for(int i=1;i<arr.length;i++){if(max<arr[i]){max = arr[i];}}return (max+1);}}
九、基數排序
基數排序也是非比較的排序演算法,對每一位進行排序,從最低位開始排序,複雜度為O(kn),為數組長度,k為數組中的數的最大的位元;
比如{987,789} ,先通過個位元排序:{987,789},再通過十位元排序:{987,789},再通過百位元排序:{789,987}
思想:
(1)取得數組中的最大數,並取得位元;
(2)arr為原始數組,從最低位開始取每個位組成radix數組;
(3)對radix進行計數排序(利用計數排序適用於小範圍數的特點);
import java.util.ArrayList;import java.util.Scanner;/** * 計數排序適用於: * (1)資料範圍較小,建議在小於1000 * (2)每個數值都要大於等於0 * @author xiazdong * */public class Count_Sort {public static void main(String[] args) {int[] array = new int[]{1046,2084,9046,12074,56,7026,8099,17059,33,1};System.out.print("排序前數組為:");print(array);int result[] = radix_sort(array);System.out.print("排序後數組為:");print(result);}//基數排序 O(kn) public static int[] radix_sort(int[]arr){int radix[] = new int[arr.length];int count = 1;int n = findMaxLength(arr);for(int i=0;i<n;i++){radix = getRadix(arr,count);arr = count_sort(arr, radix);count *=10;}return arr;}private static int findMaxLength(int[] arr) {int max = arr[0];for(int i=1;i<arr.length;i++){if(max<arr[i]){max = arr[i];}}int count = 1;int mcount = 1;while((max / mcount)!=0){mcount = 1;count++;for(int i=0;i<count;i++){mcount *=10;}}return count;}//取得需要排序的位的數組private static int[] getRadix(int[] arr,int count) {//O(n)int radix[] = new int[arr.length];for(int i=0;i<arr.length;i++){radix[i] = arr[i]/count % 10;}return radix;}//類似計數排序//arr為原始數組//radix為需要排序的位的數組public static int[] count_sort(int arr[],int radix[]){int gap = findGap(radix);int[] count = new int[gap];int[] result = new int[radix.length];for(int i=0;i<radix.length;i++){count[radix[i]]++;}for(int i=1;i<count.length;i++){count[i] = count[i] + count[i-1];}//反向填充結果數組for(int i=radix.length-1;i>=0;i--){result[count[radix[i]]-1] = arr[i]; count[radix[i]]--;}return result;}public static void print(int result[]){for(int a:result){System.out.print(a+" ");}System.out.println();}/** * 找出數組的資料範圍,即最大數的值 * @param arr * @return */private static int findGap(int[] arr) {int max = arr[0];for(int i=1;i<arr.length;i++){if(max<arr[i]){max = arr[i];}}return (max+1);}}
對比圖
此圖摘自http://www.cnblogs.com/cj723/archive/2011/04/29/2033000.html的圖
總結:每個排序都有每個排序的優點,我們需要在適當的時候用適當的演算法;
比如在基本有序、數組規模小時用直接插入排序;
比如在大數組時用快速排序;
比如如果要想穩定性,則使用歸併排序;
摘錄維基百科圖片: