1. 程式人生 > >資料結構(一):幾種常見排序演算法比較

資料結構(一):幾種常見排序演算法比較

排序

0. 常見排序演算法效率比較

時間複雜度及穩定性比較

排序方法 平均方法 最優複雜度 最壞複雜度 輔助空間 穩定性
氣泡排序 O( n 2
n^2
)
O( n 2 n^2 ) O( n
2 n^2
)
O(1) 穩定
選擇排序 O( n 2
n^2
)
O( n 2 n^2 ) O( n 2 n^2 ) O(1) 不穩定
插入排序 O( n 2 n^2 ) O( n 2 n^2 ) O( n 2 n^2 ) O(1) 穩定
希爾排序 O(nlogn)~O( n 2 n^2 ) O( n 1.3 n^{1.3} ) O( n 2 n^2 ) O(1) 不穩定
快速排序 O(nlogn) O(nlogn) O( n 2 n^2 ) O(nlogn)~O(n) 不穩定
歸併排序 O(nlogn) O(nlogn) O(nlogn) O(n) 穩定

綜上,歸併的排序方法最為合理

1. 氣泡排序

氣泡排序(Bubble Sort)是一種簡單的排序演算法。它重複地遍歷要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。遍歷數列的工作是重複地進行直到沒有再需要交換,即該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。

氣泡排序演算法步驟:

1. 比較相鄰的元素。如果第一個比第二個大(升序),就交換他們兩個。
2. 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
3. 針對所有的元素重複以上的步驟,除了最後一個。
4. 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

在這裡插入圖片描述

例項:

# coding=utf-8
def bubble_sort(alist):
    """氣泡排序"""
    # 時間複雜度
    # 最優時間複雜度:O(n) (表示遍歷一次發現沒有任何可以交換的元素,排序結束。)
    # 最壞時間複雜度:O(n^2)
    # 穩定性:穩定

    for j in range(len(alist) - 1, 0, -1):
        for i in range(j):
            if alist[i] > alist[i + 1]:
                alist[i], alist[i + 1] = alist[i + 1], alist[i]


li = [34, 26, 83, 27, 77, 31, 54, 35, 12]
print(li)
bubble_sort(li)
print(li)


# 結果:
# [34, 26, 83, 27, 77, 31, 54, 35, 12]
# [12, 26, 27, 31, 34, 35, 54, 77, 83]

2. 選擇排序

選擇排序(Selection sort)是一種簡單直觀的排序演算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

選擇排序的主要優點與資料移動有關。如果某個元素位於正確的最終位置上,則它不會被移動。選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,因此對n個元素的表進行排序總共進行至多n-1次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬於非常好的一種。

選擇排序步驟:

1. 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
2. 再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾;
3. 重複步驟2,直至所有元素均排序完成。

在這裡插入圖片描述

例項:

# coding=utf-8
def select_sort(alist):
    """選擇排序"""
    # 時間複雜度
    # 最優時間複雜度:O(n^2)
    # 最壞時間複雜度:O(n^2)
    # 穩定性:不穩定(考慮升序每次選擇最大的情況)

    n = len(alist)
    for j in range(n - 1):
        min_index = j
        for i in range(j + 1, n):
            if alist[i] < alist[min_index]:
                min_index = i
        if j != min_index:
            alist[j], alist[min_index] = alist[min_index], alist[j]


if __name__ == '__main__':
    li = [34, 26, 83, 27, 77, 31, 54, 35, 12]
    print(li)
    select_sort(li)
    print(li)

# 結果:
# [34, 26, 83, 27, 77, 31, 54, 35, 12]
# [12, 26, 27, 31, 34, 35, 54, 77, 83]

3. 插入排序

插入排序(Insertion Sort)是一種簡單直觀的排序演算法。它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。

插入排序步驟:

1. 選擇未排序的首個數據,在已排序序列中從後向前掃描,找到相應位置並插入;
2. 插入排序時,從後向前掃描過程中,反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。

在這裡插入圖片描述

例項:

# coding=utf-8
def insert_sort(alist):
    """插入排序"""
    # 時間複雜度
    # 最優時間複雜度:O(n) (升序排列,序列已經處於升序狀態)
    # 最壞時間複雜度:O(n^2)
    # 穩定性:穩定

    n = len(alist)
    for j in range(1, n):
        for i in range(j, 0, -1):
            if alist[i] < alist[i - 1]:
                alist[i], alist[i - 1] = alist[i - 1], alist[i]
            else:
                break


if __name__ == '__main__':
    li = [34, 26, 83, 27, 77, 31, 54, 35, 12]
    print(li)
    insert_sort(li)
    print(li)

# 結果:
# [34, 26, 83, 27, 77, 31, 54, 35, 12]
# [12, 26, 27, 31, 34, 35, 54, 77, 83]

4. 希爾排序

希爾排序(Shell Sort)是插入排序的一種。也稱縮小增量排序,是直接插入排序演算法的一種更高效的改進版本。希爾排序是非穩定排序演算法。該方法因DL.Shell於1959年提出而得名。 希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序演算法排序;隨著增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個檔案恰被分成一組,演算法便終止。

希爾排序步驟:

1. 將陣列列在一個表中並對其以步長為陣列長度的一半(len(list)//2)分組;
2. 對每組使用直接插入排序演算法排序;
3. 重複這過程,不過每次用更長的組(步長更長了,組數更少了)來進行。最後整個表就只有一組了;
4. 當增量減至1時,整個檔案恰被分成一組,演算法便終止。

在這裡插入圖片描述

例項:

# coding=utf-8
def shell_sort(alist):
    """希爾排序"""
    # 時間複雜度
    # 最優時間複雜度:根據步長序列的不同而不同
    # 最壞時間複雜度:O(n^2)
    # 穩定性:不穩定

    n = len(alist)
    # 初始步長
    gap = n // 2
    while gap > 0:
        # 按步長進行插入排序
        for i in range(gap, n):
            while i >= gap:
                if alist[i] < alist[i - gap]:
                    alist[i], alist[i - gap] = alist[i - gap], alist[i]
                    i -= gap
                else:
                    break

        gap = gap // 2


if __name__ == '__main__':
    li = [34, 26, 83, 27, 77, 31, 54, 35, 12]
    print(li)
    shell_sort(li)
    print(li)

# 結果:
# [34, 26, 83, 27, 77, 31, 54, 35, 12]
# [12, 26, 27, 31, 34, 35, 54, 77, 83]

5. 快速排序

快速排序(Quick sort),又稱劃分交換排序(partition-exchange sort),通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。

快速排序步驟為:

1. 從數列中挑出一個元素,稱為"基準"(pivot),
2. 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分割槽結束之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作。
3. 遞迴地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。

注意:遞迴的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞迴下去,但是這個演算法總會結束,因為在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。

在這裡插入圖片描述

例項:

# coding=utf-8
def quick_sort(alist, start, end):
    """快速排序"""
    # 時間複雜度
    # 最優時間複雜度:O(nlogn)
    # 最壞時間複雜度:O(n^2)
    # 穩定性:不穩定

    # 遞迴的退出條件
    if start >= end:
        return
    # 設定起始元素為要尋找位置的基準元素mid
    mid = alist[start]
    # left 為序列左邊的由左向右移動的遊標
    left = start
    # right 為序列右邊向左移動的遊標
    right = end
    while left < right:
        # 如果left與right未重合,right指向的元素不比基準元素小,則right向左移動
        while left < right and alist[right] >= mid:
            right -= 1
        alist[left] = alist[right]
        # 如果left與right未重合,left指向的元素比基準小,則left向右移動
        while left < right and alist[left] < mid:
            left += 1
        # 將left指向的元素放到right的位置
        alist[right] = alist[left]
    # 從迴圈退出後,left與right相遇,即left==right,此時所指的
    alist[left] = mid

    # 對基準元素左邊的子序列進行快速排序
    quick_sort(alist, start, left - 1)
    # 對基準元素右邊的子序列進行快速排序
    quick_sort(alist, left + 1, end)


if __name__ == '__main__':
    li = [34, 26, 83, 27, 77, 31, 54, 35, 12]
    print(li)
    quick_sort(li, 0, len(li) - 1)
    print(li)

# 結果:
# [34, 26, 83, 27, 77, 31, 54, 35, 12]
# [12, 26, 27, 31, 34, 35, 54, 77, 83]

6. 歸併排序

歸併排序(merge sort)是採用分治法的一個非常典型的應用。歸併排序的思想就是先遞迴分解陣列,再合併陣列。

將陣列分解最小之後,然後合併兩個有序陣列,基本思路是比較兩個陣列的最前面的數,誰小就先取誰,取了後相應的指標就往後移一位。然後再比較,直至一個數組為空,最後把另一個數組的剩餘部分複製過來即可。

歸併排序步驟:

1. 將陣列分解最小;合併兩個有序陣列
2. 比較兩個陣列的最前面的數,誰小就先取誰,取了後相應的指標就往後移一位;
3. 迴圈再比較,直至一個數組為空;
4. 最後把另一個數組的剩餘部分複製過來即可。

在這裡插入圖片描述

例項:

# coding=utf-8
def merge_sort(alist):
    """歸併排序"""
    # 時間複雜度
    # 最優時間複雜度:O(nlogn)
    # 最壞時間複雜度:O(nlogn)
    # 穩定性:穩定

    n = len(alist)
    if 1 == n:
        return alist
    # 二分分解
    mid = n // 2

    # 對左半部分進行歸併排序
    left_sorted_li = merge_sort(alist[:mid])
    right_sorted_li = merge_sort(alist[mid:])

    # 合併兩個有序陣列,並返回
    return merge(left_sorted_li, right_sorted_li)


def merge(left_sorted_li, right_sorted_li):
    """合併兩個有序陣列,將兩個有序陣列left[]和right[]合併成一個大的有序陣列"""
    left_n = len(left_sorted_li)
    right_n = len(right_sorted_li)
    left, right = 0, 0
    merge_sort_li = []
    while left < left_n and right < right_n:
        if left_sorted_li[left] <= right_sorted_li[right]:
            merge_sort_li.append(left_sorted_li[left])
            left += 1
        else:
            merge_sort_li.append(right_sorted_li[right])
            right += 1
    merge_sort_li += left_sorted_li[left:]
    merge_sort_li += right_sorted_li[right:]
    return merge_sort_li


if __name__ == '__main__':
    li = [34, 26, 83, 27, 77, 31, 54, 35, 12]
    print(li)
    sorted_li = merge_sort(li)
    print(sorted_li)


# 結果:
# [54, 26, 93, 17, 77, 31, 44, 55, 20]
# [17, 20, 26, 31, 44, 54, 55, 77, 93]