1. 程式人生 > 實用技巧 >十大排序演算法實現(python)

十大排序演算法實現(python)

目錄

1.氣泡排序

1.描述

  • 重複重複地走訪過要排序的數列,比較相鄰元素的大小,把大的元素換到後面,最大元素先浮出來,再比較剩餘需要排序數列,同樣的方法找出最大元素,直到沒有序列需要再排序

2.程式碼

def bubbleSort(arr):
    n = len(arr)
    # 遍歷所有陣列元素
    for i in range(n):
        # Last i elements are already in place
        for j in range(n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr
  
arr = [64, 34, 25, 12, 22, 11, 90]
print(bubbleSort(arr))

3.優化版本

  • 一次迴圈下來發現一次交換也沒有,說明剩下的氣泡已經按順序排列了,就不需要再迴圈了
def bubbleSort(arr):
    n = len(arr)
    # 遍歷所有陣列元素
    for i in range(n):
        indicator = False  # 用於優化(沒有交換時表示已經有序,結束迴圈)
        # Last i elements are already in place
        for j in range(0, n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                indicator = True
        if not indicator:  # 如果沒有交換說明列表已經有序,結束迴圈
            break
    return arr
arr = [64, 34, 25, 12, 22, 11, 90]
print(bubbleSort(arr))#[11, 12, 22, 25, 34, 64, 90]

4.時間複雜度和穩定性分析

  1. 時間複雜度
  • 平均情況的的時間複雜度為\(O(n^2)\)

    • 外層迴圈排序執行 N - 1次。內層迴圈相鄰元素比較最多的時候執行N次,最少的時候執行1次,平均執行 \(\frac{N+1}{2}\)次。所以迴圈體內的比較交換約執行 \(\frac{(N - 1)(N + 1)}{2} = \frac{N^2 - 1}{2}\),按照計算複雜度的原則,去掉常數,去掉最高項係數,其複雜度為\(O(N^2)\)
  • 最好的時間複雜度為\(O(n)\)

  • 檔案的初始狀態是正序的,一趟掃描即可完成排序。所需的關鍵字比較次數\(C\)和記錄移動次數\(M\)均達到最小值:\(C~min~=n-1\), \(M~min~=0\)

  • 最壞的時間複雜度為\(O(N^2)\)

    • 若初始檔案是反序的,需要進行\(n-1\)趟排序。每趟排序要進行\(n-i\)次關鍵字的比較(1≤i≤n-1),且每次比較都必須移動記錄三次來達到交換記錄位置。在這種情況下,比較和移動次數均達到最大值:

    氣泡排序的最壞時間複雜度為\(O(n^2)\)

  1. 演算法穩定性(穩定)

    氣泡排序就是把小的元素往前調或者把大的元素往後調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。所以,如果兩個元素相等,是不會再交換的;如果兩個相等的元素沒有相鄰,那麼即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前後順序並沒有改變,所以氣泡排序是一種穩定排序演算法。

2.選擇排序

1.描述

  • 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序序列中繼續尋找最小(大)元素,通過將最小元素和未排序序列第一個元素交換位置,將未排序序列最小元素放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

2.程式碼

# selection_sort.py
def selection_sort(arr):
    count = len(arr)
    for i in range(count - 1):  # 交換 n-1 次
        #在未排序序列中找到最小元素
        min = i
        # 從剩餘未排序元素中繼續尋找最小元素
        for j in range(i+1, count):
            if arr[min] > arr[j]:
                min = j
        arr[min], arr[i] = arr[i], arr[min]  # 交換,將最小的放到本輪的最前面
    return arr

my_list = [6, 23, 2, 54, 12, 6, 8, 100]
print(selection_sort(my_list))#[2, 6, 6, 8, 12, 23, 54, 100]

3.優化

  • 從每次找出一個,優化為每次找出2個,找最小元素和最大元素兩個,再從剩餘未排序元素中繼續尋找最小元素,然後放到已排序序列的末尾,從剩餘未排序元素中繼續尋找最大元素,然後放到已排序序列的開頭,因為每次都比較兩個,所以每次左右都要少比較一個
# selection_sort.py
def selection_sort(arr):
    count = len(arr)
    for i in range(count-1):  # 交換 n-1 次
        #在未排序序列中找到最小元素
        min = i
        max=count-1
        count-=1#每次左右都要少比較一個末尾的
        # 從剩餘未排序元素中繼續尋找最小元素
        for j in range(i+1, count):
            if arr[min] > arr[j]:
                min = j
            if arr[max]<arr[j]:
                max=j
        arr[min], arr[i] = arr[i], arr[min]  # 交換,將最小的放到本輪的最前面
        arr[max],arr[count]=arr[count],arr[max]## 交換,將最大的放到本輪的最後面
    return arr

my_list = [6, 23, 2, 54, 12, 6, 8, 100]
print(selection_sort(my_list))#[2, 6, 6, 8, 12, 23, 54, 100]

4.時間複雜度和穩定性分析

  1. 時間複雜度
  • 平均情況的的時間複雜度為\(O(n^2)\)

    • 第一次內迴圈比較N - 1次,然後是N-2次,N-3次,……,最後一次內迴圈比較1次。
      共比較的次數是 (N - 1) + (N - 2) + ... + 1。所以迴圈體內的比較交換約執行 \(\frac{(N - 1+1)N}{2} = \frac{N^2}{2}\),按照計算複雜度的原則,去掉常數,去掉最高項係數,其複雜度為\(O(N^2)\)
    • 雖然選擇排序和氣泡排序的時間複雜度一樣,但實際上,選擇排序進行的交換操作很少,最多會發生 N - 1次交換。而氣泡排序最壞的情況下要發生\(\frac{N^2}{2}\)交換操作。這個意義上講,交換排序的效能略優於氣泡排序
  • 最好的最壞的時間複雜度都為\(O(N^2)\)

    • 檔案的初始狀態是正序的,或是逆序的,都需要對未排序序列比較每個元素找到最小值排到已排序序列後
  1. 演算法穩定性(不穩定)
  • 在一趟選擇中,如果當前元素比一個元素大,而該小的元素又出現在一個和當前元素相等的元素後面,那麼交換後穩定性就被破壞了。舉個例子,序列5 8 5 2 9,我們知道第一遍選擇第1個元素5會和2交換,那麼原序列中2個5的相對前後順序就被破壞了,在前面的5變成了後面,所以選擇排序不是一個穩定的排序演算法。

3.插入排序

1.描述

  • 構建有序序列,對於未排序資料,排序過程中每次從無序表中取出第一個元素,在已排序序列中從後向前掃描,該元素比排序序列的當前元素大,則將排序序列的當前元素後移,將該元素依次往前比較,直到找到比它小或等於它的元素插入在其後面

2.程式碼

def insertion_sort(arr):
    # 第一層for表示迴圈插入的遍數
    for i in range(1,len(arr)):#預設第一個是已經排好序的
        # 設定當前需要插入的元素
        current=arr[i]
        j=i-1
        while j>=0 and current<arr[j]:
            # 當比較元素大於當前元素則把比較元素後移
            arr[j+1]=arr[j]
            # 往前選擇下一個比較元素
            j-=1
        # 當比較元素小於當前元素,則將當前元素插入在 其後面
        arr[j+1]=current
    return arr

print(insertion_sort([12, 11, 13, 5, 6]))#[5, 6, 11, 12, 13]

3.優化

  • 插入排序的方法需要每次將待插入元素與已排序序列中的每個元素作比較,基於這點特性,可以採用二分查詢法來減少比較操作的次數,待插入的值與已排序區域的中間值比較,不斷縮小區域範圍,直到left和right相遇,當left和right相遇的時候,待插入的位置其實就是left的位置,此時要將left到有序序列的最後一個元素都向後移動一個位置,才能插入元素。
def insertion_sort(arr):
    # 第一層for表示迴圈插入的遍數
    for i in range(1,len(arr)):
        # 設定當前需要插入的元素
        current=arr[i]
        left=0
        right=i-1

        #待插入的值與已排序區域的中間值比較,不斷縮小區域範圍,直到left和right相遇。
        while left<=right:
            mid=(right+left)//2
            if current>arr[mid]:
                left=mid+1
            else:
                right=mid-1

        # 當left和right相遇的時候,待插入的位置其實就是left的位置,此時要將left到有序序列的最後一個元素都向後移動一個位置,才能插入元素。
        for j in range(i-1,left-1,-1):
            arr[j+1]=arr[j]

        # 插入元素
        if left!=i:
            arr[left]=current
    return arr

print(insertion_sort([12, 11, 13, 5, 6]))#[5, 6, 11, 12, 13]

4.時間複雜度和穩定性分析

  1. 時間複雜度
  • 平均情況的的時間複雜度為\(O(n^2)\)

    • 最好和最壞的平均\(\frac{\frac{N^2-N}{2}+N-1}{2}=\frac{N^2}{4}+\frac{N}{2}-\frac{1}{2}\)
  • 最好的時間複雜度為\(O(n)\)

    • 檔案的初始狀態是正序的,只需要遍歷需要排序的元素n-1
  • 最壞的時間複雜度為\(O(N^2)\)

    • 若初始檔案是反序的,未排序元素與排序序列迴圈比較N - 1次,然後是N-2次,N-3次,……,最後一次內迴圈比較1次。共比較的次數是 (N - 1) + (N - 2) + ... + 1。所以迴圈體內的比較交換約執行 \(\frac{(N - 1+1)(N-1)}{2} = \frac{N^2-N}{2}\),按照計算複雜度的原則,去掉常數,去掉最高項係數,其複雜度為\(O(N^2)\)
  1. 演算法穩定性(穩定)

    比較是從有序序列的末尾開始,也就是把待插入的元素和已經有序的最大者開始比起,如果比它大則直接插入在其後面。否則一直往前找直到找到它該插入的位置。如果遇見一個與插入元素相等的,那麼把待插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序仍是排好序後的順序,所以插入排序是穩定的。

4.快速排序

1.描述

  • 從數列中挑出一個元素,稱為”基準",把一個序列(list)分為較小和較大的2個子序列,用兩個指標,一個從右往左找比基準小的元素,放到基準前,一個從左往右找比基準小的元素,放到基準後,使得基準前序列元素都比基準後序列元素小,然後遞迴地排序兩個子序列,以此達到整個資料變成有序序列

2.程式碼

def quick_sort(alist, start, end):
    """快速排序"""
    if start >= end:  # 遞迴的退出條件
        return
    mid = alist[start]  # 設定起始的基準元素
    low = start  # low為序列左邊在開始位置的由左向右移動的遊標
    high = end  # high為序列右邊末尾位置的由右向左移動的遊標
    while low < high:
        # 如果low與high未重合,high(右邊)指向的元素大於等於基準元素,則high向左移動
        while low < high and alist[high] >= mid:
            high -= 1
        alist[low] = alist[high]  # 走到此位置時high指向一個比基準元素小的元素,將high指向的元素放到low的位置上,此時high指向的位置空著,接下來移動low找到符合條件的元素放在此處
        # 如果low與high未重合,low指向的元素比基準元素小,則low向右移動
        while low < high and alist[low] < mid:
            low += 1
        alist[high] = alist[low]  # 此時low指向一個比基準元素大的元素,將low指向的元素放到high空著的位置上,此時low指向的位置空著,之後進行下一次迴圈,將high找到符合條件的元素填到此處

    # 退出迴圈後,low與high重合,此時所指位置為基準元素的正確位置,左邊的元素都比基準元素小,右邊的元素都比基準元素大
    alist[low] = mid  # 將基準元素放到該位置,
    # 對基準元素左邊的子序列進行快速排序
    quick_sort(alist, start, low - 1)  # start :0  low -1 原基準元素靠左邊一位
    # 對基準元素右邊的子序列進行快速排序
    quick_sort(alist, low + 1, end)  # low+1 : 原基準元素靠右一位  end: 最後

if __name__ == '__main__':
    alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    quick_sort(alist, 0, len(alist) - 1)
    print(alist)#[17, 20, 26, 31, 44, 54, 55, 77, 93]

3.優化

  1. 解決陣列近乎有序的情況下演算法複雜度會退化為O(n2)級別的問題可以通過隨機選擇待處理元素的方法來避免

  2. 三路快排,通過明確劃分出相等元素,解決重複元素造成劃分不平衡的問題,劃分的三組為大於array[left],小於array[left],等於array[left]

    • 每輪遞迴中,對於當前元素i,如果小於目標,放到左邊的lest_group,如果大於目標,放到右邊的greater_group,如果等於目標,放到中間。之後對兩邊的大小分組繼續遞迴,直到排序完成。
import random
def quick_sort(alist, start, end):
    """快速排序"""
    if start<end:
        random_index=random.randint(start,end)
        alist[start],alist[random_index]=alist[random_index],alist[start]
        pivot = alist[start]  # 設定起始的基準元素
        low = start  # low為序列左邊在開始位置
        high = end+1  # high為序列右邊末尾位置
        i=start+1 #當前元素
        while i < high:
            if alist[i]<pivot:
                #當前元素小於基準,當前元素換到序列左邊
                alist[i],alist[low+1]=alist[low+1],alist[i]
                low+=1
                i+=1
            elif alist[i] > pivot:
                # 當前元素小於基準,當前元素換到序列右邊
                alist[i], alist[high - 1] = alist[high - 1], alist[i]
                high-= 1
                #序列右邊元素不確定比基準元素小還是大,當前座標不前進,繼續比較
            else:
                i+=1
        #遍歷完low陣列前都是比基準數小的,放置基準數
        alist[start],alist[low]=alist[low],alist[start]
        # 對基準元素左邊的子序列進行快速排序
        quick_sort(alist, start, low - 1)  # start :0  low -1 原基準元素靠左邊一位
        # 對基準元素右邊的子序列進行快速排序
        quick_sort(alist, low, end)  # low+1 : 原基準元素靠右一位  end: 最後

if __name__ == '__main__':
    alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    quick_sort(alist, 0, len(alist) - 1)
    print(alist)#[17, 20, 26, 31, 44, 54, 55, 77, 93]

4.時間複雜度和穩定性分析

  1. 時間複雜度
  • 平均情況的的時間複雜度為\(O(nlog(n))\)

  • 最好的時間複雜度為\(O(nlog(n))\)

  • 快排本身 實際上這就是一個遞迴求解的多叉樹,什麼時候把所有葉子分清楚了就結束了,每一層具體的時間複雜度只可能等於n,時間複雜度完全取決於樹高,樹高則取決於你的左右子樹每層的工作量,比基準數大和小的數相等時最快,樹高為nlog(n)

  • 最壞的時間複雜度為\(O(N^2)\)

    • 只有比基準數大或者小的數,退化成連結串列,第一次遍歷n個數找大小,再遞迴遍歷n-1個數,n-2個數,約執行 \(\frac{(N - 1+1)N}{2} = \frac{N^2}{2}\)
  • 就地快速排序使用的空間是O(1)的,也就是個常數級;而真正消耗空間的就是遞迴呼叫了,因為每次遞迴就要保持一些資料;

    最優的情況下空間複雜度為:O(logn) ;每一次都平分陣列的情況

    最差的情況下空間複雜度為:O( n ) ;退化為氣泡排序的情況

  1. 演算法穩定性(不穩定)

    有相等的數在基準數後,且比基準數小,在後面的相等數會因為比基準數小先放到基準數前,相等數的前後順序不一樣了

5.堆排序

1.描述

  • 堆分為大頂堆和小頂堆,滿足\(Key[i]>=Key[2i+1]且key>=key[2i+2]\)稱為大頂堆,滿足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]稱為小頂堆
  • 構建大頂堆
    • 對於每一個節點而言,他的左孩子節點編號時2∗(