1. 程式人生 > >聊聊幾個簡單的排序演算法

聊聊幾個簡單的排序演算法

前言

排序是演算法的入門知識,其經典思想可以用在許多演算法中,在實際應用中是相當常見的一類。記得在本科的資料結構課上就有講過幾個經典的排序演算法,現在來好好地回顧下。
在回顧之前,瞭解一個概念,這個概念也是我剛剛瞭解的。(手動扶額-。-)
排序演算法穩定性:假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,ri=rj,且ri在rj之前,而在排序後的序列中,ri仍在rj之前,則稱這種排序演算法是穩定的;否則稱為不穩定的。
這個穩定的概念有什麼用呢?不管,先記著再說啦。

一、氣泡排序(BubbleSort)

兩個相鄰的數比較大小,較大的數下沉,較小的冒起來。
過程為:

  1. 比較相鄰的兩個資料,如果第二個數小,就交換位置;
  2. 從前往後兩兩比較,一直到比較最後面兩個資料。最終最大的數被交換到末尾位置,這樣第一個最大數的位置就排好了;
  3. 繼續重複上述過程,依次將第2、3…n-1個最大的數排好位置。

當然也可以反著來,從後面往前比較,先排好最小的數到數列到開頭的位置。


python程式碼實現:

def bubble_sort(list):
    l = len(list)
    for j in range(len(list)-1):   #單純地設定迴圈次數,標識趟次
        for i in range(l-1
): if list[i] > list[i+1]: list[i],list[i+1] = list[i+1],list[i] else: pass l = l - 1 #減少比較次數,最後n位的最大數已經排好,不需要再進行比較了 return list

搬運來的動圖,特別直觀:
這裡寫圖片描述

1.1雞尾酒排序

這是一種氣泡排序的改進演算法,可以稱之為雙向氣泡排序:

  1. 先對陣列從左往右進行升序的氣泡排序;
  2. 再對陣列進行從右往左的降序氣泡排序;
  3. 迴圈往復,不斷縮小沒有排序的陣列範圍。
def cocktail_sort(list):
    l = len(list)
    start = 0
    end = l - 1
    flag = True   #標誌上一輪迴圈是否有交換,若無,則表示排序已經完成,無需繼續迴圈(氣泡排序優化點)
    while flag:
        flag = False
        for i in range(start,end,1):
            if list[i] > list[i+1]:
                list[i],list[i+1] = list[i+1],list[i]
                flag = True
            else:
                pass
            end = end - 1
        for i in range(end,start,-1):
            if list[i] < list[i-1]:
                list[i],list[i-1] = list[i-1],list[i]
                flag = True
            else:
                pass
            start = start + 1
    return list

再搬運一張雞尾酒排序動圖:
這裡寫圖片描述

二、 選擇排序(SelctionSort)

選擇排序十分簡單直觀,步驟如下:

  1. 在序列中找到最小(大)元素,存放在序列的起始位置;
  2. 再從剩餘的未排序序列種繼續尋找最小(大)的元素,存放在已排序序列的後一位;
  3. 重複到第n-1次,完成排序。
def selection_sort(list):
    l = len(list)
    for i in range(l-1):
        _index = list.index(min(list[i:l]))   #也可以再套一層迴圈,逐一比較出最小值
        list[i],list[_index] = list[_index],list[i]

感謝網上的動圖:
這裡寫圖片描述

三、插入排序(InsertionSort)

對於未排序資料,在已排序序列中從後向前掃描,找到相應位置插入。十分類似我們打撲克時抓牌的過程。

  1. 從第一個元素開始,單個元素當然是可以認為已排序;
  2. 取出下一個元素,與已排序序列從後向前掃描比較,直到找到已排序元素小於或等於新元素的位置;
  3. 將新元素插入到找到的位置後一個的位置;
  4. 繼續取下一個元素重複步驟。
def insertion_sort(lists):
    l = len(lists)
    for i in range(1,l):   #大迴圈,開始依次抽取元素進行插入
        _tmp = lists[i]
        for j in list(range(i))[::-1]:
            if _tmp < lists[j]:   #比前一個小就繼續前進,被比較元素向後移一位
                lists[j+1] = lists[j]
                if j == 0:   #比較到隊首了,說明臨時值是最小的
                    lists[j] = _tmp
            else:
                lists[j+1] = _tmp
                break
    return lists

繼續搬運:
這裡寫圖片描述

3.1希爾排序

插入排序對那些基本有序的序列排序效率高,但對於亂序的序列,移動次數非常多導致效率較低。所以就有了插入排序的改進版——希爾排序。希爾排序會優先比較距離較遠的元素,又稱之為縮小增量排序。

  1. 設定步長,按步長將原序列分為若干子序列,分別對子序列進行插入排序;
  2. 逐漸減小步長,重複步驟1。直至步長為1,此時序列基本有序,最後進行一次插入排序。

可以看出,希爾排序可以在一開始就對距離較遠的元素進行換位、排序。避免了插入排序中,一個元素在往前比較過程中大量元素被逐一移動的過程。希爾排序的關鍵就是步長的選擇,看到一種說法說步長用質數是個不錯的選擇;也有一說用序列[1,4,13,40,121,364…]後一元素是前一元素的3倍+1;當然還有更加簡單粗暴的len/2,然後依次除以2直至1.
希爾排序動圖展示(這裡展示的步長分別為5、2、1):
這裡寫圖片描述
程式碼略,本質上就是按照步長進行多次插入排序。

四、快速排序(Quicksort)

快速排序簡稱快排,這個排序演算法就厲害了,聽說面試官特別喜歡考,所以重點來了,同志們。

  1. 從序列中取出一個值作為基準;
  2. 把所有比基準值小的擺放在基準的左邊,比基準大的擺在基準的右邊(相等的數任意一邊)。這就完成了一次分割槽操作;
  3. 對左右兩個子序列繼續做這樣的分割槽操作,直至每個區間只有一個數。這是一個遞迴過程。

失敗的一次嘗試:

def quick_sort(arr):
    lefti = 0
    righti = len(arr) - 1
    if righti == -1:
        return 0
    else:
        x = arr[0]   #x即為基準
        count = 0
        k = 0
        while lefti < righti and count < 2:
            for i in range(righti,lefti,-1):
                if righti - lefti == 1:
                    count += 1
                if arr[i] < x and count < 2:
                    arr[lefti] = arr[i]
                    righti = i
                    k = i
                    lefti += 1
                    break
            for j in range(lefti,righti,1):
                if righti - lefti == 1:
                    count += 1
                if arr[j] > x and count < 2:
                    arr[righti] = arr[j]
                    lefti = j
                    k = j
                    righti -= 1
                    break
        arr[k] = x
        quick_sort(arr[:k])
        quick_sort(arr[k+1:])

排序過程沒問題,只是迭代時修改的不是原陣列,而是新的被拆分的陣列,導致只有第一層迴圈的元素交換被保留。無奈還是參照下別的程式碼學習一哈把,coding能力還是有待加強。
參照資料後的改進版:

def quick_sort(arr,left,right):
    if left >= right:
        return
    low = left
    high = right
    key = arr[low]   #取序列的第一個值為基準
    while left < right:
        while left < right and arr[right] >= key:   #外層迴圈裡已經有left<right但內層迴圈裡仍然需要,因為要保證left和rigth最終會合相等,而不能讓left在自增過程中超過right
            right -= 1
        arr[left] = arr[right]
        while left < right and arr[left] <= key:
            left += 1
        arr[right] = arr[left]
    arr[left] = key   #此時left和right已經相等
    quick_sort(arr,low,left-1)
    quick_sort(arr,left+1,high)

大神秀技巧版(一行程式碼實現):

quick_sort = lambda array: array if len(array) <= 1 else quick_sort([item for item in array[1:] if item <= array[0]]) + [array[0]] + quick_sort([item for item in array[1:] if item > array[0]]) 

這個lambda真的很精妙,用兩個列表生成式拼接出完整列表,所有小於等於arr[0]的放左邊,所有大於arr[0]的放右邊。劣勢是佔用了新的記憶體空間,常規版的快排是in-palce的,都是原地操作。
動圖演示:
這裡寫圖片描述

五、歸併排序(MergeSort)

將兩個有序序列合併的方法很簡單,比較2個序列的第一個數,誰小就取誰,取出後刪除對應序列中的這個數。繼續比較直至所有元素都被取出。將兩個有序序列合併的過程稱為2-路歸併。歸併排序就基於此:

  1. 把待排序的序列一分為二,分出兩個子序列;
  2. 繼續將子序列分裂直至子序列中只有一個元素,一個元素自然就算排序完成;
  3. 一路分裂一路歸併,最終獲得完整序列。
def merge(arrX,arrY):   #合併排序演算法
        i,j = 0,0
        arrN = []
        while i < len(arrX) and j < len(arrY):
            if arrX[i] < arrY[j]:
                arrN.append(arrX[i])
                i += 1
            else:
                arrN.append(arrY[j])
                j += 1
        if i == len(arrX):
            arrN.extend(arrY[j:])
        if j == len(arrY):
            arrN.extend(arrX[i:])
        return arrN
def merge_sort(arr):   #迭代過程
    l = len(arr)
    if l <= 1:
        return arr
    else:
        X = merge_sort(arr[:round(l/2)])
        Y = merge_sort(arr[round(l/2):])
        return merge(X,Y)

動圖演示:
這裡寫圖片描述

六、計數排序(CountingSort)

計數排序不是基於比較的排序演算法,它依靠一個輔助陣列來實現,將輸入的資料轉化為鍵儲存在專門準備的陣列空間中,計數排序要求輸入的資料必須是正整數,且最好不要過大。計數排序是用來排序0到100之間的數字且重複項比較多的最好的演算法。

  1. 找到待排序序列種的最大值(也可以找出最小值,建立中間陣列時節省一定的空間);
  2. 統計序列中每個值為i的元素出現的次數,存入陣列C的第i項;
  3. 對所有計數從低到高向上累加,陣列C[i]中的值會變成所有小於等於i的元素個數;
  4. 從原序列反向填充目標陣列:將每個元素i填入新陣列的第C[i]項,每放一個元素C[i]減1.
def counting_sort(arr):
    m = max(arr)
    c = [0 for i in range(m+1)]
    for i in arr:
        c[i] += 1   #c[i]表示在原序列arr中值為i的元素有幾個
    for j in range(1,m+1):
        c[j] = c[j] + c[j-1]   #c[j]表示在原arr序列中最後一個值為j的元素排第幾位,或者表示小於等於j的元素個數
    res = [None for i in range(len(arr))]
    for r in range(len(arr)-1,-1,-1):
        res[c[arr[r]]-1] = arr[r]   #因為索引是從0開始的,需要-1
        c[arr[r]] -= 1  #放置好一個元素,就需要在c陣列中去掉一個元素,最後c陣列會變成全0的陣列
    return res

依然是動圖伺候:
這裡寫圖片描述

後話

其實還是有一些排序演算法沒有涉及,比如桶排序、基數排序、堆排序。畢竟不是專業的程式設計師,就不繼續深究了。文中展示的所有程式碼都是本人手寫並執行驗證通過的,可放心複製貼上食用。
關於複雜度也可以一張圖說明:
這裡寫圖片描述


還記得開頭講過的穩定性麼,開寫這篇博文時並不明白其作用。實現了這幾個排序演算法後也自己琢磨明白了一點。穩定的好處是:從一個鍵上排序,然後再從另一個鍵上排序,第一個鍵排序的結果可以為第二個鍵排序所用。
也就是說,實際應用中我們遇到的排序不是簡單地針對一個數組或者一個序列,而是有很多維度的。我們針對其中一個維度進行穩定排序,原先其他維度的先後順序不會被改變。這才是穩定性的意義所在。

最後感謝來自他人部落格的動圖,轉載宣告:
來源於一畫素的部落格:https://www.cnblogs.com/onepixel/articles/7674659.html