七大排序算法的个人总结(二)
歸并排序(Merge Sort):
?
歸并排序是一個相當“穩(wěn)定”的算法對于其它排序算法,比如希爾排序,快速排序和堆排序而言,這些算法有所謂的最好與最壞情況。而歸并排序的時間復雜度是固定的,它是怎么做到的?
兩個有序數(shù)組的合并:
首先來看歸并排序要解決的第一個問題:兩個有序的數(shù)組怎樣合成一個新的有序數(shù)組:
比如數(shù)組1{ 3,5,7,8 }數(shù)組2為{ 1,4,9,10 }:
首先那肯定是創(chuàng)建一個長度為8的新數(shù)組咯,然后就是分別從左到右比較兩個數(shù)組中哪一個值比較小,然后復制進新的數(shù)組中:比如我們這個例子:
{?3,5,7,8 } {?1,4,9,10 }? {? }一開始新數(shù)組是空的。
然后兩個指針分別指向第一個元素,進行比較,顯然,1比3小,所以把1復制進新數(shù)組中:
{?3,5,7,8 } { 1,4,9,10 }? { 1, }
第二個數(shù)組的指針后移,再進行比較,這次是3比較小:
{ 3,5,7,8 } { 1,4,9,10 }? { 1,3, }
同理,我們一直比較到兩個數(shù)組中有某一個先到末尾為止,在我們的例子中,第一個數(shù)組先用完。{ 3,5,7,8 } { 1,4,9,10 }? { 1,3,4,5,7,8 }
最后把第二個數(shù)組中的元素復制進新數(shù)組即可。
{ 1,3,4,5,7,8,9,10 }
由于前提是這個兩個數(shù)組都是有序的,所以這整個過程是很快的,我們可以看出,對于一對長度為N的數(shù)組,進行合并所需要的比較次數(shù)最多為2 * N -1(這里多謝園友@icyjiang的提醒)。
?
這其實就是歸并排序的最主要想法和實現(xiàn),歸并排序的做法是:
將一個數(shù)組一直對半分,問題的規(guī)模就減小了,再重復進行這個過程,直到元素的個數(shù)為一個時,一個元素就相當于是排好順序的。
接下來就是合并的過程了,合并的過程如同前面的描述。一開始合成兩個元素,然后合并4個,8個這樣進行。
所以可以看到,歸并排序是“分治”算法的一個經(jīng)典運用。
我們可以通過代碼來看看歸并算法的實現(xiàn):
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);}// 將兩個數(shù)組合并成一個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;}代碼量其實也并不多,主要的工作都在合并兩個數(shù)組上。從代碼上看,
if (left == right) {return new int[] { array[left] };}這個是遞歸的基準(base case),也就是結(jié)束的條件是當元素的個數(shù)只有一個時。
int mid = (right + left) / 2;int[] l = sort(array, left, mid);int[] r = sort(array, mid + 1, right);這一部分顯然就是分(divide),將一個大問題分成小的問題。
最后也就是治(conquer)了,將兩個子問題的解合并可以得到較大問題的解。
所以可以說,歸并排序是說明遞歸和分治算法的經(jīng)典例子。
?
然后就又要回到比較原始的問題了,歸并排序它為什么會快呢?
想回答這個問題可以先想一下之前說過的提高排序速度的兩個重要的途徑:一個是減少比較次數(shù),一個是減少交換次數(shù)。
對于歸并排序而言,我們來從之前的例子應該可以看到,兩個數(shù)組的合并過程是線性時間的,也就是說我們每一次比較都可以確定出一個元素的位置。這是一個重要的性質(zhì)。
我們來看一個可以用一個例子來體會一下假如有這樣一個數(shù)組{ 3,7,2,5,1,0,4,6 },
冒泡和選擇排序的比較次數(shù)是25次。
直接插入排序用了15次。
而歸并排序的次數(shù)是相對穩(wěn)定的,由我們上面提到的比較次數(shù)的計算方法,我們的例子要合并4對長度為1的,2對長度為2的,和1對長度為4的。
歸并排序的最多的比較次數(shù)為4 * 1 + 2 * 3 + 7 = 17次。(感謝@icyjiang的提醒)
?
再次說明一下,這個例子依然只是為了好理解,不能作為典型例子來看。
因為元素的隨機性,直接插入排序也可能是相當悲劇的。但我們應該從中看到的是歸并排序在比較次數(shù)上的優(yōu)勢。
至于在種優(yōu)勢是怎么來的,我個人不成熟的總結(jié)一下,就是盡量的讓上一次操作的結(jié)果為下一次操作服務。
我們每一次合并出來的數(shù)組,是不是就是為下一次合并做準備的。因為兩個要合并的數(shù)組是有序的,我們才可能高效地進行合并。
?
快速排序(Quick Sort):
這個算法的霸氣程度從它的名字就可以看出來了。快速排序的應用也是非常廣的的,各種類庫都可以看到他的身影。這當然與它的“快”是有聯(lián)系的,正所謂天下武功唯快不破。
快速排序的一個特點是,對數(shù)組的一次遍歷,可以找到一個樞紐元(pivot)確定位置,還可以把這個數(shù)組以這個樞紐元分成兩個部分,左邊的元素值都比樞紐元小,右邊的都比樞紐元大。我們遞歸地解決這兩個子數(shù)組即可。
我們還是通過一個特殊的例子來看一下快速排序的原理:
我們假設有這樣一個數(shù)組{ 4,7,3,2,8,1,5 }
對于快速排序來說,第一步就是找出一個樞紐元,而對于樞紐元的尋找是對整個算法的時間性能影響很大的,因為搞不好快速排序會退化成選擇排序那樣。
對于這個不具有代表性的例子,我們選擇的是第一個元素做為樞紐元。
pivot 4
{?4,7,3,2,8,1,5?}
其中,紅色為左指針,藍色為右指針。一開始我們從右邊開始,找到第一個比pivot小的數(shù)。停止,然后將該值賦給左指針,同樣,左指針向右移動。
也就是說我們第一次得到的的結(jié)果是這樣的:
{ 1,7,3,2,8,1,5 }
同樣的道理,我們在左邊找到一個比pivot大的值,賦值給右指針,同時右指針左移一步。
得到的結(jié)果應該是這樣的:
{ 1,7,3,2,8,7,5 }
請注意,我們的這個移動過程的前提都是左指針不能超過右指針的前提下進行的。
這兩個過程交替進行,其實就是在對元素進行篩選。這一次得到的結(jié)果是:
{ 1,2,3,2,8,7,5 }
黃色高亮表示兩個指針重疊了,這時候我們也就找到了樞紐元的位置了,將我們的樞紐元的值插入。
也就是說,我們接下來的工作就是以這個樞紐元為分割,對左右兩個數(shù)組進行同樣的排序工作。
來看看具體的代碼是怎么實現(xiàn)的:
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,一下子就移動到了前面去。
這也是我個人的一點感受,只是覺得可以這樣理解比較高效的排序算法的特性:
高效的排序算法對元素的移動效率都是比較高的。
它不像冒泡,直接插入那樣,每次可能都是步進一步,而是比較快速的移動到“感覺是正確”的位置。
想想,希爾排序不就是這么做的嗎?后面的堆排序也是這個原理。
?
其次,快速排序也符合我們前面說的,“讓上一個操作的結(jié)果為下一次操作服務”。
很明顯,在樞紐元左邊的元素都比樞紐元要小,右邊的都比樞紐元大。顯然,數(shù)據(jù)的范圍小了,數(shù)據(jù)的移動的準確性就高了。
?
但是,快速排序的一個隱患就是樞紐元的選擇,我提供的代碼中是選第一個元素做樞紐元,這是一種很冒險的做法。
比如我們對一個數(shù)組{ 9,8,7,6,5 }想通過快速排序來變成從小到大的排序。如果還是選擇以第一個元素為樞紐元的話,快速排序就變成選擇排序了。
所以,在實際應用中如果數(shù)據(jù)都是是隨機數(shù)據(jù),那么選擇第一個做樞紐元并沒有什么不妥。因為這個本來就是看“人品”的。
但是,如果是對于一些比較有規(guī)律的數(shù)據(jù),我們的“人品”可能就不會太好的。所以常見的有兩種選擇策略:
一種是使用隨機數(shù)來做選擇。呵呵,聽天由命。
另一種是取數(shù)組中的第一個,最后一個和中間一個,選擇數(shù)值介于最大和最小之間的。
這一種又叫做“三數(shù)中值分割法”。理論上,這兩種選擇策略還是可能很悲劇的。但概率要小太多了。
?
堆排序用文字太難看懂了,想畫一些圖來幫助理解,求各位大大推薦可以比較方便畫二叉樹的工具。
from:?https://www.cnblogs.com/yjiyjige/p/3256700.html
總結(jié)
以上是生活随笔為你收集整理的七大排序算法的个人总结(二)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 七大排序算法的个人总结(一)
- 下一篇: 七大排序算法的个人总结(三)