Python的十種常見算法

十種排序算法

1. 常見算法分類

十種常見排序算法通常分爲如下幾種:python

(1)非線性時間比較類排序:算法

​ a. 交換類排序(快速排序、冒泡排序)shell

​ b. 插入類排序(簡單插入排序、希爾排序)數組

​ c. 選擇類排序(簡單選擇排序、堆排序)app

​ d. 歸併排序(二路歸併排序、多路歸併排序)less

(2)線性時間非比較類排序:函數

​ a. 技術排序性能

​ b. 基數排序ui

​ c. 桶排序code

總結:

(1)在比較類排序種,歸併排序號稱最快,其次是快速排序和堆排序,二者不相伯仲,可是有一點須要注意,數據初始排序狀態對堆排序不會產生太大的影響,而快速排序卻偏偏相反。

(2)線性時間非比較類排序通常要優於非線性時間比較類排序,但前者對待排序元素的要求較爲嚴格,好比計數排序要求待待排序數的最大值不能太大,桶排序要求元素按照hash分桶後桶內元素的數量要均勻。線性時間非比計較類排序的典型特色是以空間換時間。

2. 算法描述於實現

2.1 交換類排序

交換類排序的基本方法是:兩兩比較待排序記錄的排序碼,交換不知足順序要求的偶對,直到所有知足位置。常見的冒泡排序和快速排序就屬於交換類排序。

2.1.1 冒泡排序

算法思想:

從數組中第一個數開始,依次便利數據組中的每個數,經過相鄰比較交換,每一輪循環下來找出剩餘未排序數終端最大數並「冒泡」至數列的頂端。

算法步驟:

(1)從數組中第一個數開始,依次與下一個數比較並次交換比本身小的數,直到最後一個數。若是發生交換,則繼續下面的步驟,若是未發生交換,則數組有序,排序結束,此時時間複雜度未O(n);

(2)每一輪「冒泡」結束後,最大的數將出如今亂序數列的最後一位。重複步驟1。

穩定性:穩定排序。

時間複雜度:O(n)至O(n^2),平均時間複雜度爲O(n^2)。

最好的狀況:若是待排序數據列爲正序,則一趟排序就可完成排序,排序碼的比較次數爲(n-1)次,且沒有移動,時間複雜度爲O(n)。

最壞的狀況:若是待排序數據序列爲逆序,則冒泡排序須要(n-1)趟起泡,每趟進行(n-i)次排序碼的比較和移動,即比較和移動次數均達到最大值:

比較次數:Cmax=∑i=1n−1(n−i)=n(n−1)/2=O(n^2)

移動次數等於比較次數,所以最壞時間複雜度爲O(n^2)

實例代碼:

# 冒泡排序
def bubble_sort(nums):
    for i in range(len(nums)-1):    # 這個循環負責冒泡排序進行的次數
        for j in range(len(nums)-i-1): # j爲列表下標
            if nums[j] > nums[j+1]:
                nums[j], nums[j+1] = nums[j+1], nums[j]
    return nums

print(bubble_sort([45, 32, 8, 33, 12, 22, 19, 97]))
# 輸出:[8, 12, 19, 22, 32, 33, 45, 97]

2.1.2 快速排序

冒泡排序是在相鄰的兩個記錄進行比較和交換,每次交換隻能上移或下移一個位置,致使總的比較與移動次數較多。快速排序又稱爲分區交換排序,是對冒泡排序的改進,快速排序採用的思想是分治思想。

算法原理:

(1)從待排序的n個記錄中任意選取一個記錄(一般選取第一個記錄)爲分區標準;

(2)把全部小於該排序列的記錄移動到左邊,把全部大於該排序碼的記錄移動到右邊,中間放所選記錄,稱之爲第一趟排序;

(3)而後對先後兩個子序列分別重複上述過程,直到全部記錄都排好序。

穩定性:不穩定排序

時間複雜度:O(nlog2n)至O(n^2),平均時間複雜度爲O(nlogn)。

最好的狀況:每趟排序結束後,每次劃分使兩個子文件的長度大體相等,時間複雜度爲O(nlogn)。

最壞的狀況:使待排序記錄已經拍好序,第一趟通過(n-1)次比較後第一個記錄保持位置不變,並等到一個(n-1)個元素的子記錄;第二趟通過(n-2)次比較,將第二個記錄定位在原來的位置上,並獲得一個包括(n-2)個記錄的子文件,依次類推,這樣總的比較次數是:

Cmax=∑i=1n−1(n−i)=n(n−1)/2=O(n2)

實例代碼:

# 快速排序
def quick_sort(array):
    if len(array) < 2:  # 基線條件(中止遞歸的條件)
        return array
    else:   # 遞歸條件
        base_value = array[0]   # 選擇基準值
        # 由全部小於基準值的元素組成的子數組
        less = [m for m in array[1:] if m < base_value]
        # 包括基準在內的同時和基準相等的元素
        equal = [w for w in array if w == base_value]
        # 由全部大於基準值的元素組成的子數組
        greater = [n for n in array[1:] if n > base_value]
    return quick_sort(less) + equal + quick_sort(greater)

# 示例:
array = [2,3,5,7,1,4,6,15,5,2,7,9,10,15,9,17,12]
print(quickSort(array))
# 輸出爲[1, 2, 2, 3, 4, 5, 5, 6, 7, 7, 9, 9, 10, 12, 15, 15, 17]

2.2 插入類排序

插入排序的基本方法是:每步將一個待排序的記錄,按其排序碼大小,插到前面已經排序的文件中的適當位置,直到所有插入完爲止。

2.2.1 直接插入排序

原理:從待排序的第n個記錄中的第二個記錄開始,依次與前面的記錄比較並尋找插入的位置,每次外循環結束後,將當前的數插入到合適的位置。

穩定性:穩定排序。

時間複雜度:O(n)至O(n^2),平均時間複雜度是O(n^2)。

最好狀況:當待排序記錄已經有序,這時須要比較的次數是Cmin=n−1=O(n) 。

最壞狀況:若是待排序記錄爲逆序,則最多的比較次數爲。Cmax=∑i=1n−1(i)=n(n−1)2=O(n2) 。

實例代碼:

# 直接插入排序
def insert_sort(array):
    n = len(array)
    for i in range(1, n):
        if array[i] < array[i - 1]:
            temp = array[i]
            index = i  # 待插入的下標
            for j in range(i - 1, -1, -1):  # 從i-1循環到0(包括0)
                if array[j] > temp:
                    array[j + 1] = array[j]
                    index = j  # 記錄待插入下標
                else:
                    break
            array[index] = temp
    return array


lst = [1, 3, 4, 5, 6, 99, 56, 23, 78, 90]
print(insert_sort(lst))
# [1, 3, 4, 5, 6, 23, 56, 78, 90, 99]

2.2.2 Shell排序

Shell排序又稱縮小增量排序,由D.L.Shell在1959年提出,是對直接插入排序的改進。

原理:Shell排序法是對相鄰指定距離(稱爲增量)的元素進行比較,並不斷把增量縮小至1,完成排序。

shell排序開始時增量較大,分組較多,每組的記錄數目較少,故在各組內採用直接插入排序較快,後來增量di逐漸縮小,分組數減小,各組的記錄數增多,但因爲已經按(di-1)分組排序,文件較接近於有序狀態,因此新的一趟排序過程較塊。所以Shell排序在效率上比直接插入排序有較大的改進。

在直接插入排序的基礎上,將直接插入排序中的1所有改變稱增量d便可,由於shell排序最後一輪的增量d就爲1.

穩定性:不穩定排序

時間複雜度:O(n^1.3)到O(n^2)。Shell排序算法的時間複雜度分析比較複雜,實際所需的時間取決於各次排序時增量的個數和增量的取值。研究代表,若增量的取值比較合理,Shell排序算法的時間複雜度約爲O(n^1.3)。

對於增量的選擇,Shell最初建議增量選擇爲n/2,而且對增量取半直到1;D.Knuth教授建議di+1=[di-13]序列。

def shellSort(nums):
    # 設定步長
    step = len(nums) // 2
    while step > 0:
        for i in range(step, len(nums)):
            # 相似插入排序, 當前值與指定步長以前的值比較, 符合條件則交換位置
            while i >= step and nums[i - step] > nums[i]:
                nums[i], nums[i - step] = nums[i - step], nums[i]
                i -= step
        step = step // 2
    return nums


if __name__ == '__main__':
    nums = [9, 3, 5, 8, 2, 7, 1]
    print(shellSort(nums))

2.3 選擇類排序

選擇類排序的基本方法是:每步從待排序記錄中選出排序碼最小的記錄,順序放在已排序的記錄序列的後面,直到所有排完。

2.3.1 簡單選擇排序(又稱直接選擇排序)

原理:從全部記錄中選出最小的一個數據元素與第一個位置的記錄交換;而後再剩下的記錄當中再找最小的與第二個位置的記錄交換,循環到只剩下最後一個數據元素爲止。

穩定性:不穩定排序。

時間複雜度:最壞、最好和平均複雜度均爲O(n^2),所以,簡單選擇排序也是常見排序算法中性能最差的排序算法。簡單選擇排序的比較次數與文件的初始狀態沒有關係,在第i趟排序中選出最小排序碼的記錄,須要作(n-i)次比較,所以總的比較次數是:∑i=1n−1(n−i)=n(n−1)/2=O(n2)。

# 簡單選擇排序
def selsectd_sort(array):
    # 獲取list的長度
    length = len(array)
    # 進行比較的輪數
    for i in range(0, length-1):
        smallest = i    # 默認設置最小值的index爲當前值
        # 用當先最小index的值分別與後面的值進行比較,以便獲取最小index
        for j in range(i+1, length):
            # 若是找到比當前值小的index,則進行兩值交換
            if array[j] < array[smallest]:
                array[j], array[smallest] = array[smallest], array[j]

lst = [1, 4, 5, 0, 6]
print(selsectd_sort(lst))

2.3.2 堆排序

直接選擇排序中,第一次選擇通過了(n-1)次比較,只是從排序碼序列中選出了一個最小的排序碼,而沒有保存其餘中間比較結果。因此後一趟排序時又要重複許多比較操做,下降了效率。J.Willioms和Floyd在1964年提出了堆排序方法,避免了這一缺點。

堆的性值:

(1)性質:徹底二叉樹或者是近似徹底二叉樹;

(2)分類:

​ 大頂堆:父節點不小於子節點鍵值。

​ 小頂堆:父節點不大於子節點鍵值,圖展現一個最小堆

(3)左右孩子:沒有大小的順序

(4)堆的存儲:

​ 通常都用數組來存儲堆,i節點的父節點下標就爲(i - 1)/2.。

​ 它的左右子節點下標分別爲(2*i+1)和(2*i+2)。

​ 如第0個節點左右子節點下標分別爲1和2.

(5)堆的操做

​ a. 創建堆

>以最小堆爲例,若是以數組存儲元素時,一個數組具備對應的樹表現形式,但樹並不知足堆的條件,須要從新排列元素,能夠創建「堆化」的樹。

​ b. 插入堆

>將一個新元素插入到表尾,即數組末尾時,若是新構成的二叉樹不知足堆的性質,須要從新排列元素。

​ c. 刪除堆

堆排序中,刪除一個元素老是發生在堆頂,由於堆頂的元素是最小的(小頂堆中)。表中最後一個元素用來填補空缺位置,結果樹被更新以知足堆條件。

穩定性:不穩定排序

插入代碼實現:

每次插入都是講新數據放在數組最後。能夠發現從這個新數據的父節點到根節點必然爲一個有序的數列,如今的任務是將這個新數據插入到這個有序數據中,這就相似於直接插入排序中將一個數據併入到有序區間中,這是節點「上浮」調整。

(6)堆排序的實現

因爲堆也是用數組來存儲的,故堆數組進行堆化後,第一次將A[0]與A[n-1]交換,再對A[0...n-2]從新恢復堆。第二次將A[0]與A[n-2]交換,再對A[0...n-3]從新恢復堆,重複這樣的操做直到A[0]與A[1]交換。因爲每次都是將最小的數據併入到後面的有序區間,故操做完成後整個數組就有序了。有點相似於直接選擇排序。

# 堆排序
def sift_down(array, start, end):
    """
    調整成大頂堆,初始堆時,從下往上;交換堆頂與堆尾後,從上往下調整
    :param array: 列表的引用
    :param start: 父結點
    :param end: 結束的下標
    :return: 無
    """
    while True:
        # 當列表第一個是如下標0開始,結點下標爲i,左孩子則爲2*i+1,右孩子下標則爲2*i+2;
        # 若下標以1開始,左孩子則爲2*i,右孩子則爲2*i+1
        left_child = 2*start + 1  # 左孩子的結點下標
        # 當結點的右孩子存在,且大於結點的左孩子時
        if left_child > end:
            break

        if left_child+1 <= end and array[left_child+1] > array[left_child]:
            left_child += 1
        if array[left_child] > array[start]:  # 當左右孩子的最大值大於父結點時,則交換
            array[left_child], array[start] = swap(array[left_child], array[start])

            start = left_child  # 交換以後以交換子結點爲根的堆可能不是大頂堆,需從新調整
        else:  # 若父結點大於左右孩子,則退出循環
            break

def heap_sort(array):  # 堆排序
    # 先初始化大頂堆
    first = len(array)//2 - 1  # 最後一個有孩子的節點(//表示取整的意思)
    # 第一個結點的下標爲0,不少博客&課本教材是從下標1開始,無所謂吧,你隨意
    for i in range(first, -1, -1):  # 從最後一個有孩子的節點開始往上調整
        print(array[i])
        sift_down(array, i, len(array)-1)  # 初始化大頂堆

    print("初始化大頂堆結果:", array)
    # 交換堆頂與堆尾
    for head_end in range(len(array)-1, 0, -1):  # start stop step
        array[head_end], array[0] = array[0], array[head_end] # 交換堆頂與堆尾
        sift_down(array, 0, head_end-1)  # 堆長度減一(head_end-1),再從上往下調整成大頂堆


if __name__ == "__main__":
    array = [16, 7, 3, 20, 17, 8]
    print(array)
    heap_sort(array)
    print("堆排序最終結果:", array)

(7)堆排序的性能分析

因爲每次從新恢復堆的時間複雜度爲O(logN),共(N-1)次堆調整操做,再加上前面創建堆時(N/2)次向下調整,每次調整時間複雜度也爲O(logN)。兩次操做時間相加仍是O(NlogN)。故堆排序的時間複雜度爲O(N logN)。

最壞狀況:若是待排序數組是有序的,仍然須要O(N*logN)複雜度的比較操做,只是少了移動的操做;

最好狀況:若是待排序數組是逆序的,不只須要O(N*logN)複雜度的比較操做,並且須要O(N*logN)複雜度的交換操做。總的時間複雜度仍是O(N*logN)。

所以,堆排序和快速排序再效率上是差很少的,可是堆排序通常優於快速排序的重要一點是,數據的初始分佈狀況對堆排序的效率沒有大的影響。

2.4 歸併排序

(1)算法思想:

歸併排序屬於比較類非線性時間排序,號稱比較類排序中性能最佳者,再數據應用中較廣。

歸併排序是分治法的一個典型應用。將已有序的子序列合併,獲得徹底有序的序列;即先使每一個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲二路歸併。

(2)穩定性

穩定排序算法

(3)時間複雜度

最壞,最好和平均時間複雜度都是O(nlogn)

# 歸併排序
def merge(left, right):
    # 從兩個右順序的列表李白你依次取數據比較後放入result
    # 每次咱們分別拿出兩個列表中最小的數比較,把較小的放入result
    result = []
    while len(left) > 0 and len(right) > 0:
        # 爲了保持穩定性,當遇到相等的時候優先把左側的數放進結果列表
        # 由於left原本也是大數列中比較靠左的
        if left[0] <= right[0]:
            result.append(left.pop(0))
        else:
            result.append(right.pop(0))
        # while循環出來以後,說明其中一個數組沒有數據了
        # 咱們把另外一個數組添加到結果數組後面
    result += left
    result += right
    return result

def merge_sort(array):
    # 不斷遞歸調用本身,一直到拆分紅單個元素的時候就返回這個元素,再也不拆分了
    if len(array) == 1:
        return array

    # 取拆分的中間位置
    middle = len(array) // 2
    # 拆分事後左側子串
    array_left = array[:middle]
    # 拆分事後右側子串
    array_right = array[middle:]

    # 對拆分事後的左右字串再拆分,一直到只有一個元素爲止
    # 最後一次遞歸時候, left和right都會接到一個元素的列表
    # 最後一次遞歸以前的left和right會接收到排好序的子序列
    left = merge_sort(array_left)
    right = merge_sort(array_right)

    # 咱們對返回的兩個拆分結果進行排序後合併再返回正確順序的字列表
    # 這裏咱們調用一個函數幫助咱們按順序合併left和rigth
    return merge(left, right)

lst = [5, 4, 3, 2, 1]
print(merge_sort(lst))

2.5 線性時間非比較類排序

2.5.1 計數排序

計數排序使一個非基於比較的排序算法,該算法於1954年由Harold H.Seward提出,它的優點在於對於較小範圍內的整數排序。它的複雜度爲O(n+k)(其中K使待排序數的範圍),快於任何比較排序算法,缺點就是很是消耗空間。很明顯,若是當O(k)>O(n*log(n))的時候其效率反而不如基於比較的排序,好比堆排序和歸併排序和快速排序。

(1)算法原理

基本思想是對於給定的輸入序列中的每個元素x,肯定該序列中值小於x的元素的個數。一旦有了這個信息,就能夠將x直接存放到最終的輸出序列的正確位置上。例如,若是輸入序列中只有17個元素的值小於x的值,則x能夠直接存放在輸出序列的第18個位置上。固然,若是有多個元素具備相同的值時,咱們不能將這些元素放在輸出序列的同一個位置上,在代碼中做適當的修改便可。

(2)算法步驟:

​ a. 找出待排序的數組中最大的元素;

​ b. 統計數組中每一個值爲i的元素出現的次數,存入數組c的第i項;

​ c. 對全部的計數累加(從C中的第一個元素開始,每一項和前一項相加);

​ d. 反向填充目標數組:將每一個元素i放在新數組的第C(i)項,每放一個元素就將 C(i)減去1。

(3)時間複雜度

​ O(n+k)

(4)空間複雜度

​ O(k)

(5)要求

​ 待排序數中最大數值不能太大

(6)穩定性

​ 穩定

(7)代碼示例

# 計數排序
def counting_sort(a, k):  # k = max(a)
    n = len(a)  # 計算a序列的長度
    b = [0 for i in range(n)]  # 設置輸出序列並初始化爲0
    c = [0 for i in range(k + 1)]  # 設置計數序列並初始化爲0,
    for j in a:
        c[j] = c[j] + 1
    for i in range(1, len(c)):
        c[i] = c[i] + c[i-1]
    for j in a:
        b[c[j] - 1] = j
        c[j] = c[j] - 1
    return b


print(counting_sort([1, 3, 5, 32, 423, 5, 23, 5, 75], 423))

注意:計數排序是典型的以空間換時間的排序算法,對待排序的數據有嚴格的要求,好比待排序的數值中包含負數,最大值都有限制,謹慎使用。

2.5.2 基數排序

基數排序屬於「分配式排序」,是非比較類線性時間排序的一種,又稱「桶子法」。顧名思義,它是透過鍵值的部分信息,將要排序的元素分配至某些「桶」中,已達到排序的做用。

# 基數排序
def radix_sort(list, d=3): # 默認三位數,若是是四位數,則d=4,以此類推
    for i in range(d):  # d輪排序
        s = [[] for k in range(10)]  # 因每一位數字都是0~9,建10個桶
        for j in list:
            s[int(j / (10 ** i)) % 10].append(j)
        re = [a for b in s for a in b]
    return re

print(radix_sort([12, 4, 23, 26, 85, 12, 45], 2))

2.5.3 桶排序

桶排序也是分配排序的一種,但其是基於比較排序的,這也是與基數排序最大的區別所在。

(1)算法思想

桶排序算法相似於散列表。首先要假設待排序的元素輸入符合某種均勻分佈,例如數據均勻分佈在[0, 1]區間上,則可將此區間劃分爲10個小區間,稱爲桶,對散佈到同一個桶中的元素再排序。

(2)要求

待排序數長度一致

(3)排序過程

​ a. 設置一個定量的數組看成空桶子;

​ b. 尋訪序列,而且把記錄一個一個放到對應的桶子去;

​ c. 對每一個不是空的桶子進行排序;

​ d. 從不是空的桶子裏把項目再放回原來的序列中。

>例如待排序列 k = {49, 38, 35, 97, 76, 73, 27, 49}。這些數據所有在1—100之間。所以咱們定製10個桶,而後肯定映射函數 f(k) = k/10。則第一個關鍵字49將定位到第4個桶中(49/10=4)。依次將全部關鍵字所有堆入桶中,並在每一個非空的桶中進行快速排序。

(4)時間複雜度

對N個關鍵字進行桶排序的時間複雜度分爲兩個部分:

​ a. 循環計算每一個關鍵字的桶映射函數,這個時間複雜度是O(N)

​ b. 利用先進的比較排序算法對每一個桶內的全部數據進行排序,對於N個待排數據,M個桶,平均每一個桶[N/M]個數據,則桶內排序的時間複雜度爲:∑i=1MO(Ni∗logNi)=O(N∗logNM) 。其中Ni爲第i個桶的數據量。

所以,平均時間複雜度爲線性的O(N+C),C爲桶內排序所花費的時間。當每一個桶只有一個數,則最好的時間複雜度爲: O(N)。

# 桶排序
def bucket_sort(a):
    buckets = [0] * ((max(a) - min(a)) + 1)  # 初始化桶元素爲0
    for i in range(len(a)):
        buckets[a[i] - min(a)] += 1  # 遍歷數組a,在桶的相應位置累加值
    b = []
    for i in range(len(buckets)):
        if buckets[i] != 0:
            b += [i + min(a)] * buckets[i]
    return b


print(bucket_sort([1,3, 4, 53, 23, 534, 23]))
相關文章
相關標籤/搜索