第四章 快速排序
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)快得多。