在《algorithm》中,作者單獨講mergesort作為一個小節,可以看出它的重要程度。
首先來看一下歸並排序的運用場景是怎樣的:將兩個已排序列進行排列。
主要的思想便是:比較a[i]和b[j]的大小,若a[i]≤b[j],則將第一個有序表中的元素a[i]復制到r[k]中,並令i和k分別加上1;否則將第二個有序表中的元素b[j]復制到r[k]中,並令j和k分別加上1,如此循環下去,直到其中一個有序表取完,然后再將另一個有序表中剩余的元素復制到r中從下標k到下標t的單元。
這樣說太空洞了,我們給出圖:
我們實際上是copy了原序列,所以在merge sort,其實是犧牲了空間復雜度來達到時間復雜度的提高。
我們先來看一下整個歸並排序最基礎的方法:merge:
private static void merge(Comparable[] a,int lo,int mid,int hi){ int i=lo,j=mid+1; aux=new Comparable[a.length]; for(int k=lo;k<a.length;k++){ aux[k]=a[k]; } for(int k=lo;k<=hi;k++){ if(i>mid)a[k]=aux[j++]; else if(j>hi)a[k]=aux[i++]; else if(less(aux[j],aux[i]))a[k]=a[j++]; else a[k]=aux[i++]; } }
這就是簡單的將兩個已排序列進行歸並排序的算法。
但是實際上,我們在運用中是很難有這種巧合的情況的,我們需要處理的仍然是大量的無序序列。這個時候我們有兩種方法來完成無序序列的merge sort,一種為Top-down Merge Sort,一種為Button-up Merge Sort;
我們先來說明一下
Top-down Merge Sort:
首先,我不知道怎么翻譯這個歸並算法的名字,所以我一直用的都是英文,采用的是遞歸的思想來實現排序的。遞歸就不需要我多講解了,這是一個非常好用的辦法,但是需要注意的是,一定要在方法或者函數中設置遞歸的出口判斷語句,不然會有異常拋出。Top-down Merge Srot的主要流程如下:
這個圖可以很清晰的放映出遞歸的思想。
我們接下來實現這個排序方法:
private static void TopDownMergeSort(Comparable[] a){ TopDownMergeSort(a,0,a.length-1); } private static void TopDownMergeSort(Comparable[] a,int lo,int hi){ if(hi<=lo)return; int mid=(lo+hi)/2; TopDownMergeSort(a,lo,mid); TopDownMergeSort(a,mid+1,hi); merge(a,lo,mid,hi); }
這個程序完成的流程便是下圖:
很簡單對吧!
top-down merge sort算法分析
1)算法復雜度
我們通過程序其實可以看見,歸並算法沒有運用到交換方法(exch)!所以我們這里只考慮比較次數就可以了。
這里給出定理1:
Top-down Merge Sort使用了1/2n*lgn-n*lgn次數的比較。
算法書中也給出了證明,但是書中的證明方法只證明了為什么最多是n*log(n)次比較。這里我給出另外的一個證明,雖然要low很多,但是還是能夠清楚的證明這個定理:
歸並的基本思想是合並多路有序數組,通常我們考慮兩路歸並算法。
歸並排序是穩定的,這是因為,在進行多路歸並時,如果各個序列當前元素均相等,可以令排在前面的子序列的元素先處理,不會打亂相等元素的順序。
考慮元素比較次數,兩個長度分別為m和n的有序數組,每次比較處理一個元素,因而合並的最多比較次數為(m+n-1),最少比較次數為min(m,n)。對於兩路歸並,序列都是兩兩合並的。不妨假設元素個數為n=2^h,
第一次歸並:合並兩個長度為1的數組,總共有n/2個合並,比較次數為n/2。
第二次歸並:合並兩個長度為2的數組,最少比較次數是2,最多是3,總共有n/4次,比較次數是(2~3)n/4。
第三次歸並:合並兩個長度為4的數組,最少比較次數是4,最多是7,總共有n/8次合並,比較次數是(4-7)n/8。
第k次歸並:合並兩個長度為2^(k-1)的數組,最少比較次數是2^(k-1),最多是2^k-1,總共合並n/(2^k)次,比較次數是[2^(k-1)~(2^k-1)](n/2^k)=n/2~n(1-1/2^k)。
按照這樣的思路,可以估算比較次數下界為n/2*h=nlg(n)/2。上界為n[h-(1/2+1/4+1/8+...+1/2^h)]=n[h-(1-1/2^h)]=nlog2(n)-n+1。
綜上所述,歸並排序比較次數為nlgn-n+1~nlog2(n)/2。
歸並排序引入了一個與初始序列長度相同的數組來存儲合並后的結果,因而不涉及交換。
!至於穩重的最少比較次數的得來,如果當一側的元素全部小於另一邊最小元素或者大於另一邊最大元素的時候,這個時候得到的比較次數便是最少的。因為在遍歷完該側的元素,另外一邊的元素不需要進行比較直接錄入序列則好。!
當然,書中的證明是非常棒的,知識沒有這么簡單明了:
定理2:
Top-down Merge Sort 最多接觸了6n*log(n)次數組(原諒我的語言表達,其實就是array access).
證明其實也很簡單:
Button-up Merge Sort
在Top-down Merge Sort 中,我們使用的是遞歸,主要也就是從最開始的兩個序列依次往下,直到序列長度為1的時候。那么一定會有對應的算法,從序列長度為1開始向上進行排序。我們直接給出 Button-up Merge Sort的代碼,可以很清晰的明白這個區別:
private static void ButtonUpMergeSort(Comparable[] a){ int N=a.length; aux=new Comparable[N]; for(int sz=0;sz<N;sz=sz+sz){ for(int lo=0;lo<N-sz;lo=sz+sz){ merge(a,lo,lo+sz-1,Math.min(lo+sz+sz-1, N-1)); } } }
Button-up Merge Sort算飯分析
1)算法復雜度:
Button-up Merge Sort的算法復雜度和 Top-down Merge Sort的算法復雜度是一樣的。使用的比較次數和array access次數也是一樣的。具體的證明可以和Top-down Merge Sort的證明進行參考。
比較:
這兩種實現merge sort的算法其實除了運行過程不一樣,其他的都是很類似的,其基礎都是merge sort。具體的運行區別可以用下圖很明確的就展示出來:
兩者總體來說:
1)有一樣的比較次數:1/2nlgn~nlgn;
2)一樣的賦值操作:2n*lgn;(至於這一步怎么證明的,有興趣的朋友可以自己通過程序算一下,其實很簡單的)
3)一樣的array access操作:6nlgn
Merge Sort算法總體分析
1)算法復雜度:算法復雜度其實通過上面的比較次數分析也已經得到了,為O(nlogn);
2) 穩定性:Merge sort是穩定的
3)輔助空間:其實就是空間復雜度,我們可以通過代碼得到,mergesort是要新建數組的,我們創建了一個aux數組(注意:不是在每個遞歸里面都創建了一個,而是我們至始至終只創建了一個aux數組!只是每個迭代內部都對aux進行了復制操作),所以很輕易,我們的空間復雜度(主要是輔助空間)就是O(n);
這里有個非常重要的定理:
任何基於比較的排序算法都不能“保證”對一個N個元素的序列排序的比較次數小於lg(N!)~nlg(n);
這個證明起來就非常的費勁了,我還是將書中的證明過程給出來,相信會基於一些同學一定的幫助:
改進方法:
因為大部分時候我們運用的都是Top-down Merge Sort(雖然 Button-up Merge Sort的代碼更簡潔,但是易讀性完全不如Top-down Merge Sort),所以在改進的時候,我們主要針對Top-down Merge Sort
1)使用插入排序(insertion sort)對很小的子序列:
這個很容易理解,我們減少遞歸的層數,而使用插入排序在較小的子序列進行排序會提高10%-15%的效率。注意了,這里很多人都在想,歸並算法復雜度不是平均比比插入算法復雜度好么?為什么還要使用插入算法,即便在子序列中,感覺也是歸並排序更好。這里的原理很深了,並不是這么簡單的。這里有篇文章針對這個問題進行了講解,我們可以看到,這個分析和單獨比較插入排序和歸並排序是完全不同的,雖然我也還有不太懂的地方,但是不能僅僅提出者兩個排序進行單獨討論。文章地址:http://blog.csdn.net/wu_yihao/article/details/8038998
該文章的重點過程如下:
(0) 對每個列表排序的最壞時間是Θ(k2),則n/k個列表需要Θ(nk)的漸進時間。
(1) 合並這些列表需要Θ(nlog(n/k))的時間:最初合並n/k個規模為k的列表需要 cn/k * k = Θ(n),再利用數學歸納法可證每次合並都需要Θ(n),並且需要log(n/k)次合並。或者也可以通過遞歸樹盡心分析。
(2) 總時間為Θ(nk+nlog(n/k)),我們可以利用這個總漸進時間,導出k取值的界
由反證法可以得到,k的階取值不能大於Θ(logn),並且這個界可以保證插排優化的漸進時間不會慢於原始歸並排序。
由於對數函數的增長特點,結合實際排序規模,k得實際取值一般在10~20間。
看不懂也沒關系!我們記住,在《algorithm》書中將k的值定位15,並且有效的提高了Top-down Merge Sort的運行時間和效率。
2)判斷是否這個array已經排序成功:
當我們判斷出a[mid]小於a[mid+1]的時候,如果我們跳過merge()方法,那么,我們對已排序列的運行時間就會降低為線性。這已經是很大的提升了,雖然不是對整體的運行時間進行提高。
3)降低復制到aux數組的次數:
這個就有一點tricky的感覺。我們通過代碼可以看到每一次遞歸,我們都要將數組a復制到aux里面去,但是我們可以將a的功能和aux的功能不斷轉換,來降低復制帶來的時間冗長,很好理解的,我們在比較並復制時,我們第一層遞歸是將a作為輸出,那么下一層,我們可以將aux作為輸出。而不是一直將a作為輸出,aux作為復制來進行。大大減少了復制帶來的運行時間。
三點改進在書中具體的描述為下:
以上就是merge sort的全部內容,有些內容已近遠超我們需要掌握的內容,但是我相信了解它們沒有壞處。