本题要求实现一个用选择法对整数数组进行简单排序的函数。_通俗易懂讲 Python 算法:快速排序...
原文:https://stackabuse.com/quicksort-in-python/
作者:Marcus Sanatan
譯者:老齊
歡迎在?bilibili? 搜索 freeCodeCamp 官方賬號或者直接訪問 https://space.bilibili.com/335505768 觀看我們的技術視頻介紹
快速排序是一種流行的排序算法,經常與歸并排序一起使用。快速排序是一個高效的排序算法,平均復雜度為 O(nlogn)。它受歡迎的部分原因還在于易于實施。
在本文中,我們將先使用整數集合演示快速排序算法,然后會用自定義對象深入探討這種算法的實現方式。
在分治法、原地排序和非穩定排序中,都有快速排序算法。
- 在分治法中,對大數組使用遞歸進行排序之前,使用快速排序算法將數組拆分為更小的數組,直到最后得到一個空數組或只有一個元素的數組。
- 在原地排序中,快速排序不創建數組的任何副本,它在內存中完成所有的操作。
- 穩定排序指相同的值在數組中的順序也是相同的,非穩定的排序算法不能保證這一點,只能說這樣的順序當然有可能出現,但不能保證。這在針對對象排序而不是對基本類型排序時變得很重要。例如,假設有幾個自定義的?Person 類實例具有相同的?age,即 21 歲的 Dave 和 21 歲的 Mike。如果要對包含 Dave 和 Mike 的集合使用快速排序法按年齡排序,則無法保證每次運行算法時 Dave 都會出現在 Mike 之前,反之亦然。
快速排序
快速排序算法的執行流程如下:
- 將集合分成兩個(大致)相等的部分,取一個偽隨機元素并將其用作主元(注:“主元”,原文中 pivot,在多數介紹快速排序算法的中文資料中,并不對齊單獨命名,只簡單說成是“分割點”,即劃分集合的元素,但本文作者使用了?pivot 這個詞來表示“分割點”,譯者認為很便于理解和表述,并將其翻譯為“主元”)。
- 小于主元的元素將移動到其左側,大于主元的元素將移動到其的右側。
- 分別對于主元左側和右側的元素,重復此上述,直到對整個數組完成排序。
當我們將某個元素描述為比另一個元素“大”或“小”時,并不一定意味著這些元素是整數,我們可以自定義對象,并根據選擇的任何屬性進行排序。
假設有一個自定義類?Person,每個實例都有?name?和?age?屬性,我們可以按姓名(詞典順序)或按年齡(升序或降序)進行排序。
快速排序算法的原理
對于快速排序算法,很多時候,數組是不能等分的,這是因為整個過程取決于如何選擇主元。我們需要選擇一個主元,使其比一半的元素大,比另一半的元素小。盡管這個過程看起來很直觀,但很難做到。
想一想:如何為數組選擇合適的主元?關于這個問題的解決方法,在快速排序算法的發展史上有過好多種。如果隨機選擇,是行不通的,因為這不能保障主元是合適的,隨機選擇的代價會非常“昂貴”。此外,還曾經有人提出從中間選擇一個元元素,或者選擇第一個、或者后半部分中間的,設置用更復雜的遞歸公式選擇,等等。
最直接的最簡單的方法是選擇第一個(或最后一個)元素作為主元,具有諷刺意味的是,這會導致快速排序法對已經排序(或幾乎排序了)的數組執行效果非常糟糕。
但是,大多數人還是用這種方法來實現快速排序算法,因為它很簡單,而且這種選擇主元的方式是一種非常有效的操作(我們需要重復執行),這也正是我們下面要做的。
既然我們選擇了一個主元,接下來該用它做些什么?同樣,有幾種方法可以處理各個分區。我們要設置三個指針,有一個指向主元,另外兩個分別指向“較小”元素和“較大”元素。
然后移動元素,使所有小于主元的元素都在左側,而所有較大的元素都在右側。更小或更大的元素不一定會被排序,我們只希望它們位于主元的適當的一側。然后,用遞歸方法遍歷主元的左側和右側。
一步一步來看看我們計劃要做的事情,從而理解以上所說的快速排序算法的執行過程。使用下面顯示的數組,我們選擇了第一個元素作為主元(29),low 表示指向較小元素的指針,最右邊的 high 表示指向較大元素的指針。
- 29 是第一主元,low 指向 99,high 指向 44
29 | 99 (low),27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,21,44 (high)
- high 從右向左移動,直到找到一個小于主元的值
29 | 99 (low),27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,21 (high),44
- 現在 high 指向了 21,它是一個比主元小的元素,我們想在數組的開頭附近找到一個值,使之可以用于與 21 交換。用一個比主元還小的值交換是沒有意義的,所以如果 low 指向一個更小的元素,我們試著找到一個更大的元素。
- 將 low 變量向右移動,直到找到一個大于主元的元素。幸運的是, low 已經定位在 99,它就比主元大
- 交換 high 和 low 指向的元素:
29 | 21 (low),27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,99 (high),44
- 在我們這樣做之后,就將 high 繼續向左移動,將 low 向右邊移動(21 和 99 現在所處的位置是正確的)。
- 再次,將 high 向左移動,下一個數字就是小于主元的 12。
- 現在我們通過將 low 向右移動來找一個大于主元的值,就是 41 了,它是第一個這樣的值
不斷重復上述過程,直到 low 指針和 high 指針最終在某個元素相遇:
29 | 21,27,12,19,28 (low/high),44,78,87,66,31,76,58,88,83,97,41,99,44
- 不再使用 29 作為主元了,所以剩下唯一要做的就是交換主元和 high 所指元素 28,然后我們就完成了這個遞歸步驟:
28,21,27,12,19,29,44,78,87,66,31,76,58,88,83,97,41,99,44
如你所見,我們已經讓小于 29 的所有值現在都在 29 的左側,大于 29 的所有值都在右側。
然后,算法對 28、21、27、12、19(左側)集合和44、78、87、66、31、76、58、88、83、97、41、99、44(右側)集合執行相同的操作。
實現
對數組排序
快速排序是一種自然遞歸算法,將輸入數組分成較小的數組,將元素移動到主元的適當一側,然后重復。
讓我們來看看幾個遞歸調用:
- 當第一次調用算法時,我們考慮所有的元素——從索引 0 到 n-1,其中 n 是數組中的元素數量。
- 如果主元最終位于位置 k,那么我們將對從 0 到 k-1 和從 k+1 到 n-1 的元素重復該過程。
- 在將元素從 k+1 排到 n-1 時,當前主元將在某個位置 p 結束。然后我們將元素從 k+1 序到 p-1,從 p+1 排序到 n-1,依此類推。
也就是說,我們將使用兩個函數:partition()?和 quick_sort()。quick_sort()?函數將首先用?partition()?函數對集合分組,然后在每組上遞歸調用自己。
下面從 partition() 函數開始:
def?partition(array,?start,?end):????pivot?=?array[start]
????low?=?start?+?1
????high?=?end
????while?True:
????????#?If?the?current?value?we're?looking?at?is?larger?than?the?pivot
????????#?it's?in?the?right?place?(right?side?of?pivot)?and?we?can?move?left,
????????#?to?the?next?element.
????????#?We?also?need?to?make?sure?we?haven't?surpassed?the?low?pointer,?since?that
????????#?indicates?we?have?already?moved?all?the?elements?to?their?correct?side?of?the?pivot
????????while?low?<=?high?and?array[high]?>=?pivot:
????????????high?=?high?-?1
????????#?Opposite?process?of?the?one?above
????????while?low?<=?high?and?array[low]?<=?pivot:
????????????low?=?low?+?1
????????#?We?either?found?a?value?for?both?high?and?low?that?is?out?of?order
????????#?or?low?is?higher?than?high,?in?which?case?we?exit?the?loop
????????if?low?<=?high:
????????????array[low],?array[high]?=?array[high],?array[low]
????????????#?The?loop?continues
????????else:
????????????#?We?exit?out?of?the?loop
????????????break
????array[start],?array[high]?=?array[high],?array[start]
????return?high
最后,讓我們實現?quick_sort()?函數:
def?quick_sort(array,?start,?end):????if?start?>=?end:
????????return
????p?=?partition(array,?start,?end)
????quick_sort(array,?start,?p-1)
????quick_sort(array,?p+1,?end)
有了這兩個函數,就可以對一個簡單的數組執行 quick_sort():
array?=?[29,99,27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,21,44]quick_sort(array,?0,?len(array)?-?1)
print(array)
輸出:
[12,?19,?21,?27,?28,?29,?31,?41,?44,?44,?58,?66,?76,?78,?83,?87,?88,?97,?99]由于此算法是非穩定的,所以不能保證這兩個 44 的順序總是這樣的,也許會交換 —— 雖然這在整數數組中意義不大。
對自定義對象進行排序
有幾種方法可以重寫此算法,以便在 Python 中對自定義對象進行排序。一種非常典型的 Python 方法是實現給定類的比較運算符,這意味著我們實際上不需要更改算法實現,因為?>、=、<=?等也可以應用于類對象。
另一個選擇是以參數的方式給函數傳入一個比較函數,用這個方法來執行對象的實際比較。用這種方式重寫算法函數以用于自定義對象是相當簡單的。但是請記住,算法并不穩定。
讓我們從 Person 類開始:
class?Person:????def?__init__(self,?name,?age):
????????self.name?=?name
????????self.age?=?age
????def?__str__(self):
????????return?self.name
這是一個非常基本的類,只有兩個屬性,name 和 age。我們希望使用 age 作為排序鍵,這將通過為排序算法提供自定義 lambda 函數來實現。
但首先,讓我們看看如何在函數中實現算法。我們沒有直接使用 <= 或 >= 運算符進行比較,而是在函數中來判斷哪個 Person 實例的年齡更高:
def?partition(array,?start,?end,?compare_func):????pivot?=?array[start]
????low?=?start?+?1
????high?=?end
????while?True:
????????while?low?<=?high?and?compare_fun(array[high],?pivot):
????????????high?=?high?-?1
????????while?low?<=?high?and?not?compare_fun(array[low],?pivot):
????????????low?=?low?+?1
????????if?low?<=?high:
????????????array[low],?array[high]?=?array[high],?array[low]
????????else:
????????????break
????array[start],?array[high]?=?array[high],?array[start]
????return?high
def?quick_sort(array,?start,?end,?compare_func):
????if?start?>=?end:
????????return
????p?=?partition(array,?start,?end,?compare_func)
????quick_sort(array,?start,?p-1,?compare_func)
????quick_sort(array,?p+1,?end,?compare_func)
現在,讓我們對這些實例對象的集合進行排序。你可以看到,在 quick_sort 函數的參數中,有 lambda 函數,它會對 age 屬性的值進行實際的比較:
p1?=?Person("Dave",?21)p2?=?Person("Jane",?58)
p3?=?Person("Matthew",?43)
p4?=?Person("Mike",?21)
p5?=?Person("Tim",?10)
array?=?[p1,p2,p3,p4,p5]
quick_sort(array,?0,?len(array)?-?1,?lambda?x,?y:?x.age?for?person?in?array:
????print(person)
輸出是:
TimDave
Mike
Matthew
Jane
通過這種方式實現算法,只要提供適當的比較函數,它就可以用于我們選擇的任何自定義對象。
優化快速排序算法
考慮到快速排序獨立地對給定數組的“一半”進行排序,那么它就非常便于實現并行計算。我們可以用一個單獨的線程對數組的每個“一半”進行排序,理想情況下,可以將排序所需的時間減半。
但是,如果我們在選擇主元時特別不走運,快速排序法可能會遞歸太深,此時即使用并行計算,其效率也不如歸并排序。
對小數組進行排序時,建議使用非遞歸算法。即使是像插入排序這樣的簡單操作,在小數組上也比快速排序更有效。因此,理想情況下,我們可以檢查排序對象是否只有少量元素(大多數建議是 10 個或更少),如果是這樣,就用插入排序代替。
快速排序的一個流行變體是多主元快速排序,它使用 n-1 個主元將原始數組分解為 n 個較小的數組。然而,大多數情況下只使用兩個主元,而不是更多。
有趣的事實:Java 7 的排序實現中使用了雙主元快速排序,針對較小數組的則是插入排序。
結論
正如我們前面提到的,快速排序的效率很大程度上取決于主元的選擇——它可以成就或突破算法時間(和堆棧空間)的復雜度。在使用自定義對象時,算法的非穩定性也是一個潛在的問題。
然而,盡管如此,快速排序的平均時間復雜度 O(nlogn) 和相對低的內存使用、簡單的實現方法,使它成為一種非常有效和流行的算法。
推薦閱讀:
30 行 Python 代碼實現 Twitch 主播上線實時通知
從軟件工程專業思考到的大前端技術棧
非營利組織 freeCodeCamp.org 自 2014 年成立以來,以“幫助人們免費學習編程”為使命,創建了大量免費的編程教程,包括交互式課程、視頻課程、文章等。線下開發者社區遍布 160 多個國家、2000 多個城市。我們正在幫助全球數百萬人學習編程,希望讓世界上每個人都有機會獲得免費的優質的編程教育資源,成為開發者或者運用編程去解決問題。
你也想成為 freeCodeCamp 社區的貢獻者嗎?歡迎了解?招募丨freeCodeCamp 翻譯計劃。
點擊【在看】,與朋友交流?
總結
以上是生活随笔為你收集整理的本题要求实现一个用选择法对整数数组进行简单排序的函数。_通俗易懂讲 Python 算法:快速排序...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: BZOJ 1008 [HNOI2008]
- 下一篇: ipad mini2屏幕维修+ios12