排序算法原理與Python實現(xiàn)

目錄

1.冒泡排序
2.選擇排序
3.插入排序
4.堆排序
5.歸并排序
6.快速排序
7.希爾排序

一個比較:O(n*logn)O(n^2)快多少?
假設(shè)n=100000; n^2=10^{10};n*logn=1660964;前者約是后者的6000倍;約等于一分鐘和4天的比例;
這個比較是為了說明兩種復(fù)雜度算法的差異之大;更說明了學(xué)習(xí)更有效率算法的必要性

排序算法可針對數(shù)字類型,字符,或者更高級的類型進行排序,算法邏輯都是一樣的僅僅比較邏輯做策略模式替換即可。
以下所有代碼案例,僅進行數(shù)字類型排序

一,冒泡排序

原理:遍歷數(shù)列,每個過程,比較當(dāng)前值與下一位置的值,通過交換位置,將較大的數(shù)置后;像冒泡一樣
總的比較次數(shù)為(n-1)+(n-2)+…+1≈\frac{n^2}{2}。復(fù)雜度為O(n^2)

# code utf-8
def up_sort(lis:list, n:int):
    for i in range(n):
        for j in range(0, n-i-1):
            if lis[j] < lis[j+1]:
               lis[j], lis[j+1] = lis[j+1], lis[j]
               print(lis) # 通過這一行打印排序步驟,直接看到冒泡效果

a = [i for i in range(10)]
a.reverse()

print(a)  # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
up_sort(a, len(a))
print(a)  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

二,選擇排序

原理:從左至右(or從上到下)遍歷,尋找遍歷最小值,放在數(shù)列最左端;遞歸;
總的比較次數(shù)為(n-1)+(n-2)+…+1≈\frac{n^2}{2}。復(fù)雜度為O(n^2)

# code utf-8
def selec_sort(lis:list, n:int):
    for i in range(n):
        # 尋找i~n區(qū)間的最小值
        min_index = i
        for j in range(i+1, n):
            if lis[j] < lis[min_index]:
                min_index = j
        # swap函數(shù)交換位置,如果是C++ 11以上版本,std中即包含swap
        lis[i], lis[min_index] = lis[min_index], lis[i]

a = [i for i in range(10)]
a.reverse()

print(a)  # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
selec_sort(a, len(a))
print(a)  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

三,插入排序(?? 有應(yīng)用價值)

插入排序,是優(yōu)化版的冒泡排序,在于提前終止循環(huán),節(jié)省資源;排序目標(biāo)有序性越高,插入排序的性能就越優(yōu)于冒泡和選擇
原理:從左至右(or從上到下)遍歷,將當(dāng)前值,插入到左端已排序序列的合適位置;
總的比較次數(shù)為(n-1)+(n-2)+…+1≈\frac{n^2}{2}。復(fù)雜度為O(n^2)
對于完全有序的數(shù)組,插入排序的復(fù)雜度為O(n)

# code utf-8
# 方法一:根據(jù)自己的理解寫的,把握好index的理解,否則容易出錯
def insert_sort(lis:list, n:int):   # 
    for i in range(1,n): # 最左側(cè)第一個數(shù)字不用排序,默認是已排序列
        for j in range(0, i):
            if lis[i] < lis[j]:
                a = lis[i]
                lis.remove(lis[i])
                lis.insert(j, a)
                break
        print(lis)

# 方法二:根據(jù)某教程
def insert_sort(lis:list, n:int):   # 借鑒
    for i in range(1,n): # 最左側(cè)第一個數(shù)字不用排序,默認是已排序列
        for j in range(i, 0, -1):
            if lis[j] < lis[j-1]:
                lis[j], lis[j-1] = lis[j-1], lis[j]
            else:
                break
        print(lis)

# 方法三,優(yōu)化
def insert_sort(lis:list, n:int):
    for i in range(1,n): # 最左側(cè)第一個數(shù)字不用排序,默認是已排序列
        temp = lis[i]
        target_index = i
        for j in range(i, 0, -1):
            if temp < lis[j-1]:
                lis[j] = lis[j-1] # 賦值代替交換
                target_index = j - 1 # 當(dāng)前位置
            else:
                break
        lis[target_index] = temp
        print(lis)

a = [4,7,2,8,3,9,1,6,0]
if __name__ == "__main__":
    insert_sort(a, len(a))

按照原理和代碼的說明,可以看到,插入排序在迭代次數(shù)上,比較次數(shù)上,都要少于冒泡和選擇排序;
問題:但是實際測試運行時間,會發(fā)現(xiàn)選擇排序 優(yōu)于 插入排序 優(yōu)于 冒泡排序
原因:這是因為,選擇排序比較計算多,但是交換位置操作少,而插入排序和冒泡排序存在大量數(shù)組元素位置交換,操作內(nèi)存的IO時間開銷是很大的;

優(yōu)化思路:
1,使用方法一,只交換一次,(效率未測試)
2,使用方法三,通過賦值替代交換;此方法同樣可用于優(yōu)化冒泡排序(這樣優(yōu)化后,冒泡排序就變成了選擇排序)

四,堆排序

五,歸并排序一:自頂向下遞歸(先分組,后merge)(合并排序 merge_sort)

原理:將排序序列分半,兩組排序后再合并成有序序列。遞歸。
實現(xiàn):上的細節(jié)就是:將序列無限二分,最終分為每組兩個元素的很多小組,小組內(nèi)排序后,兩個a級別小組合并排序為b界別,兩個b級別再繼續(xù)合并排序,直至合為一個有序的序列。
復(fù)雜度分析:由于是二分法分組,如果總元素是N個,那么序列分裂行為次數(shù)(即遞歸層數(shù))logn級別的

核心算法->合并兩組有序序列邏輯:例如要歸并[4,6],[3,7]兩個小組;
1,首先創(chuàng)建一個新的空數(shù)組lis_new
2,比較兩個序列首位,然后將較小的推入lis_new中。遞歸。
3,關(guān)鍵點:三個索引,兩個待合并小組當(dāng)前值的索引和新數(shù)組的索引(通過比較,賦值,移動索引,而不是刪除小組內(nèi)容,推入新數(shù)組這種有更大IO開銷的操作)
合并兩組序列的復(fù)雜度與序列元素個數(shù)有關(guān),很簡單這個算法是O(n)的。
結(jié)合上面歸并算法的理解,歸并算法有兩部分,分裂:復(fù)雜度O(logn),合并:復(fù)雜度O(n);
所以歸并排序的復(fù)雜度是O(n*logn)

歸并排序的缺點:占用內(nèi)存大,典型的空間換時間

以下代碼是我根據(jù)理解和參考自己實現(xiàn)的,雖然完成了示例中的排序,不保證完全正確。

# code utf-8
import math

def merge_lis(lis, i, j, mid):
    arr1 = [item for item in lis]
    index1, index2 = i, mid+1
    for k in range(j-i+1):
        if index1>mid:
            lis[k+i] = arr1[index2]
            index2 += 1
        elif index2>j:
            lis[k+i] = arr1[index1]
            index1 += 1
        elif arr1[index1] < arr1[index2]:
            lis[k+i] = arr1[index1]
            index1 += 1
        else:
            lis[k+i] = arr1[index2]
            index2 += 1

    print(lis)

def merge_sort(lis:list, n:int):
    def split(lis:list, i:int, j:int): # 要處理的數(shù)據(jù)段的開始位置,結(jié)束位置;前閉后閉
        if (i >= j):
            return
        # 第一步:數(shù)組二分
        mid = math.floor((i+j)/2)  # 存在數(shù)據(jù)過大,溢出風(fēng)險;向上取整
        split(lis, i, mid)
        split(lis, mid+1, j)
        # 第二步:merge兩段,排序核心
        merge_lis(lis, i, j, mid)

    split(lis, 0, n-1)

a = [4,7,2,8,3,9,1,6,0]
if __name__ == "__main__":
    merge_sort(a, len(a))

思考:對于近乎有序的數(shù)組,歸并排序與插入排序效率比較如何?答案是插入排序更快,大概是logn倍,這從兩者的復(fù)雜度上就能計算得出,因為對于近乎有序的數(shù)組,優(yōu)化后的插入排序,復(fù)雜度是接近O(n)的。
優(yōu)化
方法一:判斷。同樣的,對于歸并排序優(yōu)化后,對于近乎有序的數(shù)組,也可以是接近O(n)復(fù)雜度的,優(yōu)化方法,僅需增加一行代碼;如下代碼示例 (提升效率數(shù)十倍,當(dāng)n=50000)
方法二:結(jié)合。當(dāng)歸并遞歸到底的時候(分組足夠小的時候,比如每組元素少于10個),此時可使用插入排序來提高性能。(提升效率一倍,當(dāng)n=50000)

# 只需在上面的程序基礎(chǔ)上,增加一行;僅對于無序的進行歸并,有序的不歸并
if lis[mid] > lis[mid+1]:  
     merge_lis(lis, i, j, mid)

五,歸并排序二(自底向上,迭代)

原理不變,優(yōu)化方式相同;僅邏輯相反;

#此部分由于python的循環(huán)語法限制,不易實現(xiàn),后面補C++實現(xiàn)

六,快速排序

原理:隨機選一個數(shù)作為基準(zhǔn);將其余的數(shù)字與基準(zhǔn)比較,分為比基準(zhǔn)大和比基準(zhǔn)小的兩組;遞歸即可。
。平均復(fù)雜度為O(nlogn),如果運氣較差,每次都選最小值為基準(zhǔn),復(fù)雜度為O(n^2),本質(zhì)同選擇排序
關(guān)鍵:如何分出兩組,并找出基準(zhǔn)值的合理索引值。

1,基礎(chǔ)版(效率即優(yōu)于歸并)

# 第一版,原理實現(xiàn)
def partation(lis, start, end):
    p = lis[start]
    p_index = start
    for i in range(start+1, end+1):
        if lis[i] < p:
            lis[i], lis[p_index+1] = lis[p_index+1], lis[i]# 較小的數(shù)swap到前面
            p_index += 1 # 記錄正確索引位置
    lis[start], lis[p_index] = lis[p_index], lis[start]
    return p_index

def _quick_s(lis, start, end):
    if start > end:
        return
    index = partation(lis, start, end)  # 選擇基準(zhǔn)值,以基準(zhǔn)值分組,并返回基準(zhǔn)值索引
    _quick_s(lis, start, index-1)
    _quick_s(lis, index+1, end)

def quick_sort(lis:list, n:int):
    _quick_s(lis, 0, n-1)
    pass

a = [4,7,2,8,3,9,1,6,0]
if __name__ == "__main__":
    quick_sort(a, len(a))
    print(a)

問題:
優(yōu)化:
2,隨機化,雙路,三路
3,其他問題

六,希爾排序(高級插入排序)

原理:
復(fù)雜度為O(n^ \frac{3}{2})

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容