排序算法的python實現

感謝visualgo
排序算法我在之前這篇文章講過一遍了,但是實現我忘了,而且那篇文章也沒有加入代碼
我重新整理下吧

排序算法

排序問題有各種有趣的算法解決方案,體現了許多計算機科學的想法:

  • 比較與非比較的策略,
  • 迭代與遞歸實現,
  • 分歧和征服範式(這個或這個),
  • 最佳/最差/平均時間複雜度分析,
  • 隨機算法等

當數組被排序時,涉及其內容的許多問題變得容易(或更容易),例如

  • 搜索數組中的特定值v - 二進制搜索,
  • 在(靜態)數組中找到最小/最大/第k個最小/最大值,
  • 測試唯一性和刪除重複,
  • 計算特定值v在陣列中出現多少時間,
  • 在兩個排序的數組A和B之間設置交集/聯合,
  • 尋找目標對x和y使得x + y等於目標z等。

前六個算法是基於比較的排序算法,而最後兩個算法不是。中間三種算法是遞歸排序算法,其餘算法通常迭代實現。
基於比較的排序算法:

  1. 冒泡排序,
  2. 選擇排序,
  3. 插入排序,
  4. 歸併排序(遞歸實現),
  5. 快速排序(遞歸執行),
  6. 隨機快速排序(遞歸實現)。

不基於比較的排序算法:

  1. 計數排序,
  2. 基數排序。

冒泡排序

邏輯

給定一個N個元素的數組,冒泡排序將:
比較一對相鄰項目(a,b),
如果項目出現故障,交換對(在本例中爲> b),
重複步驟1和2,直到我們到達數組的最後(最後一對,(N - 2)項和(N -1)項使用基於0的索引)
到目前爲止,最大的項目將在最後的位置。
然後我們將N減少1,並重復步驟1,直到我們有N = 1。
(一個框,可以把框內的兩個數按大小排序,框先框住最左邊的兩個數,排序,然後右移一位,排序,繼續…)
(這樣循環一次,最大的便移動到最右端了,然後去掉最右端,再來)

時間複雜度

ON2
(標準)氣泡排序中有兩個嵌套循環。

外部循環運行正好N次迭代。
但內循環越來越短:
當i = 0,(N -1)迭代(比較和可能的交換)時,
當i = 1,(N -2)迭代時,
…,
當i =(N -2),1次迭代時,
當i =(N -1),0迭代。
因此,迭代的總數=N1+N2+...+1+0=NN12
總時間=cNN1/2=ON2

代碼實現

def bubble_sort(lists):
    for i in range(len(lists)):
        for j in range(len(lists)-i-1):
            if lists[j] > lists[j+1]:
                lists[j],lists[j+1] = lists[j+1],lists[j]
        print(lists)
    return lists

哦。。。我說看別人的代碼怎麼和我的都不一樣,別人的都是從前往後,從小到大排的,我的是從後往前,從大到小排的,雖然結果是一樣的吧
或許因爲我是完全跟着那個網站走的吧

def bubble_sort(lists):
    # 冒泡排序
    count = len(lists)
    for i in range(0, count):
        for j in range(i + 1, count):
            if lists[i] > lists[j]:
                lists[i], lists[j] = lists[j], lists[i]
    return lists

選擇排序

邏輯

給定N個項目的數組,L = 0,選擇排序將:
在[ L … N -1] 的範圍內找到最小項目X的位置,
交換X與第L項,
將下限L增加1並重復步驟1,直到L = N -2。
(找麥子,找最小的,比手上小的就記住位置及大小,一路找到最後,將那個位置與當前位置的交換,走到下一位,再次執行)

時間複雜度

ON2
類似與冒泡排序
外循環N次
內循環越來越短
因此,迭代的總數=N1+N2+...+1+0=NN12
總時間=cNN1/2=ON2

代碼實現

def select_sort(lists):
    for i in range(len(lists)):
        key = i
        for j in range(i,len(lists)):
            if lists[j] < lists[key]:
                key = j
        lists[i],lists[key] = lists[key],lists[i]
    return lists

插入排序

邏輯

(對前兩個數進行排序)
(檢測第三個數,向左比較,如果比自己大,將那個數右移一位,繼續向左比較,直到比自己小(或者沒有數(到頭了)),在該位置插入)
(檢測第四個數…)

時間複雜度

外部循環執行N次,這很清楚。
但內循環的執行次數取決於輸入:
在最佳情況下,陣列已經被排序,並且(a [j]> X)總是爲假
所以不需要數據移位,並且內循環在O(1)中運行,
在最壞情況下,陣列是反向排序的,(a [j]> X)總是爲真
插入總是出現在數組的前面,內部循環在O(N)中運行。
因此,
最佳情況時間爲ON×1=ON
最壞情況時間爲ON×N=ON2

代碼實現

def insert_sort(lists):
    for i in range(1,len(lists)):
        key = lists[i]
        for j in range(i-1,-1,-1):
            if lists[j] < key:
                lists[j+1] = key
                break
            lists[j+1] = lists[j]
        else:
            lists[0] = key

    return lists

倒着數原來能數到負數啊,但是還是不能直接搞定0位置

def insert_sort(lists):
    # 插入排序
    count = len(lists)
    for i in range(1, count):
        key = lists[i]
        j = i - 1
        while j >= 0:
            if lists[j] > key:
                lists[j + 1] = lists[j]
                lists[j] = key
            j -= 1
    return lists

我比它少一句賦值,但是用了break,但是他的看起來也不是標準的插入啊

歸併排序

邏輯

給定一組N個項目,合併排序將:
將每對單個元素(默認情況下,排序)合併到2個元素的排序數組中,
將2個元素的每對排序的數組合併爲4個元素的排序數組,
重複該過程…,
最後一步:合併2個N / 2元素的排序數組(爲了簡單的討論,我們假設N是偶數),以獲得一個完整排序的N個元素的數組。
這只是一般的想法,我們需要更多的細節,然後才能討論歸併排序的真實形式。
(排序第一第二個數,排序第三第四個數,排序一二三四)
(排序第五第六個數,排序第七第八個數,排序五六七八,排序一二三四五六七八)
(排序第九第十個數,to be continued)

時間複雜度

ON

給定大小爲N 1和N 2的兩個排序的數組A和B,我們可以在O(N)個時間內將它們有效地合併成一個大小爲N = N 1 + N 2的較大組合排序的數組。
這是通過簡單地比較兩個陣列的前端並在任何時候取兩個中較小的一個來實現的。但是,我們將需要額外的數組來正確地進行合併。

Divide and Conquer(簡稱D&C)(分治算法)

歸併排序是一個分治排序算法。
分治算法遞歸地把一個大問題分解爲多個類型相同的子問題,直到這些子問題足夠的簡單能被直接解決.最後把這些子問題的解結合起來就能得到原始問題的解.
分解步驟很簡單:將當前數組劃分成兩半(如果N是偶數則完全相等,如果N是奇數,則一邊稍大一個元素),然後遞歸地對這兩半進行排序。
治理步驟是最有效的工作:使用前面討論的合併例程合併兩個(排序)的一半以形成排序的數組。

時間複雜度計算

在歸併排序中,大部分工作是在治理/合併步驟中完成的,因爲分解步驟並不真正做任何事情(視爲O(1))。

這裏寫圖片描述
Level 1: 2^0=1 calls to merge() with N/2^1 items each, O(2^0 x 2 x N/2^1) = O(N)
Level 2: 2^1=2 calls to merge() with N/2^2 items each, O(2^1 x 2 x N/2^2) = O(N)
Level 3: 2^2=4 calls to merge() with N/2^3 items each, O(2^2 x 2 x N/2^3) = O(N)

Level (log N): 2^(log N-1) (or N/2) calls to merge() with N/2^log N (or 1) item each, O(N)

存在logN級,並且在每個級別中,我們執行O(N)工作,因此總時間複雜度爲O(N log N)。我們稍後會看到這是一個最優(基於比較的)排序算法,即我們不能比這更好。

稍稍有些難以理解。

代碼實現

def merge(left, right):
    l,r = 0,0
    nlist = []
    while l<len(left) and r<len(right):
        if left[l] < right[r]:
            nlist.append(left[l])
            l += 1
        elif left[l] > right[r]:
            nlist.append(right[r])
            r += 1
    while l < len(left):
        nlist.append(left[l])
        l += 1
    while r<len(right):
        nlist.append(right[r])
        r += 1
    return nlist
def merge_sort(lists):
    if len(lists) <= 1:
        return lists
    n = len(lists)/2
    left = merge_sort(lists[:n])
    right = merge_sort(lists[n:])
    return merge(left,right)
def merge(left, right):
    i, j = 0, 0
    result = []
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    result += left[i:]
    result += right[j:]
    return result

def merge_sort(lists):
    # 歸併排序
    if len(lists) <= 1:
        return lists
    num = len(lists) / 2
    left = merge_sort(lists[:num])
    right = merge_sort(lists[num:])
    return merge(left, right)

不需要用while循環的

快速排序

邏輯

分解步驟:選擇項目p(稱爲樞紐)
然後將[i..j]的項目分爲三部分:a [i..m-1],a [m]和[m + 1 ..j]。
a [i..m-1](可能爲空)包含小於p的項目。
a [m]是樞軸p,即索引m是數組a的排序順序中p的正確位置。a [m + 1..j](可能爲空)包含大於或等於p的項目。然後,遞歸地分類這兩個部分。
治理步驟:不要驚訝…我們什麼都不做:哦!
(選擇第一個數,將其它所有的數同它進行比較,比它小的放到前一個序列,比它大的放到後邊的序列(這樣這個數的位置就確定了))
(這樣我們有了一個位置已經確定的數以及兩個序列,在對那兩個序列進行同樣的步驟(遞歸))

時間複雜度

我們將通過首先討論其最重要的子例程O(N)分區來剖析此快速排序算法。

爲了分割一個[i..j],我們首先選擇一個[i]作爲樞軸點p。
其餘項目(即[i + 1..j])分爲3個區域:
S1 = a [i + 1..m]其中項目爲< p,
S2 = 一個[M + 1..k-1] ,其中項目≥ p,和
未知= a [k..j],其中項目尚未分配給S1或S2。
最初,S1和S2兩個區域都是空的,即除了指定樞軸p之外的所有項目都在未知區域。
然後,對於未知區域中的每個項目a [k],我們將a [k]與p進行比較,並確定兩種情況之一:
如果一個[K] ≥ p,把一個[K]爲S2,或
否則,將[k]設爲S1。
最後,我們交換一個[i]和[m]將樞軸p右移到S1和S2的中間。
這裏寫圖片描述
這裏寫圖片描述
只有一個循環遍歷(ji)次。由於j可以和N -1 一樣大,我可以低至0,所以分區的時間複雜度爲O(N)。
在這種最糟糕的情況輸入情況下,會發生什麼:
這裏寫圖片描述
當分區總是將陣列分成兩個相等的兩半,如合併排序時,出現快速排序的最佳情況。

當這種情況發生時,遞歸的深度只有日誌n。

當每個級別採用O(N)比較時,時間複雜度爲O(N log N)。

代碼實現

快速排序基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。

如序列[6,8,1,4,3,9],選擇6作爲基準數。從右向左掃描,尋找比基準數小的數字爲3,交換6和3的位置,[3,8,1,4,6,9],接着從左向右掃描,尋找比基準數大的數字爲8,交換6和8的位置,[3,6,1,4,8,9]。重複上述過程,直到基準數左邊的數字都比其小,右邊的數字都比其大。然後分別對基準數左邊和右邊的序列遞歸進行上述方法。

def quick_sort(lists,left,right):
    if left >= right:
        return lists
    key = lists[left]
    low = left
    high = right
    while left < right:
        while left < right and lists[right] >= key:
            right -= 1
        lists[left] = lists[right]
        while left < right and lists[left] <= key:
            left += 1
        lists[right] = lists[left]
    lists[right] = key
    quick_sort(lists, low, left - 1)
    quick_sort(lists, left + 1, high)
    return lists

其它方法的快速排序,說起來和我最初的設想是一樣的,可是我寫的時候不知怎麼就遞歸超時了,尷尬

def pythonic_quick_sort(a):
    if len(a) <= 1:
        return a
    pivot = a[-1]
    pivots = [i for i in a if i == pivot]
    left = pythonic_quick_sort([i for i in a if i < pivot])
    right = pythonic_quick_sort([i for i in a if i > pivot])
    return left + pivots + right
#def qsort(L):  
#   if not L: return []  
#   return qsort([x for x in L[1:] if x< L[0]]) + L[0:1] + \  
#          qsort([x for x in L[1:] if x>=L[0]]) 

隨機快速排序

邏輯

不選第一個數,隨便選個位置的數

時間複雜度

ONlogN)
不想寫了

計數(桶)排序

如果存在輸入數組的某些假設,我們可以實現更快的排序算法 - 即在O(N)中,因此我們可以避免比較項目以確定排序順序。

邏輯

假設:如果要排序的項目是小範圍的整數,我們可以計算每個整數的出現頻率(在該小範圍內),並循環通過該小範圍以排序順序輸出項目。
嘗試計數對上面的示例數組進行排序,其中所有整數都在[1..9]內,因此我們只需要計算整數1出現多少次,出現多少次整數2,出現多少次整數9,然後循環1到9。
(假設序列中的數都在1到9中,只要統計下個數,就能進行排序了)

時間複雜度

時間複雜度是O(N)來計算頻率,O(k)其中k是輸入整數的範圍,在這個例子中爲9-1 + 1 = 9。計數排序的時間複雜度是O(N + k),如果k是(非常)小,則爲O(N)。

代碼實現

def bucket_sort(lists):
    buckets = [0 for i in range(10)]
    for i in lists:
        buckets[i] += 1
    nlist=[]
    for i,j in enumerate(buckets):
        while j>0:
            nlist.append(i)
            j -= 1
    return nlist

基數排序

邏輯

把每一個項目進行排序爲字符串ð數字(我們墊整數有小於ð位數字加上零如果必要的話)。

對於最高有效位(最右邊)的數字(最左邊),我們通過N個項目,並將它們根據活動數字放入10個隊列(每個數字[0..9]一個),這就像一個修改的計數排序這個保持穩定性。然後我們再重新連接組,以便後續的迭代。
(將序列中的每一個數的最後一位按從0到9進行排列)
這裏寫圖片描述
(然後逐項扔出(先進先出))
這裏寫圖片描述
(將序列中的每一個數的倒數第二位按從0到9進行排列)
這裏寫圖片描述
(然後逐項扔出(先進先出))
這裏寫圖片描述
(依此類推,直到最高位)

時間複雜度

注意,我們只執行O(d×(N + k))次迭代。d爲最大數字位數,k爲數量。

代碼實現

def radix_sort(lists):
    lmax = max(lists)
    d=0
    while lmax > 0:
        lmax /= 10
        d += 1
    for k in range(d):
        s=[[] for i in range(10)]
        for i in lists:
            s[i/(10**k)%10].append(i)
        lists=[j for i in s for j in i]
    return lists

希爾排序

邏輯

希爾排序的實質就是分組插入排序,該方法又稱縮小增量排序,因DL.Shell於1959年提出而得名。
希爾排序,也稱遞減增量排序算法,是插入排序的一種更高效的改進版本。希爾排序是非穩定排序算法。
希爾排序是基於插入排序的以下兩點性質而提出改進方法的:
插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率
但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位

時間複雜度:O((1+τ)n)

代碼實現

def shell_sort(lists):
    step = len(lists)/2
    while step > 0:
        for i in range(step, len(lists)):
            while i >= step and lists[i-step] > lists[i]:
                lists[i], lists[i-step] = lists[i-step], lists[i]
                i -= step
        step = step/2
    return lists

堆排序

邏輯

堆排序(Heapsort)是指利用堆積樹(堆)這種數據結構所設計的一種排序算法,它是選擇排序的一種。可以利用數組的特點快速定位指定索引的元素。堆分爲大根堆和小根堆,是完全二叉樹。大根堆的要求是每個節點的值都不大於其父節點的值,即A[PARENT[i]] >= A[i]。在數組的非降序排序中,需要使用的就是大根堆,因爲根據大根堆的要求可知,最大的值一定在堆頂。

堆排序的思想

堆是一種數據結構,可以將堆看作一棵完全二叉樹,這棵二叉樹滿足,任何一個非葉節點的值都不大於(或不小於)其左右孩子節點的值。 將一個無序序列調整爲一個堆,就可以找出這個序列的最大值(或最小值),然後將找出的這個值交換到序列的最後一個,這樣有序序列就元素就增加一個,無序序列元素就減少一個,對新的無序序列重複這樣的操作,就實現了排序。

堆排序的執行過程

1.從無序序列所確定的完全二叉樹的第一個非葉子節點開始,從右至左,從下至上,對每個節點進行調整,最終將得到一個大頂堆。
對節點的調整方法:將當前節點(假設爲a)的值與其孩子節點進行比較,如果存在大於a的值的孩子節點,則從中選出最大的一個與a交換。當a來到下一層的時候重複上述過程,直到a的孩子節點的值都小於a爲止

2.將當前無序序列中的第一個元素(反映在數中是根節點b),與無序序列中的最後一個元素交換(假設爲c),b進入有序序列,到達最終位置。無序序列元素減少1個,有序序列元素增加1個,此時只有節點c可能不滿足堆的定義,對其進行調整。

3.重複2 的過程,直到無序序列的元素剩下一個時排序結束。

代碼實現

這個不是自己寫的了

def sift_down(arr, start, end):
    root = start
    while True:
        # 從root開始對最大堆調整
        child = 2 * root + 1
        if child > end:
            break

        # 找出兩個child中交大的一個
        if child + 1 <= end and arr[child] < arr[child + 1]:
            child += 1

        if arr[root] < arr[child]:
            # 最大堆小於較大的child, 交換順序
            arr[root], arr[child] = arr[child], arr[root]

            # 正在調整的節點設置爲root
            root = child
        else:
            # 無需調整的時候, 退出
            break


def heap_sort(arr):
    # 從最後一個有子節點的孩子還是調整最大堆
    first = len(arr) // 2 - 1
    for start in range(first, -1, -1):
        sift_down(arr, start, len(arr) - 1)

    # 將最大的放到堆的最後一個, 堆-1, 繼續調整排序
    for end in range(len(arr) -1, 0, -1):
        arr[0], arr[end] = arr[end], arr[0]
        sift_down(arr, 0, end - 1)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章