筆記:數據結構與算法之美 - 歸併排序、快速排序

一、分治思想


1.分治思想:分治,顧明思意,就是分而治之,將一個大問題分解成小的子問題來解決,小的子問題解決了,大問題也就解決了。
2.分治與遞歸的區別:分治算法一般都用遞歸來實現的。分治是一種解決問題的處理思想,遞歸是一種編程技巧。
二、歸併排序
1.算法原理
先把數組從中間分成前後兩部分,然後對前後兩部分分別進行排序,再將排序好的兩部分合併到一起,這樣整個數組就有序了。這就是歸併排序的核心思想。如何用遞歸實現歸併排序呢?寫遞歸代碼的技巧就是分寫得出遞推公式,然後找到終止條件,最後將遞推公式翻譯成遞歸代碼。遞推公式怎麼寫?如下
遞推公式:merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))
終止條件:p >= r 不用再繼續分解
2.代碼實現(參見下一條留言)
3.性能分析
1)算法穩定性:
歸併排序穩不穩定關鍵要看merge()函數,也就是兩個子數組合併成一個有序數組的那部分代碼。在合併的過程中,如果 A[p…q] 和 A[q+1…r] 之間有值相同的元素,那我們就可以像僞代碼中那樣,先把 A[p…q] 中的元素放入tmp數組,這樣 就保證了值相同的元素,在合併前後的先後順序不變。所以,歸併排序是一種穩定排序算法。
2)時間複雜度:分析歸併排序的時間複雜度就是分析遞歸代碼的時間複雜度
如何分析遞歸代碼的時間複雜度?
遞歸的適用場景是一個問題a可以分解爲多個子問題b、c,那求解問題a就可以分解爲求解問題b、c。問題b、c解決之後,我們再把b、c的結果合併成a的結果。若定義求解問題a的時間是T(a),則求解問題b、c的時間分別是T(b)和T(c),那就可以得到這樣的遞推公式:T(a) = T(b) + T(c) + K,其中K等於將兩個子問題b、c的結果合併成問題a的結果所消耗的時間。這裏有一個重要的結論:不僅遞歸求解的問題可以寫成遞推公式,遞歸代碼的時間複雜度也可以寫成遞推公式。套用這個公式,那麼歸併排序的時間複雜度就可以表示爲:
T(1) = C; n=1 時,只需要常量級的執行時間,所以表示爲 C。
T(n) = 2*T(n/2) + n; n>1,其中n就是merge()函數合併兩個子數組的的時間複雜度O(n)。
T(n) = 2*T(n/2) + n
     = 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n
     = 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
     = 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
     ......
     = 2^k * T(n/2^k) + k * n
     ......
當T(n/2^k)=T(1) 時,也就是 n/2^k=1,我們得到k=log2n。將k帶入上面的公式就得到T(n)=Cn+nlog2n。如用大O表示法,T(n)就等於O(nlogn)。所以,歸併排序的是複雜度時間複雜度就是O(nlogn)。
3)空間複雜度:歸併排序算法不是原地排序算法,空間複雜度是O(n)
爲什麼?因爲歸併排序的合併函數,在合併兩個數組爲一個有序數組時,需要藉助額外的存儲空間。爲什麼空間複雜度是O(n)而不是O(nlogn)呢?如果我們按照分析遞歸的時間複雜度的方法,通過遞推公式來求解,那整個歸併過程需要的空間複雜度就是O(nlogn),但這種分析思路是有問題的!因爲,在實際上,遞歸代碼的空間複雜度並不是像時間複雜度那樣累加,而是這樣的過程,即在每次合併過程中都需要申請額外的內存空間,但是合併完成後,臨時開闢的內存空間就被釋放掉了,在任意時刻,CPU只會有一個函數在執行,也就只會有一個臨時的內存空間在使用。臨時空間再大也不會超過n個數據的大小,所以空間複雜度是O(n)。

 

三、快速排序
1.算法原理
快排的思想是這樣的:如果要排序數組中下標從p到r之間的一組數據,我們選擇p到r之間的任意一個數據作爲pivot(分區點)。然後遍歷p到r之間的數據,將小於pivot的放到左邊,將大於pivot的放到右邊,將povit放到中間。經過這一步之後,數組p到r之間的數據就分成了3部分,前面p到q-1之間都是小於povit的,中間是povit,後面的q+1到r之間是大於povit的。根據分治、遞歸的處理思想,我們可以用遞歸排序下標從p到q-1之間的數據和下標從q+1到r之間的數據,直到區間縮小爲1,就說明所有的數據都有序了。
遞推公式:quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)
終止條件:p >= r
2.代碼實現(參見下一條留言)
3.性能分析
1)算法穩定性:
因爲分區過程中涉及交換操作,如果數組中有兩個8,其中一個是pivot,經過分區處理後,後面的8就有可能放到了另一個8的前面,先後順序就顛倒了,所以快速排序是不穩定的排序算法。比如數組[1,2,3,9,8,11,8],取後面的8作爲pivot,那麼分區後就會將後面的8與9進行交換。
2)時間複雜度:最好、最壞、平均情況
快排也是用遞歸實現的,所以時間複雜度也可以用遞推公式表示。
如果每次分區操作都能正好把數組分成大小接近相等的兩個小區間,那快排的時間複雜度遞推求解公式跟歸併的相同。
T(1) = C; n=1 時,只需要常量級的執行時間,所以表示爲 C。
T(n) = 2*T(n/2) + n; n>1
所以,快排的時間複雜度也是O(nlogn)。
如果數組中的元素原來已經有序了,比如1,3,5,6,8,若每次選擇最後一個元素作爲pivot,那每次分區得到的兩個區間都是不均等的,需要進行大約n次的分區,才能完成整個快排過程,而每次分區我們平均要掃描大約n/2個元素,這種情況下,快排的時間複雜度就是O(n^2)。
前面兩種情況,一個是分區及其均衡,一個是分區極不均衡,它們分別對應了快排的最好情況時間複雜度和最壞情況時間複雜度。那快排的平均時間複雜度是多少呢?T(n)大部分情況下是O(nlogn),只有在極端情況下才是退化到O(n^2),而且我們也有很多方法將這個概率降低。
3)空間複雜度:快排是一種原地排序算法,空間複雜度是O(1)
四、歸併排序與快速排序的區別
歸併和快排用的都是分治思想,遞推公式和遞歸代碼也非常相似,那它們的區別在哪裏呢?
1.歸併排序,是先遞歸調用,再進行合併,合併的時候進行數據的交換。所以它是自下而上的排序方式。何爲自下而上?就是先解決子問題,再解決父問題。
2.快速排序,是先分區,在遞歸調用,分區的時候進行數據的交換。所以它是自上而下的排序方式。何爲自上而下?就是先解決父問題,再解決子問題。
五、思考
1.O(n)時間複雜度內求無序數組中第K大元素,比如4,2,5,12,3這樣一組數據,第3大元素是4。
我們選擇數組區間A[0...n-1]的最後一個元素作爲pivot,對數組A[0...n-1]進行原地分區,這樣數組就分成了3部分,A[0...p-1]、A[p]、A[p+1...n-1]。
如果如果p+1=K,那A[p]就是要求解的元素;如果K>p+1,說明第K大元素出現在A[p+1...n-1]區間,我們按照上面的思路遞歸地在A[p+1...n-1]這個區間查找。同理,如果K<p+1,那我們就在A[0...p-1]區間查找。
時間複雜度分析?
第一次分區查找,我們需要對大小爲n的數組進行分區操作,需要遍歷n個元素。第二次分區查找,我們需要對大小爲n/2的數組執行分區操作,需要遍歷n/2個元素。依次類推,分區遍歷元素的個數分別爲n、n/2、n/4、n/8、n/16......直到區間縮小爲1。如果把每次分區遍歷的元素個數累加起來,就是等比數列求和,結果爲2n-1。所以,上述解決問題的思路爲O(n)。
2.有10個訪問日誌文件,每個日誌文件大小約爲300MB,每個文件裏的日誌都是按照時間戳從小到大排序的。現在需要將這10個較小的日誌文件合併爲1個日誌文件,合併之後的日誌仍然按照時間戳從小到大排列。如果處理上述任務的機器內存只有1GB,你有什麼好的解決思路能快速地將這10個日誌文件合併?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章