算法圖解part4:快速排序

1.分而治之 D&C(Divide and Conquer)

百度百科
所謂“分而治之” 就是把一個複雜的算法問題按一定的“分解”方法分爲等價的規模較小的若干部分,然後逐個解決,分別找出各部分的解,把各部分的解組成整個問題的解,這種樸素的思想來源於人們生活與工作的經驗,也完全適合於技術領域。諸如軟件的體系結構設計、模塊化設計都是分而治之的具體表現。

分而治之:一種著名的遞歸式問題解決方法。
三個例子來引入該思想:

  • 1.農場主分割田,要求將地皮均勻地分成方塊,且分出的方塊儘可能大;
  • 2.求一個數字數組的元素之和;
  • 3.快速排序

該思想的步驟:

1.找出基線條件,條件必須儘可能簡單
2.不斷將問題分解或者縮小規模,直到符合基線條件

D&C並非可用於解決問題的算法,而是一種解決問題的思想

1.1農場主分田

將田地均勻地分成方塊,且分出去的方塊要儘可能的大。
分田的例子中,將一條邊是另一條邊的整數倍作爲基線條件,遞歸條件爲最小邊是所切割的最大正方形邊。
遞歸條件爲縮小問題範圍,借鑑思想是:適用於這塊小地的最大方塊,也是適用於整塊地的最大方塊。
PS:這個思想可參考歐幾里得算法
在這裏插入圖片描述

1.2數組之和

用遞歸的思想寫出求和公式sum。具體思路如下:

  • 首先確定基線條件爲數組爲空或者是隻包含一個元素
    在這裏插入圖片描述
  • 遞歸條件呢?如何把問題慢慢分解呢?數組的元素之和可以是先以某種規律,取出一個元素,再將該元素與剩下的數組之和 相加,這個新數組可以再次使用規律繼續分解。。。。。。如圖:
    在這裏插入圖片描述
  • 在解決列表[2,4,6]的和問題中,整個函數的運行過程如下:
    在這裏插入圖片描述
    在這裏插入圖片描述
  • 對上述問題的小練習(python代碼):

1.sum函數的代碼:

def sum1(list):
    if list == []:
        return 0
    else:
        return list[0] + sum(list[1:])

sum1([1,2,3])

運行結果:

6

2.遞歸計算列表中的元素個數:

def count(list):
    if list == []:
        return 0
    else:
        return 1 + count(list[1:])

count([1,2,3])

運行結果:

3

3.遞歸找出最大數:

#未使用遞歸
def findmax(arr):
    max = arr[0]
    for i in range(1,len(arr)):
        if max < arr[i]:
            max = arr[i]
    return max

arr = [1,2,3,4,8,6,2,2]
findmax(arr)

運行結果:

8

使用遞歸的例子是元素數大於等於2的情況下:

#使用遞歸
def findvalue(list):
    if len(list) == 2:
        return list[0] if list[0] > list[1] else list[1]
    sub_max = findvalue(list[1:])
    return list[0] if list[0] > sub_max else sub_max

findvalue([1,8,3,4,5,4])

運行結果:

8

2.快速排序

還是一組數組,既然是數組,那麼上述D&C討論的基線條件也成立:當數組爲空或者只有一個元素。然後遞歸條件使問題範圍縮小,有了前文的鋪墊,摘出一個元素作爲基準值,當然可以是任意一個元素,我們默認第一個或者最後一個,此爲遞歸條件。下面是快速排序的精髓:

  • 1.選出基準值
  • 2.將數組分成兩個子數組:小於基準值的a和大於基準值的b
  • 3.對剩下來的兩個數組a,b進行快速排序
  • 4.合併數組 a + 基準值 + b

快速排序的要點就是:找基準,分數組,小數組左邊站,大數組右邊站,如此往復最終完成排序。就是這麼簡單!

Python代碼

//快速排序
def quicksort(list):
    if len(list)<2:                                    #基線條件:爲空或者只包含一個元素的數組是有序的
        return list
    else:                                              #遞歸條件
        root = list[0]                                 #基準值
        di   = [i for i in list[1:] if i <= root]      #小於等於基準值 爲di數組
        gao  = [i for i in list[1:] if i >  root]      #大於基準值爲gao數組
        return quicksort(di) + [root] + quicksort(gao) #對兩個數組快速排序遞歸,然後合併
        
quicksort([4,5,87,4,8,2,4,85,2,1,87,25,24,8,21,5,2,14,5,2])

運行結果:

[1, 2, 2, 2, 2, 4, 4, 4, 5, 5, 5, 8, 8, 14, 21, 24, 25, 85, 87, 87]

PS 拓展合併排序: 日後填坑
合併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。
合併排序法是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分爲若干個子序列,每個子序列是有序的。然後再把有序子序列合併爲整體有序序列。*

首先將含有N個元素的列表拆分成兩個含N/2個元素的兩個子列表,在進行歸併排序之前,希望這兩個子列表是排好序的,就可以利用遞歸的思想,繼續拆分並排序(最後拆分成N個子列表),然後合併兩個已排好序的子列表。

對於一個含N個元素的列表,需要log2Nlog2^N步把整個列表拆分成子列表,每一步至多需要比較N次,所以歸併排序最多需要Nlog2NN*log2^N次比較,是一種最爲常見的排序算法。

Python代碼:歸併遞歸 (日後填坑)

print("歸併排序")
c=[7,9,1,0,4,3,8,2,5,4,6]
#合併兩列表
def merge(a,b):#a,b是待合併的兩個列表,兩個列表分別都是有序的,合併後纔會有序
    merged = []
    i,j=0,0
    while i<len(a) and j<len(b):
        if a[i]<=b[j]:
            merged.append(a[i])
            i+=1
        else:
            merged.append(b[j])
            j+=1
    merged.extend(a[i:])
    merged.extend(b[j:])
    return merged
#遞歸操作
def merge_sort(c):
    if len(c)<=1:
        return c
    mid = len(c)//2#除法取整
    a = merge_sort(c[:mid])
    b = merge_sort(c[mid:])
    return merge(a,b)

merge_sort(c)

運行結果:(返回0,說明 1 在 list 的 0 號索引)

[0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 9]

3.再談大O表示法

  • 引一條經驗規則:c&lt;log2N&lt;n&lt;nlog2N&lt;n2&lt;n3&lt;2n&lt;3n&lt;n!c &lt; log2N &lt; n &lt; n * log2N &lt; n^2 &lt; n^3 &lt; 2^n &lt; 3^n &lt; n!

其中c是一個常量。如果一個算法的複雜度爲 clog2Nnnlog2Nc 、 log2N 、n 、 n*log2N ,那麼這個算法時間效率比較高 ;
如果是 2n,3n,n!2^n , 3^n ,n!,那麼稍微大一些的n就會令這個算法不能動了,居於中間的幾個則差強人意。

c指的是算法所需的固定時間,被稱爲常量

根據前面的經驗規則可以知道,這個常量c對同樣的算法複雜度影響比較大,如書中兩個函數第一個c爲10ms,第二個c爲1s,所以前者速度快些。對於不是同一量級複雜度的,如以簡單查找和二分查找作對比,c的作用影響很小

同一算法複雜度才談常量c;不是同一複雜度看經驗表排序。
快速排序與合併排序是同一複雜度O(nlogn)O(n*logn),而且快速排序的常數較小,故一般情況下使用快速排序。但是在最糟糕情況下,快速排序複雜度達到了O(n2)O(n^2)

排序算法穩定性: 快速、選擇排序不穩定;合併排序穩定
所謂穩定性是指待排序的序列中有兩元素相等,排序之後它們的先後順序不變.假如爲A1,A2.它們的索引分別爲1,2.則排序之後A1,A2的索引仍然是1和2.

  • 平均情況與最糟情況

快速排序有平均與最糟時間,性能高度依賴於你選擇的基準值

如圖,每次都選取第一個元素爲基準值,每次快速排序確實也分成了兩個數組,其中有一個數組每次都爲空。如此往復,棧的高度達到了8,也可也說選取第一個元素爲基準值進行排序,總共有八層 ,最後達到基線條件完成排序。這是最糟情況,棧長爲O(n)
在這裏插入圖片描述
下圖這種情況,調用棧的每一層都遍歷了所有元素,操作時間O(n);總共有8層,層數又是一個n。因此最糟糕情況的時間複雜度就有了簡單的表達:O=O(n)O(n)=n2O=O(n)*O(n)=n^2
在這裏插入圖片描述
接着就是理想最優情況:
在這裏插入圖片描述
基準值每次取中間,棧的高度爲4,即層數爲4。調用棧少了一半,棧高O(log2nlog2n),而每一層他也都是遍歷了所有元素,對所有元素操作時間也爲O(n)。那麼該平均情況時間複雜度:
O=O(log2n)O(n)=O(nlog2n)O=O(log2n)*O(n)=O(n*log2n)

最優的時間複雜度其實也就是平均複雜度時間,當你每次隨機取基準值的時候,他的複雜度也就是平均複雜度了。(最糟糕複雜度有兩個特定條件:1.有序;2.取首或尾元素。其實就兩種情況)

4.總結

  • 1.D&C將問題逐步分解。使用D&C處理列表時,基線條件很可能是空數組或只包含一個元素的數組。
  • 2.實現快速排序時,請隨機地選擇用作基準值的元素。快速排序的平均運行時間爲O(nlognn log n)。
  • 3.大O表示法中的常量有時候事關重大,這就是快速排序比合並排序快的原因所在。
  • 4.比較簡單查找和二分查找時,常量幾乎無關緊要,因爲列表很長時,O(log n)的速度比O(n)快得多。

5.歐幾里德算法

又稱輾轉相除法,是指用於計算兩個正整數a,b的最大公約數。
python代碼如下:

def gcd(a,b):
    while a != 0:
        a,b = b % a, a
    return b
gcd(40,25)

運行結果:

5

6.參考資料

《算法圖解》第四章

此部分學習算法內容已上傳github:https://github.com/ShuaiWang-Code/Algorithm/tree/master/Chapter4

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