《算法圖解》——第四章 快速排序


                      第四章   快速排序

1    分而治之(divided and conquer,D&C)

第一個🌰:如何將一塊地均勻地分成方塊,並確保分出的方塊是最大的呢?

使用D&C策略(並非解決問題的算法,而是一種解決問題的思路)!D&C解決問題的兩個步驟:

①找出基線條件,盡可能的簡單

②不斷講問題分界,或者說縮小規模,使其滿足基線條件

首先基線條件:一個條邊的長度是另一條邊的兩倍。50m*25m

再找遞歸條件,這就要用到D&C策略了,先找出可容納的最大方塊,對余下方塊使用同樣的算法。

你可以從這塊地中划出兩個640 m×640 m的方塊,同時余下一小塊地640m*400m,適用於這小塊地的最大方塊,也是適用於整塊地的最大方塊(涉及歐幾里得算法)。對這塊地實用同樣的辦法,變成400m*240m,然后變為240m*160m,最后變為160m*80,達到了基線條件。

 

第二個🌰:給定一個數字數組。需要加總並返回

用循環很容易完成:

def sum(arr):
    total = 0
    for x in arr:
        total += x
    return total
print(sum([1, 2, 3, 4]))

遞歸如何處理?

①找出基線條件:如果數組中只有一個數或者沒有數,那么加總很好算

②每次遞歸調用都必須離空數組更近一步。

兩者之間是等效的,但是右邊的數組更短,縮小了問題的規模。

sum()函數的遞歸運行過程:

提示:編寫涉及數組的遞歸函數時,基線條件通常是數組為空或只包含一個元素。陷入困境時,請檢查基線條件是不是這樣的。

練習

4.1 請編寫前述 sum 函數的代碼。

def sum(list):
    if list == []:
      return 0
    return list[0] + sum(list[1:])
print(sum([1,2,3,4,5,6,7]))

4.2 編寫一個遞歸函數來計算列表包含的元素數。

def sum(list):
    if list == []:
      return 0
    return list[0] + sum(list[1:])
print(sum([1,2,3,4,5,6,7]))

4.3 找出列表中最大的數字。

def max(list):
    if len(list) == 2:
        return list[0] if list[0] > list[1] else list[1]
    sub_max = max(list[1:])                               #這里如果list很大,棧的空間就會很大,因為max()函數一直在運行,只到list被切分成長度為2
    return list[0] if list[0] > sub_max else sub_max
print(max([1,2,3,4,5,6,7,8,9]))


4.4 還記得第1章介紹的二分查找嗎?它也是一種分而治之算法。你能找出二分查找算法的基線條件和遞歸條件嗎?

基線條件:數組只包含一個元素。

遞歸條件:把數組分成兩半,將其中一半丟棄,並對另一半執行二分查找。


 

 

 

2    快速排序

基線條件:數組為空或者只有一個元素,這樣就不需要排序了

兩個元素的數組進行元素比較即可。三個元素呢?使用D&C,將數組分解,直到滿足基線條件。

快速排序的工作原理:

①從數組中選擇一個元素,這個元素被稱為基准值(pivot)

②找出比基准值小的元素以及比基准值大的元素,這被稱為分區(partitioning),數組變為:

一個由所有小於基准值的數字組成的數組;基准值;一個由所有大於基准值的數組組成的子數組。

現在要對子數組進行排序,對於包含兩個元素的數組(左邊的子數組)以及空數組(右邊的子數組),快速排序知道如何將它們排序,因此只要對這兩個子數組進行快速排序,再合並結果,就能得到一個有序數組!

對三個元素的數組進行排序:

①選擇基准值。

②將數組分成兩個子數組:小於基准值的元素和大於基准值的元素。

③對這兩個子數組進行快速排序。

包含四個元素呢?同樣的做法,找出一個基准值,如果一個子數組有三個元素,對三個元素遞歸調用快速排序。那么五個元素同樣也可以。

代碼表示:

def quicksort(array):
    if len(array) < 2:
        return array
    else:
        pivot = array[0]                                       #將數組的第一個元素定為基准線
        less = [i for i in array[1:] if i <= pivot]            #遍歷數組剩下元素,如果小於它,放入less列表
        greater = [i for i in array[1:] if i > pivot]          #遍歷數組剩下元素,如果大於它,放入greater列表
        return quicksort(less) + [pivot] + quicksort(greater)  #對less和greater遞歸,最后返回排序數組
print quicksort([10, 5, 2, 3])


 

 

3    再談大O表示法

快速排序的情況比較棘手,在最糟情況下,其運行時間為O(n 2 )。

與選擇排序一樣慢!但這是最糟情況。在平均情況下,快速排序的運行時間為O(n log n)。

比較合並排序和快速排序:

有時候,常量的影響可能很大,對快速查找和合並查找來說就是如此。快速查找的常量比合並查找小,因此如果它們的運行時間都為O(n log n),快速查找的速度將更快。實際上,快速查找的速度確實更快,因為相對於遇上最糟情況,它遇上平均情況的可能性要大得多。


 

 

4    平均情況和最糟情況

快速排序的性能高度依賴你選擇的基准值。假設你總將第一個元素用作基准值,那么棧長為O(n),如果你總將中間的元素用作基准值,那么棧長為O(log n)。

實際上,在調用棧的每層都涉及O(n)個元素。

因此,完成每層所需的時間都是O(n)

在第二張圖中,層數為O(log n)(用技術術語說,調用棧的高度為O(log n)),每層需要的時間為O(n)。因此整個算法需要的時間為O(n)  * O(log n) = O(n log n)。這就是最佳情況。在最糟情況下,有O(n)層,因此該算法的運行時間為O(n)  * O(n) =   O(n **2 )。

這里要告訴你的是,最佳情況也是平均情況。只要你每次都隨機地選擇一個數組元素作為基准值,快速排序的平均運行時間就將為O(n log n)。

 

練習
使用大O表示法時,下面各種操作都需要多長時間?

4.5 打印數組中每個元素的值。O(n)

4.6 將數組中每個元素的值都乘以2。O(n)

4.7 只將數組中第一個元素的值乘以2。O(1)

4.8 根據數組包含的元素創建一個乘法表,即如果數組為[2, 3, 7, 8, 10],首先將每個元素都乘以2,再將每個元素都乘以3,然后將每個元素都乘以7,以此類推。O(n**2 )。


 

 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM