八大經典排序-python實現

Table of Contents

簡介

一、冒泡排序

二、選擇排序

三、插入排序

四、希爾排序

五、歸併排序

六、快速排序

七、堆排序

八、計數排序


本博客只用於自身學習,如有錯誤,虛心求教!!!

簡介

關於時間複雜度:

  1. 平方階 (O(n2)) 排序:各類簡單排序:直接插入、直接選擇和冒泡排序。
  2. 線性對數階 (O(nlog2n)) 排序:快速排序、堆排序和歸併排序;
  3. O(n1+§)) 排序,§ 是介於 0 和 1 之間的常數: 希爾排序
  4. 線性階 (O(n)) 排序: 基數排序,此外還有桶、箱排序。

關於穩定性:

穩定的排序算法:冒泡排序、插入排序、歸併排序和基數排序。

不是穩定的排序算法:選擇排序、快速排序、希爾排序、堆排序。

名詞解釋:

n:數據規模

k:「桶」的個數

In-place:佔用常數內存,不佔用額外內存

Out-place:佔用額外內存

穩定性:排序後 2 個相等鍵值的順序和排序之前它們的順序相同

一、冒泡排序

 

 

冒泡排序(Bubble Sort)也是一種簡單直觀的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢「浮」到數列的頂端。

作爲最簡單的排序算法之一,冒泡排序給我的感覺就像 Abandon 在單詞書裏出現的感覺一樣,每次都在第一頁第一位,所以最熟悉。冒泡排序還有一種優化算法,就是立一個 flag,當在一趟序列遍歷中元素沒有發生交換,則證明該序列已經有序。但這種改進對於提升性能來說並沒有什麼太大作用。

1. 算法步驟

  1. 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
  2. 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
  3. 針對所有的元素重複以上的步驟,除了最後一個。
  4. 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

2. 動圖演示

å¨å¾æ¼ç¤º

3.python代碼實現

# 冒泡排序
def bubble_sort(alist):
    # i 控制總共交換的輪數(每輪選出一個最大的移動最後)
    # if len(alist) = 9 的話,只需要 8 輪交換即可
    for i in range(len(alist)-1):
        print('##############, i = %d' %i)
        # j控制: 選出最大的, 將最大移到最後
        for j in range(len(alist)-1-i):
            if(alist[j] > alist[j+1]):  # 前面 > 後面  交換
                alist[j], alist[j+1] = alist[j+1], alist[j]
            print(alist)

out:

原列表爲:[54, 26, 93, 18, 17, 31, 44, 55, 20]
##############, i = 0  # 選出最大的 93 移到最後
[26, 54, 93, 18, 17, 31, 44, 55, 20]
[26, 54, 93, 18, 17, 31, 44, 55, 20]
[26, 54, 18, 93, 17, 31, 44, 55, 20]
[26, 54, 18, 17, 93, 31, 44, 55, 20]
[26, 54, 18, 17, 31, 93, 44, 55, 20]
[26, 54, 18, 17, 31, 44, 93, 55, 20]
[26, 54, 18, 17, 31, 44, 55, 93, 20]
[26, 54, 18, 17, 31, 44, 55, 20, 93]
##############, i = 1  # # 選出次大的 55
[26, 54, 18, 17, 31, 44, 55, 20, 93]
[26, 18, 54, 17, 31, 44, 55, 20, 93]
[26, 18, 17, 54, 31, 44, 55, 20, 93]
[26, 18, 17, 31, 54, 44, 55, 20, 93]
[26, 18, 17, 31, 44, 54, 55, 20, 93]
[26, 18, 17, 31, 44, 54, 55, 20, 93]
[26, 18, 17, 31, 44, 54, 20, 55, 93]
##############, i = 2
[18, 26, 17, 31, 44, 54, 20, 55, 93]
[18, 17, 26, 31, 44, 54, 20, 55, 93]
[18, 17, 26, 31, 44, 54, 20, 55, 93]
[18, 17, 26, 31, 44, 54, 20, 55, 93]
[18, 17, 26, 31, 44, 54, 20, 55, 93]
[18, 17, 26, 31, 44, 20, 54, 55, 93]
##############, i = 3
[17, 18, 26, 31, 44, 20, 54, 55, 93]
[17, 18, 26, 31, 44, 20, 54, 55, 93]
[17, 18, 26, 31, 44, 20, 54, 55, 93]
[17, 18, 26, 31, 44, 20, 54, 55, 93]
[17, 18, 26, 31, 20, 44, 54, 55, 93]
##############, i = 4
[17, 18, 26, 31, 20, 44, 54, 55, 93]
[17, 18, 26, 31, 20, 44, 54, 55, 93]
[17, 18, 26, 31, 20, 44, 54, 55, 93]
[17, 18, 26, 20, 31, 44, 54, 55, 93]
##############, i = 5
[17, 18, 26, 20, 31, 44, 54, 55, 93]
[17, 18, 26, 20, 31, 44, 54, 55, 93]
[17, 18, 20, 26, 31, 44, 54, 55, 93]
##############, i = 6
[17, 18, 20, 26, 31, 44, 54, 55, 93]
[17, 18, 20, 26, 31, 44, 54, 55, 93]
##############, i = 7
[17, 18, 20, 26, 31, 44, 54, 55, 93]
新列表爲:[17, 18, 20, 26, 31, 44, 54, 55, 93]

二、選擇排序

選擇排序是一種簡單直觀的排序算法,無論什麼數據進去都是 O(n²) 的時間複雜度。所以用到它的時候,數據規模越小越好。唯一的好處可能就是不佔用額外的內存空間了吧。

1. 算法步驟

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。
  3. 重複第二步,直到所有元素均排序完畢。

2. 動圖演示

3. Python 代碼實現

 

def select_sort(alist):
    for i in range(len(alist)-1):
        min = i
        # 在未排序中 選出最小的
        for j in range(i+1, len(alist)):
            if(alist[j] < alist[min]):
                min = j
        alist[min], alist[i] = alist[i], alist[min]

out:

原列表爲:[54, 26, 93, 18, 17, 31, 44, 55, 20]
[17, 26, 93, 18, 54, 31, 44, 55, 20]
[17, 18, 93, 26, 54, 31, 44, 55, 20]
[17, 18, 20, 26, 54, 31, 44, 55, 93]
[17, 18, 20, 26, 54, 31, 44, 55, 93]
[17, 18, 20, 26, 31, 54, 44, 55, 93]
[17, 18, 20, 26, 31, 44, 54, 55, 93]
[17, 18, 20, 26, 31, 44, 54, 55, 93]
[17, 18, 20, 26, 31, 44, 54, 55, 93]
新列表爲:[17, 18, 20, 26, 31, 44, 54, 55, 93]

三、插入排序

插入排序的代碼實現雖然沒有冒泡排序和選擇排序那麼簡單粗暴,但它的原理應該是最容易理解的了,因爲只要打過撲克牌的人都應該能夠秒懂。插入排序是一種最簡單直觀的排序算法,它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。

插入排序和冒泡排序一樣,也有一種優化算法,叫做拆半插入。

1. 算法步驟

  1. 將第一待排序序列第一個元素看做一個有序序列,把第二個元素到最後一個元素當成是未排序序列。
  2. 從頭到尾依次掃描未排序序列,將掃描到的每個元素插入有序序列的適當位置。(如果待插入的元素與有序序列中的某個元素相等,則將待插入元素插入到相等元素的後面。)

2. 動圖演示

å¨å¾æ¼ç¤º

 

3. Python 代碼實現

def insert_sort(alist):
    # i控制的是 未排序數組(角標)
    for i in range(1, len(alist)):
        # j 控制將拿到的元素放到前面有序序列中正確位置的過程
        for j in range(i, 0, -1):
            # 如果比前面的元素小,則往前移動
            if alist[j] < alist[j - 1]:
                alist[j], alist[j - 1] = alist[j - 1], alist[j]
            # 否則代表比前面的所有元素都小,不需要再移動
            else:
                break

out:

原列表爲:[54, 26, 93, 18, 17, 31, 44, 55, 20]
[26, 54, 93, 18, 17, 31, 44, 55, 20]
[26, 54, 93, 18, 17, 31, 44, 55, 20]
[18, 26, 54, 93, 17, 31, 44, 55, 20]
[17, 18, 26, 54, 93, 31, 44, 55, 20]
[17, 18, 26, 31, 54, 93, 44, 55, 20]
[17, 18, 26, 31, 44, 54, 93, 55, 20]
[17, 18, 26, 31, 44, 54, 55, 93, 20]
[17, 18, 20, 26, 31, 44, 54, 55, 93]
新列表爲:[17, 18, 20, 26, 31, 44, 54, 55, 93]

四、希爾排序

希爾排序,也稱遞減增量排序算法,是插入排序的一種更高效的改進版本。但希爾排序是非穩定排序算法。

希爾排序是基於插入排序的以下兩點性質而提出改進方法的:

  • 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率;
  • 但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位;

希爾排序的基本思想是:先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄「基本有序」時,再對全體記錄進行依次直接插入排序。

1. 算法步驟

  1. 選擇一個增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  2. 按增量序列個數 k,對序列進行 k 趟排序;
  3. 每趟排序,根據對應的增量 ti,將待排序列分割成若干長度爲 m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲 1 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。

2. 排序過程

希爾排序的基本思想是:將數組列在一個表中並對列分別進行插入排序,重複這過程,不過每次用更長的列(步長更長了,列數更少了)來進行。最後整個表就只有一列了。將數組轉換至表是爲了更好地理解這算法,算法本身還是使用數組進行排序。

 

例如,假設有這樣一組數[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我們以步長爲5開始進行排序,我們可以通過將這列表放在有5列的表中來更好地描述算法,這樣他們就應該看起來是這樣(豎着的元素是步長組成):

13 14 94 33 82

25 59 94 65 23

45 27 73 25 39

10

 

然後我們對每列進行排序:

10 14 73 25 23

13 27 94 33 39

25 59 94 65 82

45

 

將上述四行數字,依序接在一起時我們得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ]。這時10已經移至正確位置了,然後再以3爲步長進行排序:

 

10 14 73

25 23 13

27 94 33

39 25 59

94 65 82

45

 

排序之後變爲:

10 14 13

25 23 33

27 25 59

39 65 73

45 94 82

94

最後以1步長進行排序(此時就是簡單的插入排序了)

3. Python 代碼實現

# 希爾排序
def shell_sort(alist):
    '''
        分組之後更容易理解,對每列進行插入排序
    '''
    # 初始步長
    gap = len(alist) // 2
    while gap > 0:
        print('###############,gap爲:%d' %gap)
        for i in range(gap, len(alist)):
            # 每個步長進行插入排序
            temp = alist[i]
            j = i
            # 插入排序 (分組,每組發生交換的執行)
            while j >= gap and alist[j - gap] > temp:
                alist[j] = alist[j - gap]
                j -= gap
            alist[j] = temp
            print(alist)
       
        # 得到新的步長
        gap = gap // 2

out:

原列表爲:[54, 26, 93, 18, 17, 31, 44, 55, 20]
###############,gap爲:4
[17, 26, 93, 18, 54, 31, 44, 55, 20]  # 54 > 17 交換
[17, 26, 93, 18, 54, 31, 44, 55, 20]  # 26 < 31 不交換
[17, 26, 44, 18, 54, 31, 93, 55, 20]  # 93 > 44 交換
[17, 26, 44, 18, 54, 31, 93, 55, 20]  # 18 < 55 不交換
[17, 26, 44, 18, 20, 31, 93, 55, 54]  # 17 > 20 > 54 交換
###############,gap爲:2
[17, 26, 44, 18, 20, 31, 93, 55, 54]
[17, 18, 44, 26, 20, 31, 93, 55, 54]
[17, 18, 20, 26, 44, 31, 93, 55, 54]
[17, 18, 20, 26, 44, 31, 93, 55, 54]
[17, 18, 20, 26, 44, 31, 93, 55, 54]
[17, 18, 20, 26, 44, 31, 93, 55, 54]
[17, 18, 20, 26, 44, 31, 54, 55, 93]
###############,gap爲:1
[17, 18, 20, 26, 44, 31, 54, 55, 93]
[17, 18, 20, 26, 44, 31, 54, 55, 93]
[17, 18, 20, 26, 44, 31, 54, 55, 93]
[17, 18, 20, 26, 44, 31, 54, 55, 93]
[17, 18, 20, 26, 31, 44, 54, 55, 93]
[17, 18, 20, 26, 31, 44, 54, 55, 93]
[17, 18, 20, 26, 31, 44, 54, 55, 93]
[17, 18, 20, 26, 31, 44, 54, 55, 93]
新列表爲:[17, 18, 20, 26, 31, 44, 54, 55, 93]
[Finished in 0.0s]

五、歸併排序

歸併排序(Merge sort)是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。且各層分治遞歸可以同時進行。

採用 divide-and-conquer approach:

Divide: 把長度爲 n 的 array 分成兩半。

Conquer: 把左半邊 array 及右半邊 array 分別以 Merge Sort 進行 sorting。 (recursive)

Combine: merge 左半邊及右半邊 sorted array。

作爲一種典型的分而治之思想的算法應用,歸併排序的實現由兩種方法:

  1. 自上而下的遞歸(所有遞歸的方法都可以用迭代重寫,所以就有了第 2 種方法);
  2. 自下而上的迭代;

1. 算法步驟

遞歸法(Top-down)

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

迭代法(Bottom-up)

2. 動圖演示

 

å¨å¾æ¼ç¤º

當最左邊的分到最細之後無法再劃分左右然後開始進行合併。

第一次組合完成[4, 7]的合併

第二次組合完成[4, 7, 8]的合併

第三次組合完成[3, 5]的合併

第四次組合完成[3, 5, 9]的合併

第五次組合完成[3, 4, 5, 7, 8, 9]的合併結束排序。

3. Python 代碼實現

非遞歸實現:

def merge_sort(alist):
    for i in range(1, len(alist)):
        for low in range(0, len(alist)):
            mid = low + i
            height = min(low + 2 * i, len(alist))
            if mid < height:
                left = seq[low:mid]
                right = seq[mid:height]
                alist[low:height] = merge(left, right)
            low += 2 * i
        i *= 2
    return alist

def merge(left, right):
    result = []
    while left and right:
        if left[0] <= right[0]:
            result.append(left.pop(0))
        else:
            result.append(right.pop(0))

    if left:
        result += left
    if right:
        result += right
    return result
    
if __name__ == '__main__':
    seq = [5, 4, 3, 0, 1, 2, 7, 5, 11,9]
    print(merge_sort(seq))

遞歸實現:

# 合併操作——遞歸實現
def merge_sort(alist):
    if len(alist) <= 1:
        return alist
    mid = len(alist) // 2
    left = alist[:mid]
    right = alist[mid:]

    left = merge_sort(left)
   right = merge_sort(right)  
   return merge(left, right)

def merge(left, right):
        result = []
        while left and right:
            if left[0] <= right[0]:
                result.append(left.pop(0))
            else:
                result.append(right.pop(0))

        if left:
            result += left
        if right:
            result += right
        return result

六、快速排序

快速排序是由東尼·霍爾所發展的一種排序算法。在平均狀況下,排序 n 個項目要 Ο(nlogn) 次比較。在最壞狀況下則需要 Ο(n2) 次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他 Ο(nlogn) 算法更快,因爲它的內部循環(inner loop)可以在大部分的架構上很有效率地被實現出來。

 

快速排序使用分治法(Divide and conquer)策略來把一個串行(list)分爲兩個子串行(sub-lists)。

 

快速排序又是一種分而治之思想在排序算法上的典型應用。本質上來看,快速排序應該算是在冒泡排序基礎上的遞歸分治法。

快速排序的名字起的是簡單粗暴,因爲一聽到這個名字你就知道它存在的意義,就是快,而且效率高!它是處理大數據最快的排序算法之一了。雖然 Worst Case 的時間複雜度達到了 O(n²),但是人家就是優秀,在大多數情況下都比平均時間複雜度爲 O(n logn) 的排序算法表現要更好,可是這是爲什麼呢,我也不知道。好在我的強迫症又犯了,查了 N 多資料終於在《算法藝術與信息學競賽》上找到了滿意的答案:

快速排序的最壞運行情況是 O(n²),比如說順序數列的快排。但它的平攤期望時間是 O(nlogn),且 O(nlogn) 記號中隱含的常數因子很小,比複雜度穩定等於 O(nlogn) 的歸併排序要小很多。所以,對絕大多數順序性較弱的隨機數列而言,快速排序總是優於歸併排序。

1. 算法步驟

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

遞歸的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞歸下去,但是這個算法總會退出,因爲在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。

2. 動圖演示

å¨å¾æ¼ç¤º

快速排序採用的是一種分而治之的思想

平均時間複雜度:O(nlgn)

最差時間複雜度:O(n^2)

3. Python代碼實現

# 快排算法
def quick_sort(alist):
    	if len(alist) < 2:  # 基線條件
    		return alist
    	else: # 遞歸條件
    		pivot = alist[0]  
    		less = [i for i in alist[1:] if i <= alist[0]]
    		greater = [i for i in alist[1:] if i > alist[0]]
    		print(less + [pivot] + greater)
        return quick_sort(less) + [pivot] + quick_sort(greater)

out:

alist = [11, 3, 14, 5, 21, 2, 2]
print('alist快速排序之後:', quick_sort(alist))
# out:
[3, 5, 2, 2, 11, 14, 21]
[2, 2, 3, 5]
[2, 2]
[14, 21]
alist快速排序之後: [2, 2, 3, 5, 11, 14, 21]

解法2:

#coding:utf-8
def quick_sort(lists, left, right):
    '''快速排序'''
    # 跳出遞歸判斷
    if left >= right:
        return lists
 
    # 選擇參考點,該調整範圍的第1個值
    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[left] = key
 
    # 遞歸,並返回結果
    quick_sort(lists, low, left - 1)    # 遞歸左邊部分
    quick_sort(lists, left + 1, high)   # 遞歸右邊部分
    return lists

解法2-另寫

def partition(alist, left, right):
    '''快速排序'''
    # 選擇參考點,該調整範圍的第1個值
    key = alist[left]

    # 循環判斷直到遍歷全部
    while left < right:
        # 從右邊開始查找大於(實際爲小於)參考點的值
        while left < right and alist[right] >= key:
            right -= 1
        alist[left] = alist[right]  # 這個位置的值先挪到左邊

        # 從左邊開始查找小於(實際爲大於)參考點的值
        while left < right and alist[left] < key:
            left += 1
        alist[right] = alist[left]  # 這個位置的值挪到右邊

    # 寫回改成的值
    alist[left] = key
    return left

def  quick_sort(alist, left, right):
    if left >= right:
        return alist
    j = partition(alist, left, right)
    quick_sort(alist, left, j - 1)  # 遞歸左邊部分
    quick_sort(alist, j + 1, right)  # 遞歸右邊部分
    return alist

if __name__ == '__main__':
    seq = [5, 4, 3, 0, 1, 2, 7, 5, 11,9]
    print(quick_sort(seq, 0, len(seq)-1))

解法2-另寫

def partition(alist, left, right):
    '''快速排序'''
    # 選擇參考點,該調整範圍的第1個值
    key = left

    # 循環判斷直到遍歷全部
    while left < right:
        # 從右邊開始查找大於(實際爲小於)參考點的值
        while left < right and alist[right] >= key:
            right -= 1
        #alist[left] = alist[right]  # 這個位置的值先挪到左邊

        # 從左邊開始查找小於(實際爲大於)參考點的值
        while left < right and alist[left] < key:
            left += 1
        #alist[right] = alist[left]  # 這個位置的值挪到右邊
        alist[left], alist[right] = alist[right], alist[left]
    # 寫回改成的值
    #alist[left] = key
    alist[left], alist[key] = alist[key], alist[left]
    return left

def  quick_sort(alist, left, right):
    if left >= right:
        return alist
    j = partition(alist, left, right)
    quick_sort(alist, left, j - 1)  # 遞歸左邊部分
    quick_sort(alist, j + 1, right)  # 遞歸右邊部分
    return alist

if __name__ == '__main__':
    seq = [5, 4, 3, 0, 1, 2, 7, 5, 11,9]
    print(quick_sort(seq, 0, len(seq)-1))

快排的平均情況和最糟情況

七、堆排序

參考:https://blog.csdn.net/minxihou/article/details/51850001

https://blog.csdn.net/dujiahaogod/article/details/79688331

堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。堆排序可以說是一種利用堆的概念來排序的選擇排序。分爲兩種方法:

  1. 大頂堆:每個節點的值都大於或等於其子節點的值,在堆排序算法中用於升序排列;
  2. 小頂堆:每個節點的值都小於或等於其子節點的值,在堆排序算法中用於降序排列;

堆排序的平均時間複雜度爲 Ο(nlogn)。


其實我們的堆排序算法就是抓住了堆的這一特點,每次都取堆頂的元素,將其放在序列最後面,然後將剩餘的元素重新調整爲最大堆,依次類推,最終得到排序的序列。

  1. 將初始待排序關鍵字序列(R1,R2....Rn)構建成大頂堆,此堆爲初始的無序區
  2. 將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區(R1,R2,......Rn-1)和新的有序區(Rn)
  3. 由於交換後新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,......Rn-1)調整爲新堆,然後再次將R[1]與無序區最後一個元素交換,得到新的無序區(R1,R2....Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數爲n-1,則整個排序過程完成

堆可以分爲大根堆和小根堆,這裏用最大堆的情況來定義操作:

(1)最大堆調整(MAX_Heapify):將堆的末端子節點作調整,使得子節點永遠小於父節點。這是核心步驟,在建堆和堆排序都會用到。比較i的根節點和與其所對應i的孩子節點的值。當i根節點的值比左孩子節點的值要小的時候,就把i根節點和左孩子節點所對應的值交換,當i根節點的值比右孩子的節點所對應的值要小的時候,就把i根節點和右孩子節點所對應的值交換。然後再調用堆調整這個過程,可見這是一個遞歸的過程。

(2)建立最大堆(Build_Max_Heap):將堆所有數據重新排序。建堆的過程其實就是不斷做最大堆調整的過程,從len/2出開始調整,一直比到第一個節點。

(3)堆排序(HeapSort):移除位在第一個數據的根節點,並做最大堆調整的遞歸運算。堆排序是利用建堆和堆調整來進行的。首先先建堆,然後將堆的根節點選出與最後一個節點進行交換,然後將前面len-1個節點繼續做堆調整的過程。直到將所有的節點取出,對於n個數我們只需要做n-1次操作。


堆排序,顧名思義,就是基於堆。因此先來介紹一下堆的概念。 

堆分爲最大堆和最小堆,其實就是完全二叉樹。最大堆要求節點的元素都要大於其孩子,最小堆要求節點元素都小於其左右孩子,兩者對左右孩子的大小關係不做任何要求,其實很好理解。有了上面的定義,我們可以得知,處於最大堆的根節點的元素一定是這個堆中的最大值。其實我們的堆排序算法就是抓住了堆的這一特點,每次都取堆頂的元素,將其放在序列最後面,然後將剩餘的元素重新調整爲最大堆,依次類推,最終得到排序的序列。


節點 i 的左孩子索引: 2*i+1 

節點 i 的右孩子索引: 2*i+2 

結點 i 的父節點索引 :(i-1)/2


1. 算法步驟

  1. 創建一個堆 H[0……n-1];
  2. 把堆首(最大值)和堆尾互換;
  3. 把堆的尺寸縮小 1,並調用 shift_down(0),目的是把新的數組頂端數據調整到相應位置;
  4. 重複步驟 2,直到堆的尺寸爲 1。

堆排序就是把堆頂的最大數取出, 

將剩餘的堆繼續調整爲最大堆,以遞歸實現 

剩餘部分調整爲最大堆後,再次將堆頂的最大數取出,再將剩餘部分調整爲最大堆,這個過程持續到剩餘數只有一個時結束


在堆的數據結構中,堆中的最大值總是位於根節點。堆中定義以下幾種操作:

  • 最大堆調整(Max Heapify):將堆的末端子節點作調整,使得子節點永遠小於父節點
  • 創建最大堆(Build Max Heap):將堆中的所有數據重新排序
  • 堆排序(HeapSort):移除位在第一個數據的根節點,並做最大堆調整的遞歸運算

2. 動圖演示

另一個堆排序動畫展示:https://bajdcc.github.io/html/heap.html

3. Python代碼實現(畫樹狀圖容易理解)

版本1

def heap_sort(lst):
    #生成最大堆
    for start in range(len(lst)//2-1, -1, -1):
        shift_heap(lst, start, len(lst)-1)
    #最大堆調整
    for end in range(len(lst)-1, 0, -1):
        lst[0], lst[end] = lst[end], lst[0]
        shift_heap(lst, 0, end - 1)
    return lst

def shift_heap(lst, start, end):
    root = start
    while True:
    	child = root * 2 + 1
        if child > end:
            break
        if child + 1 <= end and lst[child] < lst[child + 1]:
            child = child + 1
        if lst[root] < lst[child]:
            lst[root], lst[child] = lst[child], lst[root]
            root = child
        else:
            break

lst = [1,2,3,5,4,2,3,5,6]
# lst = [3,3,2,2,1,4,1,4]
print(heap_sort(lst))

版本2

# 堆排序
def heap_sort(alist):
    def max_heapify(start, end):
        """最大堆調整"""
        root = start
        while True:
            child = 2 * root + 1
            # child角標 超出角標最大值
            if child > end: 
                break

            # 左節點 < 右節點 child改爲右節點角標
            if child + 1 <= end and alist[child] < alist[child + 1]: 
                child += 1 

            # 根節點值 < child節點值  交換
            if alist[root] < alist[child]: 
                alist[root], alist[child] = alist[child], alist[root]
                root = child
            else:
                break

# 創建最大堆(保證了 根節點 大於 子節點!!!!!!!!!!!)
     # (len(alist)-2)//2 = len(alist)//2 - 1 爲根節點,從最底層根節點開始
    for start in range((len(alist) - 2) // 2, -1, -1):
        max_heapify(start, len(alist) - 1)
        print(alist)
# 堆排序
    for end in range(len(alist) - 1, 0, -1):
        alist[0], alist[end] = alist[end], alist[0]
        max_heapify(0, end - 1)
        print('########')
        print(alist)
    return alist

out:

原列表爲:[54, 26, 93, 18, 17, 31, 44, 55, 20]
[54, 26, 93, 55, 17, 31, 44, 18, 20] #座標 3<->7 換,18<->55
[54, 26, 93, 55, 17, 31, 44, 18, 20] #座標 2<->5 換,
[54, 55, 93, 26, 17, 31, 44, 18, 20] #座標 1<->3 換,26<->55
[93, 55, 54, 26, 17, 31, 44, 18, 20] #座標 0<->2 換,54<->93
########
[55, 26, 54, 20, 17, 31, 44, 18, 93]
########
[54, 26, 44, 20, 17, 31, 18, 55, 93]
########
[44, 26, 31, 20, 17, 18, 54, 55, 93]
########
[31, 26, 18, 20, 17, 44, 54, 55, 93]
########
[26, 20, 18, 17, 31, 44, 54, 55, 93]
########
[20, 17, 18, 26, 31, 44, 54, 55, 93]
########
[18, 17, 20, 26, 31, 44, 54, 55, 93]
########
[17, 18, 20, 26, 31, 44, 54, 55, 93]
新列表爲:[17, 18, 20, 26, 31, 44, 54, 55, 93]

八、計數排序

參考:https://blog.csdn.net/wardseptember/article/details/81434641

計數排序的核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有確定範圍的整數。

1. 算法步驟

2. 動圖演示

 

3. Python代碼實現

# 計數排序
def count_Sort(alist):
    alist_len = len(alist)
    alist_max = max(alist)#返回最大值
    alist_min = min(alist)#返回最小值

    #計算待排序列表的元素數值區域長度,如4-9共9+1-4=6個數
    count_alist_length = alist_max + 1 - alist_min
    count_alist = [0] * count_alist_length  #構造一個全爲0列表

    for number in alist:
        count_alist[number - alist_min] += 1  #統計列表中每個值出現的次數,

    #使counting_arr[i]存放<=i的元素個數,就是待排序列表中比某個值小的元素有多少個
    for i in range(1, count_alist_length):
        count_alist[i] = count_alist[i] + count_alist[i-1]

    order_list = [0] * alist_len   #存放排序結果

    #使每個元素被放在ordered中正確的位置,升序
    for i in range(alist_len): 
        order_list[count_alist[alist[i] - alist_min]-1] = alist[i]#-1是因爲下標從0開始的
        count_alist[alist[i] - alist_min] -= 1 #每歸位一個元素,就少一個元素

    return order_list

參考:https://github.com/hustcc/JS-Sorting-Algorithm

https://zh.wikipedia.org/wiki/%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95

http://www.javashuo.com/article/p-cuipkveg-ne.html

http://www.javashuo.com/article/p-qupwsrwi-ep.html