MergeSort是個相對古老的算法了,爲何如今咱們還要討論這麼古老的東西呢?有幾個緣由:
-
它雖然年齡很大了,可是在實踐中一直被沿用,仍然是不少程序庫中的標準算法之一。
-
實現它的本質是分治思想,是一個理解分治算法思想的好例子,好起點。
-
本文會使用「遞歸樹」來對它進行運行時間分析,後面會集合這種思路生成「主方法」。
輸入一個數組,數組裏面的每一個數字是不重複的,輸出是已經排序好的數組。
可能以前咱們有所知道一些排序算法,好比SelectionSort,掃描全數組找到最小元素,把它放到輸出數組的第一位,接着掃描複製次小的元素,以此類推;好比BubbleSort,對相鄰無序的元素進行比較,執行反覆的交換,直到最後數組完成排序。等等。但這些算法的問題就是運行時間是平方級的。那咱們來看看今天這個排序算法用更少的運行時間是怎麼實現的。
例子 想要理解MergeSort算法是如何運行的,一個最簡單的方法就是看看具體的例子。
它把數組 [5, ,4, 1, 8, 7, 2, 6, 3] 劃分爲更小的數組(子問題)[5, 4, 1, 8] 和 [7, 2, 6, 3]排序,經過神奇的遞歸操做,獲得每一個排序後的子數組,再將子數組合並起來。
由上面的圖咱們能夠知道,Merge的時候,其實輸入兩個已經排序好的數組C, D,再把它們排序輸出到B。
索引 k 操控的是輸出數組,索引 i,j 操控的是已排序好的C和D子數組,都是從左向右掃描。每一次的for循環,其實就是找C和D中最小的數字,由於C和D是已排序好的數組,因此最小的數字就是i或j對應的元素。比較後把它放入輸出數組B中,並將對應的索引+1,這樣下次循環就跳過已複製的元素了。因此最後的數組B輸出是按序方式生成的。
咱們先來對Merge程序算算它的執行操做數量。 第1,2行有一次賦值操做。 第3行是一個for循環,每個for循環裏,有比較操做(C[i]和D(i)比較),賦值操做(B[k]的賦值),遞增操做(i或j加1),循環變量k還要加1,因此每一次循環一共有4次操做。
一共就是4n+2次操做,爲了後面的計算方便,當n>=1時,4n+2<6n(去掉常數項), 咱們取6n次操做做爲Merge程序操做上界。
咱們如今再來看MergeSort的運行時間。 爲了簡單起見,假如輸入數組的長度是n的2次方(若是沒有這個假設只須要額外工做就能消除這個假設),咱們用遞歸樹的方法來分析運行時間的上界,每個節點就表示一次遞歸調用。
最外層是整個原始的輸入數組,它在進行MergeSort的時候會有2個遞歸調用,因此這是一個二叉樹(每一個節點都有兩個子節點),第一層的2個節點就是原始數組的左半部分和右半部分,再次遞歸最後到達最底層,一個長度爲1或0的數組。咱們須要知道幾個數量:
因爲每深刻一層,數組長度就縮小一半,第0層是n,第1層是n/2(除了一次2),第2層是n/4(除了2次2),最後一層的數組長度是不大於1的,就是除以了log2(n)次2,因此最後一層是log2(n)層。(也能夠假定n/2^a = 1, 求解a)若是沒有n是2的次方這個假設,就向上取整。一共就是log2(n) +1層。
遞歸樹的第j層有多少個節點(子問題)?每一個節點的數組長度是多少?
由於每一層的遞歸數量是前一層的兩倍,因此第j層就有2^j個子問題。每一個節點的長度,總長度是n,均分到每一個節點就是n/(2^j)個長度。
= 層數 * (第 j 層的子問題數量 * 每一個第j層子問題完成的工做數) = 層數 * (第 j 層的子問題數量 * (每一個第j層子問題的規模 * Merge的時間))
= (log2(n)+1) * ( 2^j * n/(2^j) * 6n)
這時候,咱們就能夠義正詞嚴的說遞歸的分治算法比其它更簡單的算法要快的多啦。看圖直觀感覺一下
當數據很是大的時候,它是很是有優點的,指數函數增加十分的緩慢。