排序算法

1. 冒泡排序

# coding=utf-8
'''冒泡排序
排序過程:
    沉澱法(比大,大的下沉):(visualgo上面的動態圖就是沉澱法)
        第 1 次從 頭 開始比較前後兩個數字的大小,最大的數沉澱在第 n 位置,比較n-1次;
        第 2 次從 頭 開始比較前後兩個數字的大小,次大的數沉澱在第 n-1 位置,比較n-2次;
        ...
        第 n-1 次從頭開始比較前後兩個數字的大小,次小的數沉澱在第 2 位置,比較1次;
        === 此時,最小數和次小數經過第n-1次比較,最小數已經在第1個位置了 ===
        第 n 次,該比較爲無意義的,子循環條件也證明了:range(1,1),爲空,沒有比較操作了,比較0次.

    冒泡法(比小,小的上浮):
        第 1 次從 最後 開始比較前後兩個數字的大小,最小的數浮在在第 1 位置,比較n-1次;
        第 2 次從 最後 開始比較前後兩個數字的大小,次小的數浮在在第 2 位置,比較n-2次;
        ...
        第 n-1 次從 最後 開始比較前後兩個數字的大小,次大數數浮在第 n-1 位置,比較1次;
        === 此時,最大數和次大數經過第n-1次比較,最大數已經被"浮"(沉澱)在第 n 個位置了 ===
        第 n 次,該比較爲無意義的,子循環條件也證明了:range(n-1,n-1),直接返回上一次的list,比較0次.

    所以,外層循環條件可以寫 range(n-1):循環總次數爲: n-1 + n-2 + ... + 1
        也可以寫range(n):循環總次數爲: n-1 + n-2 + ... + 1 + 0

    平均時間複雜度爲O(n^2);
    最好情況(順序)爲O(n);
    最壞情況(倒序)爲O(n^2):n-1 + n-2 + ... + 1=n(n-1)/2
    算法穩定.

    改進的bubble_sort,因爲冒泡排序的在當前子循環過程中,除了把當前最大數沉澱到最後,
    在比較的過程中,會對當前遍歷中的每個數進行比較,在到達最後的過程中,期間如果沒有任何
    交換操作,說明,當前遍歷的已經是順序的了,無需要在做後面的遍歷了.現對每個遍歷
    加一個標誌,什麼時候整個遍歷沒有交換操作,則整個排序終止.
'''

list = [5, 8, 1, 4, 2, 7, 3, 6]
list_ascended = [1, 2, 3, 4]
list_descended = [5, 4, 3, 2]

def bubble_sort(list):
    n = len(list)
    for i in range(n):  # 參數爲 n 話,i從0到n-1,一共遍歷 n 次
        print(list)
        flag = True
        # 向下沉澱
        # for j in range(1, n - i):  # 以1爲起點(從0開始比較),n-i爲終點,向後遍歷;
        #     if list[j - 1] > list[j]: # 每次子循環從最開始開始向下沉,沉澱到 n-i 爲止
        #         list[j - 1], list[j] = list[j], list[j - 1]
        # 向上冒泡
        for j in range(n - 1, i, -1):  # 以n-1爲起點,i爲終點,向前遍歷(j取值範圍爲[n-1,i+1])
            if list[j] < list[j - 1]:   # 每次子循環從最底下向上冒,冒到 i 爲止
                list[j - 1], list[j] = list[j], list[j - 1]
                flag = False  # 一次交換都沒有 flag 才能保持住原始的標誌
        if flag == True:
            return list
    return list

print(bubble_sort(list))
print('-----')
print(bubble_sort(list_ascended))
print('-----')
print(bubble_sort(list_descended))




# list = [5, 8, 1, 4]

# def bubble_sort(list):
#     n = len(list)
#     for i in range(n - 1):
#         print(list)
#         print('----')
#         print("i\t%d"%i)

#         # # 向下沉澱
#         for j in range(1, n - i):  # 每次子循環從最開始開始向下沉,沉澱到 n-i 爲止
#             if list[j - 1] > list[j]:
#                 list[j - 1], list[j] = list[j], list[j - 1]
#             print("j\t%d"%j)
#
#         # # 向上冒泡
#         # for j in range(n - 1, i, -1):  # 每次子循環從最底下向上冒,冒到 i 爲止
#         #     if list[j] < list[j - 1]:
#         #         list[j - 1], list[j] = list[j], list[j - 1]
#     return list

# print(bubble_sort(list))

2. 選擇排序

# coding=utf-8

'''注意:“交換”是個比較耗時的操作!'''
'''選擇排序

    選擇排序相比冒泡,大大減少了元素的交換次數.
    其實我們很容易發現,在還未完全確定當前最小元素之前,這些交換都是無意義的。
    我們可以通過設置一個變量min,然後每一次子循環中,僅存儲較小元素的數組下標,
    當前子循環結束之後,那這個變量存儲的就是當前子循環最小元素的下標,此時再執行交換操作即可。

    第 1 次遍歷 n-1 個數,找到最小的數,然後與第 1 個數交換;
    第 2 次遍歷 n-2 個數,找到最小的數,然後與第 2 個數交換;
    ... ...
    第 n-1 次遍歷 1 個數,找到最小的數,然後與第 n-1 個數交換;
    == 如果外層循環爲 range(n-1),則沒有第n次遍歷;
       如果外層爲 range(n),則有第n次遍歷,但是子循環條件爲 range(n,n),仍爲無效遍歷.==

    平均時間複雜度爲 O(n^2),該算法不穩定.
'''

list = [5, 8, 1, 4, 2, 7, 3, 6]
# list_ascended = [1, 2, 3, 4]
# list_descended = [5, 4, 3, 2]

def select_sort(list):
    n = len(list)
    for i in range(n):  # 參數爲 n 和 n-1 都可以,本質上都是 n-1 次遍歷
        min = i      # 記錄當前最小數的序號(位置)
        print(list)
        for j in range(i + 1, n):   # 以i+1爲起點,0爲終點,向後遍歷
            if list[j] < list[min]:
                min = j      # 記錄當前最小數的序號
        if min != i:      # 將當前最小數放在子循環的開頭
            list[min], list[i] = list[i], list[min]     # 每次只進行一次"交換"操作
    return list

print(select_sort(list))
# print('-----')
# print(select_sort(list_ascended))
# print('-----')
# print(select_sort(list_descended))

3. 插入排序

# coding=utf-8
'''插入排序(Insertion Sort)

    它的工作原理是通過構建有序序列,對於未排序數據,
    在已排序序列中從後向前掃描,找到相應位置並插入。

    步驟:
        1.從第一個元素開始,該元素可以認爲已經被排序
        2.取出下一個元素,在已經排序的元素序列中從後向前掃描
        3.如果該元素(已排序)大於新元素,將該元素移到下一位置
        4.重複步驟3,直到找到已排序的元素小於或者等於新元素的位置,並做插入到該位置後
        5.重複步驟2~4

    平均時間複雜度爲 O(n^2),該算法穩定.
    注:對於基本有序的數組,使用直接插入排序的效率是很高的.
'''


list = [5, 8, 1, 4, 2, 7, 3, 6]
# list_ascended = [1, 2, 3, 4]
# list_descended = [5, 4, 3, 2]

def insert_sort(list):
    n = len(list)
    for i in range(1, n):
        print(list)
        for j in range(i, 0, -1):  #  以i爲起點,0爲終點,向前遍歷
            if list[j] < list[j - 1]:
                list[j], list[j - 1] = list[j - 1], list[j]
            else:
                break
    return list

print(insert_sort(list))
# print('-----')
# print(insert_sort(list_ascended))
# print('-----')
# print(insert_sort(list_descended))

4. 希爾排序

# coding=utf-8

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

    “比較”在希爾排序中是最主要的操作,而不是“交換”。用這樣步長串行的希爾排序比插入排序和堆排序都要快,
    甚至在小數組中比快速排序還快,但是在涉及大量數據時希爾排序還是比快速排序慢。

    平均時間複雜度爲:O(nlogn)
    希爾排序是非穩定排序算法。
'''

# list = [8,7,6,5,4,13,2,1]
list = [5, 8, 1, 4, 2, 7, 3, 6]
# list_ascended = [1, 2, 3, 4]
# list_descended = [5, 4, 3, 2]

def shell_sort(list):
    step = len(list) // 2
    while step > 0:
        # print("step=%d"%step)
        # print(list)
        for i in range(step, len(list)):
            # 類似插入排序, 當前值與指定步長之前的值比較, 符合條件則交換位置
            while i >= step and list[i - step] > list[i]:
                list[i], list[i - step] = list[i - step], list[i]
                i -= step    # 這裏類似插入排序要逐步向前比較插入,每次跳過step的步長
            # print(list)
        step //= 2

    return list

print(shell_sort(list))

5. 歸併排序

# coding=utf-8
'''歸併排序
    歸併排序(Merge sort,臺灣譯作:合併排序)是建立在歸併操作上的一種有效的排序算法。
    該算法是採用分治法(Divide and Conquer)的一個非常典型的應用

    步驟:
        1.申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列
        2.設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置
        3.比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置
        4.重複步驟3直到某一指針達到序列尾
        5.將另一序列剩下的所有元素直接複製到合併序列尾

    算法特點:
        可以用一個例子來體會一下假如有這樣一個數組{ 3,7,2,5,1,0,4,6 },
        冒泡和選擇排序的比較次數是25次。直接插入排序用了15次。
        而歸併排序的次數是相對穩定的,由我們上面提到的比較次數的計算方法,
        我們的例子要合併4對長度爲1的,2對長度爲2的,和1對長度爲4的。
        歸併排序的最多的比較次數爲4 * 1 + 2 * 3 + 7 = 17次。

        因爲元素的隨機性,直接插入排序也可能是相當悲劇的,歸併排序在比較次數上的優勢。

    將數列分開成小數列一共要 logn 步,每一步合併有序數列(調用一次 merge),複雜度爲O(n)
    歸併排序平均時間複雜度爲O(nlogn),是一種穩定的算法.
'''

# list = [5, 8, 1, 4, 2, 7, 3, 6]
list = [8, 7, 6, 5, 4, 13, 2, 1]

# 合併數列
def merge(left, right):
    list = []
    i, j = 0, 0
    while i < len(left) and j < len(right):
        if left[i] < right[j]:
            list.append(left[i])
            i += 1
        else:
            list.append(right[j])
            j += 1
    # 把比較後,較長的 list剩下來的部分接上去
    list += left[i:]
    list += right[j:]
    return list

# 遞歸的方法從最小單元開始拆分,合併
def merge_sort(list):
    if len(list) <= 1:
        return list
    num = len(list) // 2
    left = merge_sort(list[:num])  # 遞歸調用,左半部分排好序
    right = merge_sort(list[num:])  # 遞歸調用,右半部分排好序
    # print('list:%s'%list)
    # print('left:%s'%left)
    # print('right:%s'%right)
    # print('merge:%s'%merge(left, right))
    # print('------')
    return merge(left, right)

print(merge_sort(list))

6. 快速排序

# coding=utf-8

'''快速排序
    它採用了一種分治的策略,通常稱其爲分治法(Divide-and-ConquerMethod)。

該方法的基本思想是:
    1. 從數列中挑出一個元素,稱爲 “基準”(pivot).
    2. 分區操作(partition):重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。
        在這個分區退出之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作。
    3. 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。

快排的特點:
    元素的移動效率高了,儘量讓一次移動,爲後面的操作服務.
            ——高效的排序算法對元素的移動效率都是比較高的。

時間複雜度:
    第一個關鍵字正好是待排序的序列的中間值(比如1到8中的5),因此遞歸樹是平衡的,
    此時性能也比較好。此時時間複雜度是 O(nlogn)
    在最壞狀況下則需要Ο(n^2)次比較(如原始是咧爲倒序倒序),此時的遞歸樹是一棵嚴重偏斜的樹。

穩定性:
    由於關鍵字的比較和交換是跳躍進行的,因此,快速排序是一種不穩定的排序方法。
    參考:http://book.51cto.com/art/201108/287089.htm
'''

list = [5, 8, 1, 4, 2, 7, 3, 6]
# list = [8, 7, 6, 5, 4, 3, 2, 1]

def quick_sort(list, left, right):

    if left >= right:    # 當前分區排序的停止條件,返回的數列爲 整個list(而不是分區中的部分數列的,但是發生變化的是分區中的部分數列)
        return list      #     當前分區間內操作完成後,小於基數的數都在基數左邊,大於基數的數都在基數右邊

    start = left    # start 記錄當前排序分區的開始位置
    end = right   # end 記錄當前分區排序的停止位置
    key = list[start]   # 每次分區開始後,以第一個數爲比較的基數
    # key = list[(start + end)//2]

    while left < right:
        # 右指針向前移動,直到找到比基數大的數,則跳出循環,並將該數賦值給左指針所指位置————
        # 此時,右指針相當於在這個位置挖了個坑,隨時等着左指針找一個小於基數的數來填上這個本來比基數大的數的位置.
        while left < right and list[right] >= key:
            right -= 1
        list[left] = list[right]

        # 左指針向後移動,直到找到一個大於基數的數,則跳出循環,並賦值給當前右指針所停留的位置————
        # 即補上上次右指針在右邊挖的坑,但賦值的同時,相當於左指針給自己當前位置又挖了個坑,再次等右指針的大數來填這個坑
        while left < right and list[left] <= key:
            left += 1
        list[right] = list[left]

    # 在左右相互填坑的遊戲中,如果左右指針重合了——表示左指針左邊的數都比基數小,右指針右邊的數都比基數大
    list[right] = key   # 則最終這個坑由基數來填上,此時本輪填坑遊戲結束,並在進去下個分區排序的流程前,通過 return返回list.

    quick_sort(list, start, left - 1)
    quick_sort(list, left + 1, end)

    return list

print('排序結果爲:')
print(quick_sort(list, 0, len(list) - 1))

7. 堆排序

# coding=utf-8
'''堆排序:
堆的概念:
    若以一維數組存儲一個堆,則堆對應一棵完全二叉樹,且所有非葉結點的值
    均不大於(或不小於)其子女的值,根結點(堆頂元素)的值是最小(或最大)的.

堆排序的過程:
    1.初始時把要排序的n個數的序列看作是一棵順序存儲的二叉樹(一維數組存儲二叉樹),
    調整它們的存儲序,使之成爲一個堆,將堆頂元素輸出,得到n個元素中最小的元素;
    2.然後對前面(n-1)個元素重新調整使之成爲堆,輸出堆頂元素;
    3.重複1,2,直到只有兩個節點的堆,並對它們作交換,最後得到有n個節點的有序序列。

穩定性:
    不穩定

時間複雜度:
    nlogn
'''

# list = [9, 8, 7, 6, 5, 4, 3, 2, 1]
list = [50, 80, 10, 40, 20, 70, 30, 60]


# 堆排序,整個過程就是不斷地從堆頂移除,調整最大數到堆頂,重複len(list)次
def heap_sort(list):
    size = len(list)
    build_heap(list, size)
    for size_i in range(size)[::-1]:
        list[0], list[size_i] = list[size_i], list[0]   # 將放在堆頂的當前最大的數推出(放在最後一個當前遍歷的最後一個位置,並將當前堆中的最後一個數放在堆頂.)
        adjust_heap(list, 0, size_i)   # 注意此時size=i,下一個迭代中整棵樹截斷了已經排好當前最大數,並且每次都是從堆頂開始進行下一次遍歷
    return list

# 創建堆:原始序列的堆,經過所有非葉子節點從後向前的調用adjust(遞歸方式,注意max的跟蹤),保證調整後的堆爲"大頂堆"
def build_heap(list, size):
    for none_leaf in range(size // 2)[::-1]:   # size//2 爲最後一個非葉子節點的序號
        adjust_heap(list, none_leaf, size)

# 調整堆,使得大小爲size_i的樹爲"大頂堆"
def adjust_heap(list, parent, size):
    lchild = 2 * parent + 1
    rchild = 2 * parent + 2
    if lchild < size:   # 如果當前節點是非葉子節點(還有孩子),纔有繼續遞歸調整堆的必要
        max = parent   # max 記錄了當前子樹(三個節點)中最大的數的序號
        if lchild < size and list[lchild] > list[max]:
            max = lchild
        if rchild < size and list[rchild] > list[max]:
            max = rchild
        if max != parent:
            # 將當前最大數(孩子節點所在位置)和parent節點調換位置,以保證parent節點上的數比孩子節點都大
            list[max], list[parent] = list[parent], list[max]
            # 當前max經過上一步的調換,爲上個parent節點的所在的數,如果還有孩子節點則繼續adjust
            adjust_heap(list, max, size)


print("排序之後:%s" %heap_sort(list))

8. 基數排序

# coding=utf-8

'''基數排序:
原理類似桶排序,這裏總是需要10個桶,多次使用.
首先以個位數的值進行裝桶,即個位數爲1則放入1號桶,爲9則放入9號桶,暫時忽視十位數
例如,待排序數組[62,14,59,88,16]
分配10個桶,桶編號爲0-9,以個位數數字爲桶編號依次入桶,變成下邊這樣
| 0 | 0 | 62 | 0 | 14 | 0 | 16 | 0 | 88 | 59 |
| 0 | 1 |  2 | 3 |  4 | 5 |  6 | 7 |  8 |  9 |——桶編號

將桶裏的數字順序取出來,輸出結果:[62,14,16,88,59]
再次入桶,不過這次以十位數的數字爲準,進入相應的桶,變成下邊這樣:

由於前邊做了個位數的排序,所以當十位數相等時,個位數字是由小到大的順序入桶的,
就是說,入完桶還是有序:
| 0 | 14,16 | 0 | 0 | 0 | 59 | 62 | 0 | 88 | 0 |
| 0 | 1     | 2 | 3 | 4 | 5  | 6  | 7 | 8  | 9 |——桶編號

穩定性:
    基數排序法是屬於穩定性的排序,在某些時候,基數排序法的效率高於其它的穩定性排序法。
效率:
    其時間複雜度爲O (nlog(r)m),其中r爲所採取的基數,而m爲堆數.
'''

import math
list = [14, 16, 59, 62, 188 ,276, 533, 64]

def radix_sort(list, radix=10):  # 基數不僅可以以10爲基,還可以以其他數爲基
    # k 記錄了整數的最高位
    k = int(math.ceil(math.log(max(list), radix)))  # log 函數的默認底數爲e,這裏設置爲10;ceil返回大於參數的最小數,爲浮點型
    bucket = [[] for _ in range(radix)]
    for i in range(k):

        for j in list:
            i_int = int(j / (radix ** i) )  # 將小數點移動到當前比較的數位後
            bucket[i_int % radix].append(j)  # 再以基數取餘,得到當前位上的數字;並裝入對應的桶中
        del list[:]  # 清空列表

        # 將這次桶裝的數字按照當前順序放入list,爲下一次入桶準備
        for s in bucket:
            list += s
            del s[:]
    return list


print("排序之後:%s" % radix_sort(list)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章