目錄
第四章 快速排序
快速排序——一種常用的優雅的排序算法。快速排序使用分而治之的策略。
分而治之(divide and conquer,D&C)——一種著名的遞歸式問題解決方法。
4.1 分而治之 D&C
有一塊土地,你要將這塊地均勻地分成方塊,且分出的方塊要儘可能大。
D&C解決問題的過程包括兩個步驟:
(1) 找出基線條件,這種條件必須儘可能簡單。
(2) 不斷將問題分解(或者說縮小規模),直到符合基線條件。
在這個問題中,基線條件:最容易處理的情況是,一條邊的長度是另一條邊的整數倍。如果一邊長25 m,另一邊長50 m,那麼可使用的最大方塊爲 25 m×25 m。
遞歸條件:每次遞歸調用都必須縮小問題的規模。 首先找出這塊地可容納的最大方塊。
對餘下的那一小塊地使用相同的算法。、
因此,對於最初的那片土地,適用的最大方塊爲80 m× 80 m。
“適用於這小塊地的最大方塊,也是適用於整塊地的最大方塊。”具體原理參見歐幾里得算法:輾轉相除,找到兩個正整數的最大公約數。歐幾里得算法百度百科
D&C的工作原理:
(1) 找出簡單的基線條件;
(2) 確定如何縮小問題的規模,使其符合基線條件。
動手練習:
給定一個數字數組。 將這些數字相加,並返回結果。使用循環可以很方便完成任務。
循環實現數組裏的元素相加(code)
#用循環實現數組裏的數相加
def sum(arr):
total = 0
for i in arr:
total += i
return total
print(sum([5,4,3,2]))
#OUT: 14
遞歸函數實現數組裏的元素相加(code)
#用遞歸實現
def sum(arr):
b = arr.pop()
if arr == []: #基線條件
return b
else:
return b+sum(arr) #遞歸
print(sum([5,4,3,2]))
#OUT: 14
涉及數組的遞歸函數時,基線條件通常是數組爲空或只包含一個元素。
練習1
4.1 請編寫前述sum函數的代碼。
答:如上
4.2 編寫一個遞歸函數來計算列表包含的元素數。
答:如上
4.3 找出列表中最大的數字。
答:可以用循環,遞歸的方法或者調用python的函數。(選擇排序代碼就不放了)
#1.循環
def findlargest(arr):
large = arr[0]
for i in range(len(arr)):
if arr[i]>=large:
large = arr[i]
return large
print(findlargest([1,2,3,4,5]))
#OUT: 5
#2.遞歸 https://blog.csdn.net/Sukiyou_xixi/article/details/95099292
def find_max(arr):
tmp = arr.pop(0)
if len(arr) == 0:
return tmp
max = find_max(arr)
if max > tmp:
return max
else:
return tmp
print(find_max([15, 10, 90, 200, 20]))
#OUT: 200
#3.python 內置函數
a = [1,2,3,4]
print(max(a))
#OUT: 4
4.4 還記得第1章介紹的二分查找嗎?它也是一種分而治之算法。你能找出二分查找算法的基線條件和遞歸條件嗎?
答:基線條件是查找的數正好是想要的數。遞歸條件就是,在符合條件那邊繼續折半查找。
4.2 快速排序
快速排序也是一種常見的排序算法,比選擇排序快。
快速排序也採用了D&C,分而治之的策略:找基線條件,分解問題成基線條件去解決。遞歸的思想。
快速排序是怎麼進行的?
第一步找基線條件。
如果一個數組只有一個數,或者爲空,則排序就原樣返回,不需要排序。那麼排序的基線條件就是數組爲空或只包含一個元素。
如果數組有很多數,我們進行分解和歸納。
如果數組有兩個數:
如果有三個數 :要使用D&C,分解數組,直到滿足基線條件。
排序前,確定基準值(pivot),我們採用數組第一個元素作爲基準值,根據基準值進行分區。所有小於基準值的元素放在它左邊,大的放右邊,
分區完,你有一個由所有小於基準值的數字組成的子數組;基準值;一個由所有大於基準值的數組組成的子數組。
接着對左右兩個數組再進行以上操作:以基準值再分左右區,直至返回一個元素。
其實 任何元素用作基準值都可行。你能對含有一個、兩個、三個、四個元素的數組進行排序,同理以此類推,快速排序對任何長度的數組都管用。這裏涉及到一個歸納證明的知識點。
快速排序代碼
def quicksort(arr):
if len(arr)<2: #基線條件
return arr
else:
pivot = arr[0] #基準值
less = [i for i in arr[1:] if i<=pivot]
greater = [i for i in arr[1:] if i>pivot]
return quicksort(less)+[pivot]+quicksort(greater)
print(quicksort([10,5,2,3]))
OUT:[2, 3, 5, 10]
4.3 再談大 O 表示法
快速排序的獨特之處在於,其速度取決於選擇的基準值。討論快速排序的大O表示之前,回顧一下一些查找和排序算法的常見的大O運行時間。
還有一種名爲合併排序(merge sort)的排序算法,其運行時間爲O(n log n),比選擇排序快得多。
快速排序速度與基準值的選取有關,平均情況下快速排序的運行時間爲O(n log n),最糟情況下,其運行時間爲O()。
4.3.1 比較合併排序和快速排序
比如二分查找和簡單查找:
暫且認爲簡單查找一次時間常量(算法所需的固定時間量)爲10毫秒,二分查找時間常量是1秒。看起來好像是簡單查找快,但如果要在包含40億個元素的列表中查找:
還是二分查找快的多。如果兩個算法的大O表示不同,時間常量無關緊要。
但有時候,常量的影響可能很大。比如對快速查找和合並查找而言,快速查找的常量比合並查找小,因此如果它們的運行時間都爲O(n log n),快速查找的速度將更快。實際上,快速查找的速度確實更快,因爲相對於遇上最糟情況,它遇上平均情況的可能性要大得多。
什麼是平均情況?最糟情況?
4.3.2 平均情況和最糟情況
使用快速排序,考慮兩種基準值情況。
1.基準值每次都選擇列表的第一個元素,且處理的數組是有序的。
在此過程中,分區時,其中一個子數組始終爲空,這導致調用棧非常長。這是最糟情況,棧長爲O(n)。
2.也是上面的有序數組,但每次都選取數組的中間元素作爲基準值。
這樣每次都將數組分成兩半,所以不需要那麼多遞歸調用。你很快就到達了基線條件,因此調用棧短得多。 這是最佳情況,棧長爲O(logn)。
看第一種情況棧的第一層。你將一個元素用作基準值,並將其他的元素劃分到兩個子數組中。這涉及數組中的全部8個元素,因此該操作的時間爲O(n)。實際上,在調用棧的每層都涉及O(n)個元素。
第二種情況,以中間元素作爲基準,對半劃分,每次也將涉及O(n)個元素。
因此,完成每層所需的時間都爲O(n)。
第一種情況,有O(n)層,因此該算法的運行時間爲O(n) * O(n) = O(),最糟情況。
第二種情況,層數爲O(log n)(用技術術語說,調用棧的高度爲O(log n)),而每層需要的時間爲O(n)。因此整個算法需要的時間爲O(n) * O(log n) = O(n log n)。這就是最佳情況。
最佳情況也是平均情況。只要你每次都隨機地選擇一個數組元素作爲基準值,快速排序的平均運行時間就將爲O(n log n)。快速排序是最快的排序算法之一,也是D&C典範。
練習2
使用大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()
4.4 小結
D&C將問題逐步分解。使用D&C處理列表時,基線條件很可能是空數組或只包含一個元素的數組。
實現快速排序時,請隨機地選擇用作基準值的元素。快速排序的平均運行時間爲O(n log n)。
大O表示法中的常量有時候事關重大,這就是快速排序比合並排序快的原因所在。
比較簡單查找和二分查找時,常量幾乎無關緊要,因爲列表很長時,O(log n)的速度比O(n)快得多。