1. 程式人生 > >Python-排序-氣泡排序-優化

Python-排序-氣泡排序-優化

這是我通過極客專欄《資料結構與演算法之美》學習後的思考,分享一下,希望對你有所幫助。上一篇文章 工作後,為什麼還要學習資料結構與演算法 的思維導圖展現了這個專欄的內容。

說到演算法中的排序,氣泡排序是最簡單的一種排序演算法了,甚至不學資料結構與演算法的同學都會使用它。但是你有沒有想過可以怎麼優化?

什麼是氣泡排序:就像水慢慢燒開,氣泡從下往上越來越大那樣,第一次迴圈都把n個元素中最大的元素移動至最後位置,第二次從前 n-1 個位置中找出最大元素放在最後,重複執行,直到最後結果全部有序。

最基本的演算法實現,無優化版:

  def bubble_sort
(collection): """ 無任何優化版 """ compare_count=0 length = len(collection) for i in range(length-1): print(collection) #方便檢視陣列的排序過程 for j in range(length-1-i): compare_count+=1 if collection[j] > collection[j+1]: tmp = collection[
j] collection[j] = collection[j+1] collection[j+1] = tmp print(f"總迴圈次數{compare_count}") return collection

下面來執行一下,看看執行的過程,及總迴圈次數:

    print("bubble_sort begin.")
    unsorted = [3,4,2,1,5,6,7,8]
    print("bubble_sort end: ",*bubble_sort(unsorted))

執行結果如下:

bubble_sort begin.
[3, 4, 2, 1, 5, 6, 7, 8]
[3, 2, 1, 4, 5, 6, 7, 8]
[2, 1, 3, 4, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8]
總迴圈次數28
bubble_sort end:  1 2 3 4 5 6 7 8

通過排序的過程可以發現,在第 4 次冒泡時,資料已經有序,因此可以加入判斷,如果本次迴圈沒有冒泡(交換),說明資料已經有序,可以直接退出,優化後的程式碼如下:

優化一

def bubble_sort2(collection):
    """
    如果沒有元素交換,說明資料在排序過程中已經有序,直接退出迴圈
    """
    compare_count=0
    length = len(collection)
    for i in range(length-1):
        swapped = False
        print(collection)
        for j in range(length-1-i):
            compare_count+=1
            if collection[j] > collection[j+1]:
                swapped = True
                tmp = collection[j]
                collection[j] = collection[j+1]
                collection[j+1] = tmp
        if not swapped: break  # Stop iteration if the collection is sorted.
    print(f"總迴圈次數{compare_count}")
    return collection

下面來執行一下,看看執行的過程,及總迴圈次數:

    print("bubble_sort2 begin.")
    unsorted = [3,4,2,1,5,6,7,8]
    print("bubble_sort2 end:",*bubble_sort2(unsorted))

執行結果如下:

bubble_sort2 begin.
[3, 4, 2, 1, 5, 6, 7, 8]
[3, 2, 1, 4, 5, 6, 7, 8]
[2, 1, 3, 4, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8]
總迴圈次數22
bubble_sort2 end: 1 2 3 4 5 6 7 8

至此,還有沒有其他優化方法呢? 聰明的你可能看到了,總迴圈次數是比較多的,僅比未優化版少了 6 次迴圈次數。有沒有辦法減少總迴圈次數呢?

觀察資料可以發現,資料已經初始有序,可以分為兩部分,無序部分 3 4 2 1 和有序部分 5 6 7 8 ,每次迴圈如果能夠發現無序和有序的邊界,然後下次冒泡僅對無序部分進行比較和冒泡,可大大減少比較次數(迴圈次數),從而加快速度。

問題是,怎麼發現這個邊界呢?
第一次冒泡的過程中,第一個元素 4 被移動到下標為【3】的位置(python 列表索引從 0 開始),位置 【3】就是有序部分的開始位置。

第二次冒泡的過程中,第一個元素 3 被移動到下標為【2】的位置(python 列表索引從 0 開始),位置 【2】就是有序部分的開始位置。

可以推斷出,一次冒泡的過程中,最後一個被交換的元素下標即為無序和有序的邊界,因而下次冒泡,僅對 0 ~ 邊界 的元素冒泡即可大大減少迴圈次數。

優化二:

def bubble_sort3(collection):
    """
    bubble_sort2的基礎上再優化。
    優化思路:在排序的過程中,資料可以從中間分為兩段,一段是無序狀態,另一段是有序狀態。
    每一次迴圈的過程中,記錄最後一個交換元素的公交車,它便是有序和無序狀態的邊界
    下一次僅迴圈到邊界即可,從而減少迴圈次數,達到優化。
    """
    compare_count=0
    length = len(collection)
    last_change_index = 0 #最後一個交換的位置
    border = length-1 #有序和無序的分界線
    for i in range(length-1):
        swapped = False
        print(collection)
        
        for j in range(0,border):
            compare_count+=1
            if collection[j] > collection[j+1]:
                swapped = True
                collection[j], collection[j+1] = collection[j+1], collection[j]
                last_change_index = j
        if not swapped: break  # Stop iteration if the collection is sorted.

        border = last_change_index # 最後一個交換的位置就是邊界

    print(f"總迴圈次數{compare_count}")
    return collection

下面來執行一下,看看執行的過程,及總迴圈次數:

    print("bubble_sort3 begin.")
    unsorted = [3,4,2,1,5,6,7,8]
    print("bubble_sort3 end:",*bubble_sort3(unsorted))

執行結果如下:

bubble_sort3 begin.
[3, 4, 2, 1, 5, 6, 7, 8]
[3, 2, 1, 4, 5, 6, 7, 8]
[2, 1, 3, 4, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8]
總迴圈次數10
bubble_sort3 end: 1 2 3 4 5 6 7 8

可以看到結果的總迴圈次數為 10 ,與第二版相比,迴圈次數減少了一倍。

氣泡排序演算法的效能分析:

1、執行效率

最小時間複雜度:很好計算,最好的情況就是資料一開始就是有序的,因此一次冒泡即可完成,時間複雜度為 O(n)

最大時間複雜度:也很好計算,最壞的情況就是資料一開始就是倒序的,因此進行 n-1 次冒泡即可完成,時間複雜度為 O(n^2)

平均時間複雜度,嚴格來說平均時間複雜度就是加權平均期望時間複雜度,分析的時候要結合概率認的知識,對於包含 n 個數據的陣列,有 n! 種排序方式,不同的排列方式,氣泡排序的執行時間肯定是不同的,如果要用概率認的方法定量分析平均時間複雜度,涉及的資料推理會很複雜,這裡有一種思路,通過有序度逆序度這兩個概念來分析。有序度就是有順序的元素的個數,比如 3,1 ,2 這三個資料有有序度為1 即 (1,2) 一個,相反,逆序度為 2,即(3,2)(3,1)這兩個, 1, 2, 3 這三個資料的有序度為 3:(1,2)(1,3)(2,3),逆序度為 0,完全有序的資料序列的有序度也叫滿有序度。
有個公式

逆序度 = 滿有序度 - 有序度

排序的過程就是增加有序度,減少逆序度,最後達到滿有序度,說明排序完成。逆序度也主濁元素的交換次數,最壞情況,初始狀態的有序度為 0 ,逆序度為 n*(n-1)/2 , 所以要進行 n*(n-1)/2 次交換操作,最好情況,補充狀態完全有序,逆序度為 0 不需要進行交換,這裡平均交換次數我們可以取個平均值即 n*(n-1)/4。

而比較次數肯定比交換次數要多,因而平均情況下,無論演算法怎麼優化,時間複雜度不會低於 n*(n-1)/4,也就是 O(n^2)。

2、記憶體消耗

演算法的記憶體消耗可以通過空間複雜度來衡量,氣泡排序僅需要一個變數
tmp 來儲存交換的資料,因此空間複雜度為 O(1),空間複雜度為 O(1) 的排序演算法,也叫 原地排序演算法

3、排序演算法的穩定性

針對排序演算法,有一個重要的衡量指標,就是穩定性,這個概念是說,如果待排序的序列中存在值相等的元素,經過排序之後,相等元素之間原有的先後順序不變。假如有序列 4,1,2,2,我們把第一個 2 叫 2’,第二個2 叫 2’’,如果排序之後,為1,2’,2’’,4 那麼這個排序演算法就是穩定的,否則就是不穩定的。穩不穩定有什麼用嗎,值都是一樣的?當然有用,因為在軟體開發中,要排序的資料不單單是一個屬性的資料,而是有多個屬性的物件,假如對訂單排序,要求金額排序,訂單金額相同的情況下,按時間排序。最先想到的方法就是先對金額排序,在金額相同的訂單區間內按時間排序,理解起來不難,有沒有想過,實現起來很複雜。

但是藉助穩定的排序演算法,就很簡單了,先按訂單時間排一次序,再按金額排一次序就可以了。

小結

對排序演算法的分析無外乎時間複雜度(最好,最壞,平均),空間複雜度,穩定性這些方面,只要理解其思路,弄明白其適用場景,不需要死記。優化思路可以通過觀察分析得出,還有一點,氣泡排序雖然使用了陣列儲存資料但是並沒有使用陣列隨機訪問的特性,因此改用連結串列這種儲存結構,使用氣泡排序仍然是可以實現的,你可以嘗試下。

關注個人微信公眾號 somenzz 與你一起學習。
0