[譯] Python 實現排序算法

Python 實現排序算法

簡介

有時,咱們在應用程序中存儲或檢索的數據有多是亂序的。若是想要正確處理或者有效使用數據,咱們可能須要對數據從新排序。多年來,計算機科學家創造了許多排序算法來處理數據。前端

在本文中,咱們將瞭解一些流行的排序算法,瞭解它們是如何工做的,並用 Python 來實現它們。們還將會比較它們對列表中的元素排序的速度。python

爲了簡單起見,這些算法將對列表中的數字都進行升序排序。固然,你能夠根據本身的須要來自由調整。android

冒泡排序

這個簡單的排序算法會經過迭代列表成對的比較列表中的元素,而且交換它們,直到較大的元素「冒泡」到列表的末尾,較小的元素保持在「底部」。ios

介紹

咱們首先比較列表的前兩個元素。若是第一個元素大於第二個元素,咱們交換它們。若是它們已經排好序,咱們將它們保持原樣。而後咱們移動到下一對元素,比較它們的值,並根據須要交換。這個過程將持續到列表中的最後一對元素。git

當到達列表的末尾時,它會對每對元素重複此過程。可是,這個過程是很低效的。若是咱們只須要在數組裏面進行一次交換怎麼辦?爲何咱們仍然會迭代 n^2 次,即便數組已經排好序了?github

顯然,爲了優化算法,咱們須要在完成排序時中止它。算法

那咱們怎麼知道已經完成了排序?若是元素是有序的,那咱們就沒必要繼續交換。所以,每當交換值時,咱們會將一個標誌值設置爲 True 以重複排序過程。若是沒有發生交換,標誌值將保持爲 False,算法將中止。後端

實現

優化以後,咱們能夠經過如下的 Python 代碼來實現冒泡排序:api

def bubble_sort(nums):
    # 咱們將標誌值 swapped 設爲 True,以便循環可以執行至少一次
    swapped = True
    while swapped:
        swapped = False
        for i in range(len(nums) - 1):
            if nums[i] > nums[i + 1]:
                # 交換元素
                nums[i], nums[i + 1] = nums[i + 1], nums[i]
                # 把標誌值設爲 True 以便咱們能再次循環
                swapped = True

# 檢查是否可以正確執行
random_list_of_nums = [5, 2, 1, 8, 4]
bubble_sort(random_list_of_nums)
print(random_list_of_nums)
複製代碼

這個算法在一個 while 循環裏面運行,僅當沒有元素可以交換時纔會跳出循環。咱們在開始時將 swapped 設爲 True,以確保算法至少能夠執行一次。數組

時間複雜度

在最壞的狀況下(當列表處於相反的順序時),該算法必須交換數組的每一個項。每次迭代的時候,標誌值 swapped 都會被設置爲 True。所以,若是咱們在列表中有 n 個元素,咱們將對每一個元素迭代 n 次,所以冒泡排序的時間複雜度爲 O(n^2)。

選擇排序

該算法將列表分爲兩部分:已排序部分和未排序部分。咱們不斷地刪除列表中未排序部分的最小元素,並將其添加到已排序部分中。

介紹

實際上,咱們並不須要爲已排序的元素建立一個新的列表,咱們要作的是將列表最左邊的部分做爲已排序部分。而後咱們搜索整個列表中最小的元素,並將其與第一個元素交換。

如今咱們知道列表的第一個元素是有序的,咱們將繼續搜索剩餘元素中最小的元素,並將其與第二個元素交換。這將迭代到待檢查元素是剩餘列表的最後一項時。

實現

def selection_sort(nums):
    # i 的值對應於已排序值的數量
    for i in range(len(nums)):
        # 咱們假設未排序部分的第一項是最小的
        lowest_value_index = i
        # 這個循環用來迭代未排序的項
        for j in range(i + 1, len(nums)):
            if nums[j] < nums[lowest_value_index]:
                lowest_value_index = j
        # 將未排序元素的最小的值與第一個未排序的元素的值相交換
        nums[i], nums[lowest_value_index] = nums[lowest_value_index], nums[i]

# 檢驗算法是否正確
random_list_of_nums = [12, 8, 3, 20, 11]
selection_sort(random_list_of_nums)
print(random_list_of_nums)
複製代碼

能夠看到隨着 i 的增長,咱們須要檢查的元素愈來愈少。

時間複雜度

在選擇排序算法中,咱們能夠經過檢查 for 循環次數來輕鬆獲得時間複雜度。對於一個有 n 個元素的列表,外層循環會迭代 n 次。當 i 的值爲 1 時,內層循環會迭代 n-1 次,i 值爲 2 時迭代 n-2 次而後依此類推。

算法比較的次數和爲 (n - 1) + (n - 2) + ... + 1,由此可得選擇排序算法的時間複雜度爲 O(n^2)。

插入排序

與選擇排序同樣,該算法將列表分爲已排序部分和未排序部分。它會經過迭代未排序的部分將遍歷到的元素插入到排序列表中的正確位置。

介紹

咱們假設列表的第一個元素已排序。而後咱們遍歷到下一個元素,咱們稱之爲 x。若是 x 值大於第一個元素,咱們將繼續遍歷。若是 x 值較小,咱們將第一個元素的值複製到第二個位置,而後將第一個元素值設置爲 x

當咱們處理未排序部分的其餘元素時,咱們不斷地將已排序部分中較大的元素向上移動,直到遇到小於 x 的元素或到達已排序部末尾的元素,而後將 x 放在正確的位置。

實現

def insertion_sort(nums):
    # 咱們假設第一個元素已經排好序,而後從第二個元素開始遍歷
    for i in range(1, len(nums)):
        item_to_insert = nums[i]
        # 同時保留上一個元素的下標的索引
        j = i - 1
        # 若是排序段的全部項大於要插入的項,則將其向前移動
        while j >= 0 and nums[j] > item_to_insert:
            nums[j + 1] = nums[j]
            j -= 1
        # 插入的元素
        nums[j + 1] = item_to_insert

# 驗證算法是否正確
random_list_of_nums = [9, 1, 15, 28, 6]
insertion_sort(random_list_of_nums)
print(random_list_of_nums)
複製代碼

時間複雜度

在最壞的狀況下,數組將按相反的順序排序。插入排序函數中外層的 for 循環老是會迭代 n-1 次。

在最壞的狀況下,內部 for 循環將交換一次,而後交換兩次,依此類推。交換的數量將是 1 + 2 + ... + (n - 3) + (n - 2) + (n - 1),這使得插入排序具備 O(n^2) 的時間複雜度。

堆排序

這種流行的排序算法,像插入排序和選擇排序同樣,將列表分爲已排序部分和未排序部分。它將列表的未排序段轉換爲數據結構堆,以便咱們能有效地肯定最大的元素。

介紹

咱們首先將列表轉換成一個最大堆 —— 一種最大元素爲根節點的二叉樹。而後把咱們把這個節點放在列表的尾部。而後咱們重建這個少了一個值的最大堆,將新的最大值放在列表的最後一項以前。

而後咱們重複這個構建堆的過程,直到刪除全部節點。

實現

咱們建立一個輔助函數 heapify 來幫助實現這個算法:

def heapify(nums, heap_size, root_index):
    # 設最大元素索引爲根節點索引
    largest = root_index
    left_child = (2 * root_index) + 1
    right_child = (2 * root_index) + 2

    # 若是根節點的左子節點是有效索引,而且元素大於當前最大元素,則更新最大元素
    if left_child < heap_size and nums[left_child] > nums[largest]:
        largest = left_child

    # 對根節點的右子節點執行相同的操做
    if right_child < heap_size and nums[right_child] > nums[largest]:
        largest = right_child

    # 若是最大的元素再也不是根元素,則交換它們
    if largest != root_index:
        nums[root_index], nums[largest] = nums[largest], nums[root_index]
        # 調整堆以確保新的根節點元素是最大元素
        heapify(nums, heap_size, largest)

def heap_sort(nums):
    n = len(nums)

    # 利用列表建立一個最大堆
    # range 的第二個參數表示咱們將停在索引值爲 -1 的元素以前,即列表中的第一個元素
    # range 的第三個參數表示咱們朝反方向迭代
    # 將 i 的值減小1
    for i in range(n, -1, -1):
        heapify(nums, n, i)

    # 將最大堆的根元素移動到列表末尾
    for i in range(n - 1, 0, -1):
        nums[i], nums[0] = nums[0], nums[i]
        heapify(nums, i, 0)

# 驗證算法是否正確
random_list_of_nums = [35, 12, 43, 8, 51]
heap_sort(random_list_of_nums)
print(random_list_of_nums)
複製代碼

時間複雜度

讓咱們先看看 heapify 函數的時間複雜度。在最壞的狀況下,最大元素永遠不是根元素,這會致使遞歸調用 heapify 函數。雖然遞歸調用可能看起來很是損耗性能,但請記住,咱們這裏使用的是二叉樹。

可視化一個包含 3 個元素的二叉樹,它的高度爲 2。如今可視化一個包含 7 個元素的二叉樹,它的高度爲 3。這棵樹按對數方式增加到 nheapify 函數在 O(log(n)) 時間遍歷該樹。

heap_sort 函數迭代數組 n 次。所以,堆排序算法的總時間複雜度爲 O(nlog(n))。

歸併排序

這種分而治之的算法將一個列表分紅兩部分,並一直將剩下的列表分別一分爲二直到列表中只剩下一個元素爲止。

相鄰元素成爲排序對,而後合併排序對並和其它排序對進行排序。這個過程將一直持續到咱們獲得一個對未排序輸入列表中全部元素排序的排序列表爲止。

介紹

咱們遞歸地將列表分紅兩半,直到獲得長度爲 1 的列表。而後咱們合併被分割出的每一部分,在這個過程當中對它們進行排序。

排序是經過比較每一半的最小元素來完成的。每一個列表的第一個元素是第一個要比較的元素。若是前半部分以較小的值開頭,那麼咱們將其添加到排序列表中。而後咱們比較前半部分的第二個最小值和後半部分的第一個最小值。

每次咱們在半段的開頭選擇較小的值時,咱們都會移動須要比較的項目。

簡介

def merge(left_list, right_list):
    sorted_list = []
    left_list_index = right_list_index = 0

    # 咱們常用列表長度,所以將它建立爲變量方便使用
    left_list_length, right_list_length = len(left_list), len(right_list)

    for _ in range(left_list_length + right_list_length):
        if left_list_index < left_list_length and right_list_index < right_list_length:
            # 咱們檢查每一個列表開頭的哪一個值較小
            # 若是左列表開頭的項較小,將它添加到已排序列表
            if left_list[left_list_index] <= right_list[right_list_index]:
                sorted_list.append(left_list[left_list_index])
                left_list_index += 1
            # 若是右列表開頭的項較小,將它添加到已排序列表
            else:
                sorted_list.append(right_list[right_list_index])
                right_list_index += 1

        # 若是已到達左列表的末尾,則添加右列表中的元素
        elif left_list_index == left_list_length:
            sorted_list.append(right_list[right_list_index])
            right_list_index += 1
        # 若是已到達右列表的末尾,則添加左列表中的元素
        elif right_list_index == right_list_length:
            sorted_list.append(left_list[left_list_index])
            left_list_index += 1

    return sorted_list

def merge_sort(nums):
    # 若是列表中只有一個元素,則返回它
    if len(nums) <= 1:
        return nums

    # 使用向下取整獲取中點,索引必須是整數
    mid = len(nums) // 2

    # 對每一半進行排序和合並
    left_list = merge_sort(nums[:mid])
    right_list = merge_sort(nums[mid:])

    # 將已排序的列表合併爲新列表
    return merge(left_list, right_list)

# 驗證算法是否正確
random_list_of_nums = [120, 45, 68, 250, 176]
random_list_of_nums = merge_sort(random_list_of_nums)
print(random_list_of_nums)
複製代碼

請注意,merge_sort() 函數與之前的排序算法不一樣,它返回一個已排序的新列表,而不是對現有列表進行排序。

所以,歸併排序須要空間來建立和輸入列表大小相同的新列表。

時間複雜度

咱們首先看看 merge 函數。它須要兩個列表,並迭代 n 次,其中 n 是兩個列表合併後的大小。merge_sort 函數將給定數組拆分爲 2 個,並遞歸地對子數組進行排序。因爲遞歸的輸入是給定數組的一半,就像二叉樹同樣,這使得處理所需的時間以對數方式增加到 n

所以,歸併排序算法的整體時間複雜性是 O(nlog(n))。

快速排序

這種分而治之的算法是本文中最經常使用的排序算法。若是合理地使用,那麼它將具備很高的效率,而且不須要像歸併排序同樣使用額外的空間。咱們圍繞一個基準值對列表進行分區,並對基準值周圍的元素進行排序。

介紹

快速排序首先對列表進行分區 —— 選擇待排序列表的第一個值。該值被稱爲基準值。全部小於基準值的元素都將被移到其左側。

此時基準值在正確的位置,咱們遞歸地對基準值周圍的元素進行排序,直到整個列表有序。

實現

# 快速排序分區有不一樣的方法,下面實現了 Hoare 的分區方案。Tony Hoare 還建立了快速排序算法。
def partition(nums, low, high):
    # 咱們選擇中間元素做爲基準值。
    # 有些實現方法選擇第一個元素或最後一個元素做爲基準值。 
    # 有時將中間元素或一個隨機元素做爲基準值。
    # 還有不少能夠選擇或建立的方法。
    pivot = nums[(low + high) // 2]
    i = low - 1
    j = high + 1
    while True:
        i += 1
        while nums[i] < pivot:
            i += 1

        j -= 1
        while nums[j] > pivot:
            j -= 1

        if i >= j:
            return j

        # 若是 i 處的元素(在基準值左側)大於 j 處的元素(在基準值右側),則交換它們。
        nums[i], nums[j] = nums[j], nums[i]

def quick_sort(nums):
    # 建立一個輔助函數來進行遞歸調用
    def _quick_sort(items, low, high):
        if low < high:
            # 這是基準元素後的索引,咱們的列表在這裏被拆分
            split_index = partition(items, low, high)
            _quick_sort(items, low, split_index)
            _quick_sort(items, split_index + 1, high)

    _quick_sort(nums, 0, len(nums) - 1)

# 檢驗算法是否正確
random_list_of_nums = [22, 5, 1, 18, 99]
quick_sort(random_list_of_nums)
print(random_list_of_nums)
複製代碼

時間複雜度

最壞的狀況是始終選擇最小或最大元素做爲基準值。這將建立一個大小爲 n-1 的分區,致使遞歸調用 n-1 次。這致使在最壞狀況下的時間複雜度爲 O(n^2)。

雖然最壞的狀況比較糟糕,但快速排序仍然被大量使用,由於它的平均時間複雜度比其餘排序算法快得多。雖然 partition 函數使用嵌套的 while 循環,但它會對數組的全部元素進行比較以進行交換。所以,它的時間複雜度只有 O(n)。

若是選擇一個好的基準值,快速排序函數將把數組分紅兩部分,這兩部分將隨 n 呈對數增加。所以,快速排序算法的平均時間複雜度爲 O(nlog(n))。

Python 的內置排序函數

雖然理解這些排序算法是有益的,但在大多數 Python 項目中,你可能會使用語言中已經提供的排序函數。

咱們能夠更改列表,使其內容按 sort() 方法排序:

apples_eaten_a_day = [2, 1, 1, 3, 1, 2, 2]
apples_eaten_a_day.sort()
print(apples_eaten_a_day) # [1, 1, 1, 2, 2, 2, 3]
複製代碼

或者咱們可使用 sorted() 函數建立新的排序列表:

apples_eaten_a_day_2 = [2, 1, 1, 3, 1, 2, 2]
sorted_apples = sorted(apples_eaten_a_day_2)
print(sorted_apples) # [1, 1, 1, 2, 2, 2, 3]
複製代碼

它們都是按升序排序的,但你能夠經過將 reverse 標誌設置爲 True 來輕鬆按降序排序:

# 對列表進行反向排序
apples_eaten_a_day.sort(reverse=True)
print(apples_eaten_a_day) # [3, 2, 2, 2, 1, 1, 1]

# 反向排序以獲取新列表
sorted_apples_desc = sorted(apples_eaten_a_day_2, reverse=True)
print(sorted_apples_desc) # [3, 2, 2, 2, 1, 1, 1]
複製代碼

與咱們建立的排序算法函數不一樣,這兩個函數均可以對元組和類的列表進行排序。sorted() 函數能夠對任何可迭代對象進行排序,其中包括了 —— 你能夠建立的列表,字符串,元組,字典,集合,和自定義迭代器

這些排序函數實現了 Tim Sort 算法,這是一種受歸併排序和插入排序啓發的算法。

速度比較

爲了瞭解它們的執行速度,咱們生成了一個介於 0 到 1000 之間的 5000 個數字的列表。而後咱們計算每一個算法完成所需的時間。每一個算法運行 10 次,以便咱們創建更可靠的性能模型。

下面是結果,時間以秒爲單位:

Run 冒泡 選擇 插入 歸併 快速
1 5.53188 1.23152 1.60355 0.04006 0.02619 0.01639
2 4.92176 1.24728 1.59103 0.03999 0.02584 0.01661
3 4.91642 1.22440 1.59362 0.04407 0.02862 0.01646
4 5.15470 1.25053 1.63463 0.04128 0.02882 0.01860
5 4.95522 1.28987 1.61759 0.04515 0.03314 0.01885
6 5.04907 1.25466 1.62515 0.04257 0.02595 0.01628
7 5.05591 1.24911 1.61981 0.04028 0.02733 0.01760
8 5.08799 1.25808 1.62603 0.04264 0.02633 0.01705
9 5.03289 1.24915 1.61446 0.04302 0.03293 0.01762
10 5.14292 1.22021 1.57273 0.03966 0.02572 0.01606
平均 5.08488 1.24748 1.60986 0.04187 0.02809 0.01715

若是你本身進行測試,你會獲得不一樣的值,可是觀察到的性能模型應該是相同或類似的。冒泡排序是全部算法中執行速度最慢、表現最差的。雖然它做爲排序和算法的介紹頗有用,但不適合實際使用。

咱們還注意到快速排序很是快,它的速度幾乎是歸併排序的兩倍,並且它在運行時不須要額外的空間。回想一下,咱們的分區是基於列表的中間元素,不一樣的分區方法可能會有不一樣的結果。

因爲插入排序執行的比較要比選擇排序少得多,所以插入排序的實現一般更快,但在咱們的測試中,選擇排序會稍微快一些。

插入排序比選擇排序交換元素的次數更多。若是交換值比比較值佔用更多的時間,那麼這個「相反」的結果是可信的。

選擇排序算法時要注意使用場景,由於它會影響性能。

總結

排序算法爲咱們提供了許多排序數據的方法。咱們研究了 6 種不一樣的算法——冒泡排序、選擇排序、插入排序、歸併排序、堆排序、快速排序 —— 以及它們在 Python 中的實現。。

算法執行的比較和交換量以及代碼運行的環境是決定性能的關鍵因素。在實際的 Python 應用程序中,建議咱們堅持使用內置的 Python 排序函數,由於它們在輸入和速度上具備靈活性。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索