歸併排序(Merge Sort):算法
歸併排序是一個至關「穩定」的算法對於其它排序算法,好比希爾排序,快速排序和堆排序而言,這些算法有所謂的最好與最壞狀況。而歸併排序的時間複雜度是固定的,它是怎麼作到的?數組
兩個有序數組的合併:ide
首先來看歸併排序要解決的第一個問題:兩個有序的數組怎樣合成一個新的有序數組:工具
好比數組1{ 3,5,7,8 }數組2爲{ 1,4,9,10 }:性能
首先那確定是建立一個長度爲8的新數組咯,而後就是分別從左到右比較兩個數組中哪個值比較小,而後複製進新的數組中:好比咱們這個例子:ui
{ 3,5,7,8 } { 1,4,9,10 } { }一開始新數組是空的。spa
而後兩個指針分別指向第一個元素,進行比較,顯然,1比3小,因此把1複製進新數組中:指針
{ 3,5,7,8 } { 1,4,9,10 } { 1, }code
第二個數組的指針後移,再進行比較,此次是3比較小:blog
{ 3,5,7,8 } { 1,4,9,10 } { 1,3, }
同理,咱們一直比較到兩個數組中有某一個先到末尾爲止,在咱們的例子中,第一個數組先用完。{ 3,5,7,8 } { 1,4,9,10 } { 1,3,4,5,7,8 }
最後把第二個數組中的元素複製進新數組便可。
{ 1,3,4,5,7,8,9,10 }
因爲前提是這個兩個數組都是有序的,因此這整個過程是很快的,咱們能夠看出,對於一對長度爲N的數組,進行合併所須要的比較次數最多爲2 * N -1(這裏多謝園友@icyjiang的提醒)。
這其實就是歸併排序的最主要想法和實現,歸併排序的作法是:
將一個數組一直對半分,問題的規模就減少了,再重複進行這個過程,直到元素的個數爲一個時,一個元素就至關因而排好順序的。
接下來就是合併的過程了,合併的過程如同前面的描述。一開始合成兩個元素,而後合併4個,8個這樣進行。
因此能夠看到,歸併排序是「分治」算法的一個經典運用。
咱們能夠經過代碼來看看歸併算法的實現:
public static int[] sort(int[] array, int left, int right) { if (left == right) { return new int[] { array[left] }; } int mid = (right + left) / 2; int[] l = sort(array, left, mid); int[] r = sort(array, mid + 1, right); return merge(l, r); } // 將兩個數組合併成一個 public static int[] merge(int[] l, int[] r) { int[] result = new int[l.length + r.length]; int p = 0; int lp = 0; int rp = 0; while (lp < l.length && rp < r.length) { result[p++] = l[lp] < r[rp] ? l[lp++] : r[rp++]; } while (lp < l.length) { result[p++] = l[lp++]; } while (rp < r.length) { result[p++] = r[rp++]; } return result; }
代碼量其實也並很少,主要的工做都在合併兩個數組上。從代碼上看,
if (left == right) { return new int[] { array[left] }; }
這個是遞歸的基準(base case),也就是結束的條件是當元素的個數只有一個時。
int mid = (right + left) / 2; int[] l = sort(array, left, mid); int[] r = sort(array, mid + 1, right);
這一部分顯然就是分(divide),將一個大問題分紅小的問題。
最後也就是治(conquer)了,將兩個子問題的解合併能夠獲得較大問題的解。
因此能夠說,歸併排序是說明遞歸和分治算法的經典例子。
而後就又要回到比較原始的問題了,歸併排序它爲何會快呢?
想回答這個問題能夠先想一下以前說過的提升排序速度的兩個重要的途徑:一個是減小比較次數,一個是減小交換次數。
對於歸併排序而言,咱們來從以前的例子應該能夠看到,兩個數組的合併過程是線性時間的,也就是說咱們每一次比較均可以肯定出一個元素的位置。這是一個重要的性質。
咱們來看一個能夠用一個例子來體會一下假若有這樣一個數組{ 3,7,2,5,1,0,4,6 },
冒泡和選擇排序的比較次數是25次。
直接插入排序用了15次。
而歸併排序的次數是相對穩定的,由咱們上面提到的比較次數的計算方法,咱們的例子要合併4對長度爲1的,2對長度爲2的,和1對長度爲4的。
歸併排序的最多的比較次數爲4 * 1 + 2 * 3 + 7 = 17次。(感謝@icyjiang的提醒)
再次說明一下,這個例子依然只是爲了好理解,不能做爲典型例子來看。
由於元素的隨機性,直接插入排序也多是至關悲劇的。但咱們應該從中看到的是歸併排序在比較次數上的優點。
至於在種優點是怎麼來的,我我的不成熟的總結一下,就是儘可能的讓上一次操做的結果爲下一次操做服務。
咱們每一次合併出來的數組,是否是就是爲下一次合併作準備的。由於兩個要合併的數組是有序的,咱們纔可能高效地進行合併。
快速排序(Quick Sort):
這個算法的霸氣程度從它的名字就能夠看出來了。快速排序的應用也是很是廣的的,各類類庫均可以看到他的身影。這固然與它的「快」是有聯繫的,正所謂天下武功惟快不破。
快速排序的一個特色是,對數組的一次遍歷,能夠找到一個樞紐元(pivot)肯定位置,還能夠把這個數組以這個樞紐元分紅兩個部分,左邊的元素值都比樞紐元小,右邊的都比樞紐元大。咱們遞歸地解決這兩個子數組便可。
咱們仍是經過一個特殊的例子來看一下快速排序的原理:
咱們假設有這樣一個數組{ 4,7,3,2,8,1,5 }
對於快速排序來講,第一步就是找出一個樞紐元,而對於樞紐元的尋找是對整個算法的時間性能影響很大的,由於搞很差快速排序會退化成選擇排序那樣。
對於這個不具備表明性的例子,咱們選擇的是第一個元素作爲樞紐元。
pivot 4
{ 4,7,3,2,8,1,5 }
其中,紅色爲左指針,藍色爲右指針。一開始咱們從右邊開始,找到第一個比pivot小的數。中止,而後將該值賦給左指針,一樣,左指針向右移動。
也就是說咱們第一次獲得的的結果是這樣的:
{ 1,7,3,2,8,1,5 }
一樣的道理,咱們在左邊找到一個比pivot大的值,賦值給右指針,同時右指針左移一步。
獲得的結果應該是這樣的:
{ 1,7,3,2,8,7,5 }
請注意,咱們的這個移動過程的前提都是左指針不能超過右指針的前提下進行的。
這兩個過程交替進行,其實就是在對元素進行篩選。這一次獲得的結果是:
{ 1,2,3,2,8,7,5 }
黃色高亮表示兩個指針重疊了,這時候咱們也就找到了樞紐元的位置了,將咱們的樞紐元的值插入。
也就是說,咱們接下來的工做就是以這個樞紐元爲分割,對左右兩個數組進行一樣的排序工做。
來看看具體的代碼是怎麼實現的:
public static void sort(int[] array, int start, int end) { if (start >= end) { return; } int left = start; int right = end; int temp = array[left]; while (left < right) { while (left < right && temp < array[right]) { right--; } if (left < right) { array[left] = array[right]; left++; } while (left < right && temp > array[left]) { left++; } if (left < right) { array[right] = array[left]; right--; } } array[left] = temp; sort(array, start, left - 1); sort(array, left + 1, end); }
接下來仍是一樣的問題,快速排序爲何會快呢?若是沒有足夠的強大,那不是「浪得虛名」嗎?
首先仍是看看前面的例子。
首先能夠比較容易感覺到的就是元素的移動效率高了。好比說例子中的1,一會兒就移動到了前面去。
這也是我我的的一點感覺,只是以爲能夠這樣理解比較高效的排序算法的特性:
高效的排序算法對元素的移動效率都是比較高的。
它不像冒泡,直接插入那樣,每次可能都是步進一步,而是比較快速的移動到「感受是正確」的位置。
想一想,希爾排序不就是這麼作的嗎?後面的堆排序也是這個原理。
其次,快速排序也符合咱們前面說的,「讓上一個操做的結果爲下一次操做服務」。
很明顯,在樞紐元左邊的元素都比樞紐元要小,右邊的都比樞紐元大。顯然,數據的範圍小了,數據的移動的準確性就高了。
可是,快速排序的一個隱患就是樞紐元的選擇,我提供的代碼中是選第一個元素作樞紐元,這是一種很冒險的作法。
好比咱們對一個數組{ 9,8,7,6,5 }想經過快速排序來變成從小到大的排序。若是仍是選擇以第一個元素爲樞紐元的話,快速排序就變成選擇排序了。
因此,在實際應用中若是數據都是是隨機數據,那麼選擇第一個作樞紐元並無什麼不妥。由於這個原本就是看「人品」的。
可是,若是是對於一些比較有規律的數據,咱們的「人品」可能就不會太好的。因此常見的有兩種選擇策略:
一種是使用隨機數來作選擇。呵呵,聽天由命。
另外一種是取數組中的第一個,最後一個和中間一個,選擇數值介於最大和最小之間的。
這一種又叫作「三數中值分割法」。理論上,這兩種選擇策略仍是可能很悲劇的。但機率要小太多了。
堆排序用文字太難看懂了,想畫一些圖來幫助理解,求各位大大推薦能夠比較方便畫二叉樹的工具。