Python中的堆排序

來源:stackabuse.com/heap-sort-i…python

做者:Olivera Popović面試

翻譯:老齊算法


介紹

堆排序是高效排序算法的另外一個例子,它的主要優勢是,不管輸入數據如何,它的最壞狀況運行時間都是O(n*logn)。api

顧名思義,堆排序在很大程度上依賴於堆數據結構——優先級隊列的常見實現。數組

毫無疑問,堆排序是一種簡單的排序算法,並且與其餘簡單實現相比,堆排序是更有效,也很常見。安全

堆排序

堆排序的工做原理是從堆逐個「移除」元素並將它們添加到已排序的數組裏,在進一步解釋和從新訪問堆數據結構以前,咱們應該瞭解堆排序自己的一些屬性。bash

它是一種原地算法(譯者注:in-place algorithm,多數翻譯爲「原地算法」,少數也翻譯爲「就地算法」。這種算法是使用小的、固定數量的額外內存空間來轉換資料的算法。),意味着它須要恆定數量的內存,即所需內存不取決於初始數組自己的大小,而取決於存儲該數組所需的內存。微信

例如,不須要原始數組的副本,也不須要遞歸和遞歸調用堆棧。最簡單的堆排序實現一般使用第二個數組來存儲排序後的值。咱們將使用這種方法,由於它在代碼中更直觀、更易於實現,但它也是百分百的原地算法。數據結構

堆排序不穩定,意思是相等的值,並不會在一樣的相對位次上。對於整數、字符串等這些基本類型,不會出現這類問題,但當咱們對複雜類型的對象排序時,可能會遇到。app

例如,假設咱們有一個自定義類Person帶有agename屬性,在一個數組中幾個此類的實例對象,好比按順序出現19歲的名叫「Mike」的人和一個19歲的名叫「David」的人。

若是咱們決定按年齡對這些人進行排序,就不能在排序數組中保證「Mike」會出如今「David」以前,即便他們在初始數組中是按這個順序出現的。「Mike」有可能出如今「David」以前,但不能保證百分之百如此。

堆數據結構

堆是計算機科學中最流行和最經常使用的一種數據結構——更不用說在軟件工程面試中很是流行了

咱們將討論跟蹤最小元素(最小堆)的堆,但它們也能夠很容易地實現對最大元素(最大堆)的跟蹤。

簡單地說,最小堆是一種基於樹的數據結構,其中每一個節點比其全部子節點都小。一般使用二叉樹。堆有三個基本操做——delete_minimum()get_minimum()add()

每次,你只能刪除堆中的第一個元素,而後對其進行「從新排序」。在添加或刪除元素後,堆對本身會「從新排序」,以便最小的元素始終處於第一個位置。

注意:這毫不意味着堆是排序的數組。每一個節點都小於其子節點這一事實不足以保證整個堆是按升序排列的。

咱們來看一個關於堆的例子:

正如咱們看到的,上面的例子確實符合堆的描述,可是沒有排序。咱們不會詳細討論堆實現,由於這不是本文的重點。當在堆排序中使用堆數據結構時,咱們所利用的堆數據結構的關鍵優點是:下一個最小的元素始終是堆中的第一個元素。

實現

數組排序

譯者注: 做者在本文中並無嚴格區分Python中的列表和數組,而是將列表看作了數組,這對於列表中的元素是同一種類型的元素而言,無可厚非。對於排序,只有是同一種類型的元素,纔有意義。

Python提供了建立和使用堆的方法,因此咱們沒必要本身單獨爲了實現它們去寫代碼了:

  • heappush(list, item):向堆中添加一個元素,而後對其從新排序,使其保持堆狀態。可用於空列表。
  • heappop(list):刪除第一個(最小的)元素並返回該元素。此操做以後,堆仍然是一個堆,所以咱們沒必要調用heapify()
  • heapify(list):將給定的列表變成一個堆。

如今咱們知道了這些,堆排序的實現就至關簡單了:

from heapq import heappop, heappush

def heap_sort(array):
    heap = []
    for element in array:
        heappush(heap, element)

    ordered = []

    # While we have elements left in the heap
    while heap:
        ordered.append(heappop(heap))

    return ordered

array = [13, 21, 15, 5, 26, 4, 17, 18, 24, 2]
print(heap_sort(array))
複製代碼

輸出

[2, 4, 5, 13, 15, 17, 18, 21, 24, 26]
複製代碼

如咱們所見,堆數據結構的繁重工做已經完成,咱們所要作的只是添加所需的全部元素並逐個刪除它們。它就像一臺硬幣計數機,根據輸入的硬幣的價值對它們進行分類,而後咱們能夠取出它們。

自定義對象排序

當使用自定義類時,事情會變得更加複雜。一般,爲了使用咱們的排序算法,建議不要重寫類中的比較運算符,而是建議重寫該算法,以便使用lambda函數比較。

可是,因爲咱們的實現依賴於內置堆方法,所以不能在這裏這樣作。

Python確實提供瞭如下方法:

  • heapq.nlargest(*n*, *iterable*, *key=None*):返回一個列表,其中包含由iterable定義的數據集中的n個最大元素。
  • heapq.nsmallest(*n*, *iterable*, *key=None*):返回一個列表,其中包含由iterable定義的數據集中的n個最小元素。

咱們可使用它來簡單地獲取n = len(array)最大/最小元素,可是方法自己不使用堆排序,本質上等同於只調用sorted()方法。

咱們留給自定義類的惟一解決方案是實際重寫比較運算符。遺憾的是,這使咱們侷限於對每一個類只能進行一種比較。在咱們的示例中,咱們被侷限於按年份對Movie對象進行排序。

可是,它確實讓咱們演示了在自定義類上使用堆排序。咱們來定義Movie類:

from heapq import heappop, heappush

class Movie:
    def __init__(self, title, year):
        self.title = title
        self.year = year

    def __str__(self):
        return str.format("Title: {}, Year: {}", self.title, self.year)

    def __lt__(self, other):
        return self.year < other.year

    def __gt__(self, other):
        return other.__lt__(self)

    def __eq__(self, other):
        return self.year == other.year

    def __ne__(self, other):
        return not self.__eq__(other)
複製代碼

如今,讓咱們稍微修改一下heap_sort()函數:

def heap_sort(array):
    heap = []
    for element in array:
        heappush(heap, element)

    ordered = []

    while heap:
        ordered.append(heappop(heap))

    return ordered
複製代碼

最後,讓咱們實例化一些電影,將它們放入一個數組中,而後對它們進行排序:

movie1 = Movie("Citizen Kane", 1941)
movie2 = Movie("Back to the Future", 1985)
movie3 = Movie("Forrest Gump", 1994)
movie4 = Movie("The Silence of the Lambs", 1991);
movie5 = Movie("Gia", 1998)

array = [movie1, movie2, movie3, movie4, movie5]

for movie in heap_sort(array):
    print(movie)
複製代碼

輸出:

Title: Citizen Kane, Year: 1941
Title: Back to the Future, Year: 1985
Title: The Silence of the Lambs, Year: 1991
Title: Forrest Gump, Year: 1994
Title: Gia, Year: 1998
複製代碼

與其餘排序算法的比較

堆排序被普遍使用,主要緣由是它的可靠性,儘管它常常被運行良好的「快速排序」法所超越(譯者注: 本文的微信公衆號「老齊教室」以系列文章,介紹各類排序算法,而且用Python語言實現,敬請關注)。

堆排序的主要優勢是時間複雜度上的O(n*logn)上限以及安全性。Linux內核開發人員給出了使用堆排序而不是快速排序的如下理由:

堆排序的平均排序時間和最壞排序時間均爲O(n*logn),雖然qsort的平均速度快了20%,但不得不容忍O(n*n)的最壞可能情形和額外的內存支出,這使它不太適合在操做系統內核中使用。

此外,快速排序算法法在可預測的狀況下表現不佳。而且,若是對內部實現有足夠的瞭解,你可能會意識到它形成的安全風險(主要是DDoS攻擊),由於不良的O(n^2)行爲很容易被觸發。

常常被用來與堆排序比較的另外一種算法是歸併排序算法(譯者注: 本微信公衆號也會刊發相關文章給予介紹,敬請關注),它們具備相同的時間複雜度。

歸併排序的優勢是穩定、可並行運算,而堆排序二者都作不到。

另外一個注意事項是:即便複雜度相同,堆排序在大多數狀況下也比歸併排序慢,由於堆排序具備較大的常數因子。

然而,堆排序比歸併排序更容易實現,所以當內存比速度更重要時,它是首選。

結論

正如咱們所看到的,堆排序不像其餘高效的通用算法那麼流行,可是它的可預測行爲(而不是不穩定的行爲)使它成爲一個很好的算法,適用於內存和安全性比稍快的運行速度更重要的場合。

實現和利用Python提供的內置功能是很是直觀的,咱們實際上要作的就是將元素放在一個堆中並取出它們,就像對待硬幣計數器同樣。

關注微信公衆號:老齊教室。讀深度文章,得精湛技藝,享絢麗人生。

WechatIMG6
相關文章
相關標籤/搜索