標籤:遞迴 合并演算法 移動 dex 缺陷 相對 對象 container ace
轉自;http://flyingcat2013.blog.51cto.com/7061638/1281026
前面的三種排序演算法(冒泡排序,選擇排序,插入排序)在平均情況下均為O(n^2)複雜度,在處理較大資料的時候比較吃力。現在來說說相對快速一些的演算法,例如下面的歸併排序。
演算法概述/思路
歸併排序是基於一種被稱為“分治”(divide and conquer)的策略。其基本思路是這樣的:
1.對於兩個有序的數組,要將其合并為一個有序數組,我們可以很容易地寫出如下代碼:
| 123456789101112131415161718 |
//both a and b is ascend.public void merge(int[] a, int[] b, int[] c){ int i=0,j=0,k=0; while (i<=a.length && j<=b.length){ if (a[i]<=b[i]){ c[k++]=a[i++]; } else{ c[k++]=b[j++]; } } while (i<=a.length){ c[k++]=a[i++]; } while (j<=b.length){ c[k++]=b[j++]; }} |
容易看出,這樣的合并演算法是高效的,其時間複雜度可達到O(n)。
2.假如有一個無序數組需要排序,但它的兩個完全劃分的子數組A和B分別有序,藉助上述代碼,我們也可以很容易實現;
3.那麼,如果A,B無序,怎麼辦呢?可以把它們再分成更小的數組。
4.如此一直劃分到最小,每個子數組都只有一個元素,則可以視為有序數組。
5.從這些最小的數組開始,逆著上面的步驟合并回去,整個數組就排好了。
總而言之,歸併排序就是使用遞歸,先分解數組為子數組,再合並數組。
下面是歸併排序的(圖片來自維基百科):
代碼實現
| 1234567891011121314151617181920212223242526272829303132333435363738 |
//歸併排序 public static void mergeSort(int[] arr){ int[] temp =new int[arr.length]; internalMergeSort(arr, temp, 0, arr.length-1); } private static void internalMergeSort(int[] a, int[] b, int left, int right){ //當left==right的時,已經不需要再劃分了 if (left<right){ int middle = (left+right)/2; internalMergeSort(a, b, left, middle); //左子數組 internalMergeSort(a, b, middle+1, right); //右子數組 mergeSortedArray(a, b, left, middle, right); //合并兩個子數組 } } // 合并兩個有序子序列 arr[left, ..., middle] 和 arr[middle+1, ..., right]。temp是輔助數組。 private static void mergeSortedArray(int arr[], int temp[], int left, int middle, int right){ int i=left; int j=middle+1; int k=0; while ( i<=middle && j<=right){ if (arr[i] <=arr[j]){ temp[k++] = arr[i++]; } else{ temp[k++] = arr[j++]; } } while (i <=middle){ temp[k++] = arr[i++]; } while ( j<=right){ temp[k++] = arr[j++]; } //把資料複製回原數組 for (i=0; i<k; ++i){ arr[left+i] = temp[i]; } } |
需要說明的是,在合并數組的時候需要一個temp數組。我們當然有足夠的理由在每次調用的時候重新new一個數組(例如,減少一個參數),但是,注意到多次的建立數組對象會造成額外的開銷,我們可以在開始就建立一個足夠大的數組(等於原數組長度就行),以後都使用這個數組。實際上,上面的代碼就是這麼寫的。
演算法效能/複雜度
歸併排序的效率是很高的,由於遞迴劃分為子序列只需要logN複雜度,而合并每兩個子序列需要大約2n次賦值,為O(n)複雜度,因此,只需要簡單相乘即可得到歸併排序的時間複雜度 O(㏒n)。並且由于歸並演算法是固定的,不受輸入資料影響,所以它在最好、最壞、平均情況下表現幾乎相同,均為O(㏒n)。
但是,歸併排序最大的缺陷在於其空間複雜度。從上面的代碼可以看到,在合并子數組的時候需要一個輔助數組,然後再把這個資料拷貝回原數組。所以,歸併排序的空間複雜度(額外空間)為O(n)。可不可以省略這個數組呢?不行!如果取消輔助數組而又要保證原來的數組中資料不被覆蓋,那就必須要在數組中花費大量時間來移動資料。不僅容易出錯,還降低了效率。因此這個輔助空間是少不掉的。
演算法穩定性
因為我們在遇到相等的資料的時候必然是按順序“抄寫”到輔助數組上的,所以,歸併排序同樣是穩定演算法。
演算法適用情境
歸併排序在資料量比較大的時候也有較為出色的表現(效率上),但是,其空間複雜度O(n)使得在資料量特別大的時候(例如,1千萬資料)幾乎不可接受。而且,考慮到有的機器記憶體本身就比較小,因此,採用歸併排序一定要注意。
用Java寫演算法之歸併排序