用python編寫排序算法


交換排序 === 冒泡排序,快速排序

插入排序 ===直接插入排序,希爾排序

選擇排序 === 簡單選擇排序,堆排序

歸並排序

基數排序

冒泡排序

要點

冒泡排序是一種交換排序。

什么是交換排序呢?

交換排序:兩兩比較待排序的關鍵字,並交換不滿足次序要求的那對數,直到整個表都滿足次序要求為止。

原理:比較兩個相鄰的元素,將值大的元素交換到前面。

算法思想

它重復地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重復地進行直到沒有再需要交換,也就是說該數列已經排序完成。

這個算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端,故命名冒泡。

假設有一個大小為 N 的無序序列。冒泡排序就是要每趟排序過程中通過兩兩比較,找到第 i 個小(大)的元素,將其往上排。

以上圖為例,演示一下冒泡排序的實際流程:

假設有一個無序序列 { 4. 3. 1. 2, 5 }

  • 第一趟排序:通過兩兩比較,找到第一小的數值 1 ,將其放在序列的第一位。

  • 第二趟排序:通過兩兩比較,找到第二小的數值 2 ,將其放在序列的第二位。

  • 第三趟排序:通過兩兩比較,找到第三小的數值 3 ,將其放在序列的第三位。

至此,所有元素已經有序,排序結束。

要將以上流程轉化為代碼,我們需要像機器一樣去思考,不然編譯器可看不懂。

假設要對一個大小為 N 的無序序列進行升序排序(即從小到大)。

  • 每趟排序過程中需要通過比較找到第 i 個小的元素。

  • 所以,我們需要一個外部循環,從數組首端(下標 0) 開始,一直掃描到倒數第二個元素(即下標 N - 2) ,剩下最后一個元素,必然為最大。

假設是第 i 趟排序,可知,前 i-1 個元素已經有序。現在要找第 i 個元素,只需從數組末端開始,掃描到第 i 個元素,將它們兩兩比較即可。

  • 所以,需要一個內部循環,從數組末端開始(下標 N - 1),掃描到 (下標 i + 1)。

 1 def bubbleSort(list):
 2     # 外層循環
 3     for j in range(len(list) - 1):
 4         count = 0
 5         # 內層循環
 6         for i in range(0, len(list)-1-j):
 7             if list[i] > list[i + 1]:
 8                 list[i], list[i + 1] = list[i + 1], list[i]
 9                 count += 1
10         if 0 == count:
11             break
12 
13 
14 if __name__ == '__main__':
15     list = [32, 12, 66, 17, 80, 58, 46, 25, 74]
16     print(list)
17     bubbleSort(list)
18     print(list)
冒泡python
 

算法分析

冒泡排序算法的性能

 

時間復雜度

由上面的例子可知  總的遍歷比較次數為 4 + 3 + 2 + 1 = 10次

對於n位的數列則有比較次數為 (n-1) + (n-2) + ... + 1 = n * (n - 1) / 2,這就得到了最大的比較次數
而O(N^2)表示的是復雜度的數量級。舉個例子來說,如果n = 10000,那么 n(n-1)/2 = (n^2 - n) / 2 = (100000000 - 10000) / 2,相對10^8來說,10000小的可以忽略不計了,所以總計算次數約為0.5 * N^2。用O(N^2)就表示了其數量級(忽略前面系數0.5)。

若文件的初始狀態是正序的,一趟掃描即可完成排序。所需的關鍵字比較次數 C 和記錄移動次數 M 均達到最小值:Cmin = N - 1, Mmin = 0。所以,冒泡排序最好時間復雜度為 O(N)。

若初始文件是反序的,需要進行 N -1 趟排序。每趟排序要進行 N - i 次關鍵字的比較(1 ≤ i ≤ N - 1),且每次比較都必須移動記錄三次來達到交換記錄位置。在這種情況下,比較和移動次數均達到最大值:

Cmax = N(N-1)/2 = O(N2)
Mmax = 3N(N-1)/2 = O(N2)

冒泡排序的最壞時間復雜度為 O(N2)。因此,冒泡排序的平均時間復雜度為 O(N2)。

總結起來,其實就是一句話:當數據越接近正序時,冒泡排序性能越好。

算法穩定性

冒泡排序就是把小的元素往前調或者把大的元素往后調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。

所以相同元素的前后順序並沒有改變,所以冒泡排序是一種穩定排序算法。

優化

對冒泡排序常見的改進方法是加入標志性變量swapped ,用於標志某一趟排序過程中是否有數據交換。

如果進行某一趟排序時並沒有進行數據交換,則說明所有數據已經有序,可立即結束排序,避免不必要的比較過程。

 
 1 def bubbleSort2(alist):
 2     list_len = len(alist)
 3     for i in range(list_len):
 4         # 定義一個swapped
 5         # 如果有元素交換過就置為True
 6         # 如果沒有元素交換過就退出循環
 7 
 8         swapped = False
 9         for j in range(list_len-1-i):
10             if alist[j] > alist[j+1]:
11                 alist[j],alist[j+1] = alist[j+1],alist[j]
12                 swapped = True
13         # 攪拌排序
14         # 從后面往前面檢索,如果前面比后面的大,就交換
15         if swapped:
16             swapped = False
17             # 由於上面已經有一個元素在最后排好序了,所以這時要減2
18             for j in range(list_len - 2 - i,0,-1):
19                 if alist[j] < alist[j - 1]:
20                     alist[j], alist[j - 1] = alist[j - 1], alist[j]
21                     swapped = True
22         # 如果沒有發生元素交換,就說明列表已經是有序的了
23         # 這時可以直接退出循環
24         if not swapped:
25             return alist
26 
27 
28 def main():
29     print(bubbleSort2([22, 3, 1, 6, 7, 8, 2, 5]))
30 
31 if __name__ == '__main__':
32     main()
python優化

 

快速排序

要點

快速排序是一種交換排序。

快速排序由 C. A. R. Hoare 在 1962 年提出。

算法思想

它的基本思想是:

通過一趟排序將要排序的數據分割成獨立的兩部分:分割點左邊都是比它小的數,右邊都是比它大的數。

然后再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。方式不唯一

詳細的圖解往往比大堆的文字更有說明力,所以直接上圖:

 

上圖中,演示了快速排序的處理過程:

  1. 初始狀態為一組無序的數組:2、4、5、1、3。

  2. 經過以上操作步驟后,完成了第一次的排序,得到新的數組:1、2、5、4、3。

  3. 新的數組中,以 2 為分割點,左邊都是比 2 小的數,右邊都是比 2 大的數。

  4. 因為 2 已經在數組中找到了合適的位置,所以不用再動。

  5. 2 左邊的數組只有一個元素 1,所以顯然不用再排序,位置也被確定。(注:這種情況時,left 指針和 right 指針顯然是重合的。因此在代碼中,我們可以通過設置判定條件遍歷 left 必須小於 right,如果不滿足,則不用排序了)。

  6. 而對於 2 右邊的數組 5、4、3,設置 left 指向 5,right 指向 3,開始繼續重復圖中的一、二、三、四步驟,對新的數組進行排序。

分解和合並迭代實現方式:

在數列之中,選擇一個元素作為"基准"(pivot),或者叫比較值。

數列中所有元素都和這個基准值進行比較,如果比基准值小就移到基准值的左邊,如果比基准值大就移到基准值的右邊

以基准值左右兩邊的子列作為新數列,不斷重復第一步和第二步,直到所有子集只剩下一個元素為止。

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]])

 簡潔快排匿名函數生成器

代碼實現:
 1 def quick_sort(arr):
 2     """快速排序"""
 3     if len(arr) < 2:
 4         return arr
 5     # 選取基准,隨便選哪個都可以,選中間的便於理解
 6     base = arr[len(arr) // 2]
 7     # 定義基准值左右兩個空數列用於存放排序后的
 8     left, right = [], []
 9     # 從原始數組中移除基准值
10     arr.remove(base)
11     for i in arr:
12         # 大於基准值放右邊
13         if i >= base:
14             right.append(i)
15         else:
16             # 小於基准值放左邊
17             left.append(i)
18     # 使用迭代進行比較
19     return quick_sort(left) + [base] + quick_sort(right)
快速排序

 

算法分析

快速排序算法的性能

 

時間復雜度

當數據有序時,以第一個關鍵字為基准分為兩個子序列,前一個子序列為空,此時執行效率最差。

而當數據隨機分布時,以第一個關鍵字為基准分為兩個子序列,兩個子序列的元素個數接近相等,此時執行效率最好。

所以,數據越隨機分布時,快速排序性能越好;數據越接近有序,快速排序性能越差。

空間復雜度

快速排序在每次分割的過程中,需要 1 個空間存儲基准值。而快速排序的大概需要 Nlog2N 次的分割處理,所以占用空間也是 Nlog2N 個。

算法穩定性

在快速排序中,相等元素可能會因為分區而交換順序,所以它是不穩定的算法。

 

快速排序優化

分治思想的排序在處理大數據集量時效果比較好,小數據集性能差些。

快速排序有一個缺點就是對於小規模的數據集性能不是很好。可能有人認為可以忽略這個缺點不計,因為大多數排序都只要考慮大規模的適應性就行了。但是快速排序算法使用了分治技術,最終來說大的數據集都要分為小的數據集來進行處理,所以快排分解到最后幾層性能不是很好,所以我們就可以使用揚長避短的策略去優化快排:

  1. 先使用快排對數據集進行排序,此時的數據集已經達到了基本有序的狀態
  2. 然后當分區的規模達到一定小時,便停止快速排序算法,而是改用插入排序,插入排序在對基本有序的數據集排序有着接近線性的復雜度,性能比較好。

 

插入排序

要點

直接插入排序是一種最簡單的插入排序。

插入排序:每一趟將一個待排序的記錄,按照其關鍵字的大小插入到有序隊列的合適位置里,直到全部插入完成。

 

算法思想

在講解直接插入排序之前,先讓我們腦補一下我們打牌的過程。

  • 先拿一張 5 在手里,

  • 再摸到一張 4,比 5 小,插到 5 前面,

  • 摸到一張 6,嗯,比 5 大,插到 5 后面,

  • 摸到一張 8,比 6 大,插到 6 后面,

  • 。。。

  • 最后一看,我靠,湊到的居然是同花順,這下牛逼大了。

以上的過程,其實就是典型的直接插入排序,每次將一個新數據插入到有序隊列中的合適位置里。

很簡單吧,接下來,我們要將這個算法轉化為編程語言。

假設有一組無序序列 R0, R1, … , RN-1。

  • 我們先將這個序列中下標為 0 的元素視為元素個數為 1 的有序序列。

  • 然后,我們要依次把 R1, R2, … , RN-1 插入到這個有序序列中。所以,我們需要一個外部循環,從下標 1 掃描到 N-1 。

  • 接下來描述插入過程。假設這是要將 Ri 插入到前面有序的序列中。由前面所述,我們可知,插入 Ri 時,前 i-1 個數肯定已經是有序了。

所以我們需要將 Ri 和 R0 ~ Ri-1 進行比較,確定要插入的合適位置。這就需要一個內部循環,我們一般是從后往前比較,即從下標 i-1 開始向 0 進行掃描。

核心代碼

 1 def insertionSort(arr): 
 2   
 3     for i in range(1, len(arr)): 
 4   
 5         key = arr[i] 
 6   
 7         j = i-1
 8         while j >=0 and key < arr[j] : 
 9                 arr[j+1] = arr[j] 
10                 j -= 1
11         arr[j+1] = key 
12   
13   
14 arr = [12, 11, 13, 5, 6] 
15 insertionSort(arr) 
16 print ("排序后的數組:") 
17 for i in range(len(arr)): 
18     print ("%d" %arr[i])
19 
20 ------------------------
21 
22 arr = [1,12,2, 11, 13, 5, 6,18,4,9,-5,3,11] 
23 def insertionSort(arr):
24     #從要排序的列表第二個元素開始比較
25     for i in range(1,len(arr)):
26         j = i
27         #從大到小比較,直到比較到第一個元素
28         while j > 0:
29             if arr[j] < arr[j-1]:
30                 arr[j-1],arr[j] = arr[j],arr[j-1]
31             j -= 1        
32     return arr
33 print(insertionSort(arr))
34 
35 ----------------------------------
36 
37 # 一次往數組添加多個數字
38 def AppendNumbers(array):
39     num = input('Numbers:(split by spaces)\t').split()
40     for i in num:
41         array.append(int(i))
42     print('排序前數組:{}.'.format(array))
43 
44 def InsertionSort(array):
45     AppendNumbers(array)  # 添加
46 
47     list = []
48     while True:
49         for i in array:
50             minimum = min(array)
51             if i == minimum:
52                 list.append(i)
53                 array.remove(i)  # 刪去最小值
54 
55         if array == []:
56             break
57 
58     print('排序后數組:{}.'.format(list))
59 
60 array = [6, 4, 45, -2, -1, 2, 4, 0, 1, 2, 3, 4, 5, 6, -4, -6,  7, 8, 8, 34, 0]
61 InsertionSort(array)
插入排序

算法分析

直接插入排序的算法性能

 

時間復雜度

當數據正序時,執行效率最好,每次插入都不用移動前面的元素,時間復雜度為 O(N)。

當數據反序時,執行效率最差,每次插入都要前面的元素后移,時間復雜度為 O(N2)。

所以,數據越接近正序,直接插入排序的算法性能越好。

空間復雜度

由直接插入排序算法可知,我們在排序過程中,需要一個臨時變量存儲交換的數據和下標,不需要額外的存儲空間,所以空間復雜度為 1 。

算法穩定性

直接插入排序的過程中,不需要改變相等數值元素的位置,所以它是穩定的算法。

 

插入排序優化

當有序區間數據量很大時,查找數據的插入位置就會顯得非常耗時,插入排序算法每次都是從有序區間查找插入位置,以此為切入點,我們可以使用二分查找法來快速確認待插入的位置,於是就有了優化版的插入排序算法,也叫二分查找插入算法。

 1 def insert_sort(data_list):
 2     '''
 3     無優化版
 4     '''
 5     count=0 #統計循環次數
 6     length = len(data_list)
 7     for i in range(1,length ): #默認第一個位置的元素是已排序區間,因此下標從 1 開始
 8         tmp = data_list[i] #待插入的數據
 9         j = i 
10         while j > 0: #從已排序區間查找插入位置
11             count +=1
12             if tmp < data_list[j-1]:
13                 data_list[j] = data_list[j-1]  #元素向后移動,騰出插入位置
14             else:
15                 break
16             j -= 1
17         data_list[j] = tmp #插入操作
18         print(data_list)
19     print(f"總循環次數為 {count}")
20     return data_list
21 
22 if __name__ == "__main__":
23     unsort = [1,3,4,2,1,5,6,7,8,4]
24     print(*insert_sort(unsort)) 
25 ------------------------------------------------------------
26 上述代碼中的 count 只是為了統計循環次數,目的是和優化版的進行對比,當然您也可以對時間復雜度進行分析來對比性能的差異。 print(data_list) 是為了打印出每一次插入后數據列的結果,您可以對比結果來理解插入排序算法。
27 
28 ----------------------------------------------------------------
29 def insert_sort2(data_list):
30     '''
31     使用二分查找函數確定待插入元素在有序區間的插入位置
32     '''
33     count=0 #統計循環次數
34     length = len(data_list)
35     for i in range(1,length ): #默認第一個位置的元素是已排序區間,因此下標從 1 開始
36         print(data_list)
37         wait_insert_data = data_list[i] ##等待插入元素
38         move_index = i 
39         insert_index,count1 = binary_search(data_list[0:i],wait_insert_data) #尋找插入位置
40         count+=count1 #統計循環次數需要加上二分查找的循環次數
41         while move_index > insert_index: #移動元素,直到待插入位置處
42             count+=1
43             data_list[move_index] = data_list[move_index - 1]
44             move_index -= 1
45         data_list[insert_index] = wait_insert_data #插入操作
46         print(data_list)
47     print(f"總循環次數為 {count}")
48     return data_list
49 
50 
51 def binary_search(data_list,data):    
52     """
53     輸入:有序列表,和待查找的數據data
54     輸出:data 應該在該有序列表的插入位置
55     count 變量純粹是為了統計循環次數而使用的,實際應用時可去除。
56     """
57     count = 0
58     length = len(data_list)
59     low = 0
60     high = length-1
61     ##如果給定元素大於等於最后一個元素,則插入最后元素位置的后面
62     ##如果小於第一個元素,則插入位置0 
63     if data >= data_list [length -1]: return length,0
64     elif data < data_list [0]: return 0,0
65     insert_index = 0 
66     while low < high-1:
67         count +=1
68         mid = (low + high)//2 #python中的除法結果默認為浮點數取整數部分時使用 //
69         if data_list[mid] > data:
70             high = mid
71             insert_index = high
72         else:
73             low = mid
74             insert_index = low+1  #如果值相同或者值大於mid的值,那么插入位置位於其后面
75     return insert_index,count
76 
77 if __name__ == "__main__":
78     unsort = [1,3,4,2,1,5,6,7,8,4]
79     print(*insert_sort2(unsort))
插入排序

 

 

 

希爾排序-分治思想的插入排序

要點

希爾(Shell)排序又稱為縮小增量排序,它是一種插入排序。它是直接插入排序算法的一種威力加強版。

該方法因 DL.Shell 於 1959 年提出而得名。

算法思想

希爾排序的基本思想是:

把記錄按步長 gap 分組,對每組記錄采用直接插入排序方法進行排序。

隨着步長逐漸減小,所分成的組包含的記錄越來越多,當步長的值減小到 1 時,整個數據合成為一組,構成一組有序記錄,則完成排序。

我們來通過演示圖,更深入的理解一下這個過程。

 

在上面這幅圖中:

初始時,有一個大小為 10 的無序序列。

在第一趟排序中,我們不妨設 gap1 = N / 2 = 5,即相隔距離為 5 的元素組成一組,可以分為 5 組。

  • 接下來,按照直接插入排序的方法對每個組進行排序。

在** 第二趟排序中**,我們把上次的 gap 縮小一半,即 gap2 = gap1 / 2 = 2 (取整數)。這樣每相隔距離為 2 的元素組成一組,可以分為 2 組。

  • 按照直接插入排序的方法對每個組進行排序。

在第三趟排序中,再次把 gap 縮小一半,即 gap3 = gap2 / 2 = 1。這樣相隔距離為 1 的元素組成一組,即只有一組。

  • 按照直接插入排序的方法對每個組進行排序。此時,排序已經結束。

需要注意一下的是,圖中有兩個相等數值的元素 5 和 5 。我們可以清楚的看到,在排序過程中,兩個元素位置交換了。

所以,希爾排序是不穩定的算法。

核心代碼

 1 def shell_sort(data_list):
 2     '''
 3     思想:分治策略
 4    使用 for 循環
 5     '''
 6     length = len(data_list)
 7     space  = length//2
 8     while space > 0:
 9         for i in range(space,length ): #默認第一個位置的元素是已排序區間,因此下標從 1 開始
10             tmp = data_list[i] #待插入的數據
11             for j in range(i-space,-1,-space): #從已排序區間查找插入位置
12                 if tmp < data_list[j]:
13                     data_list[j+space] = data_list[j]  #元素向后移動,騰出插入位置
14                     i = j #最后的j即為插入的位置
15                 else:
16                     break
17             data_list[i] = tmp #插入操作
18             print(data_list)
19         space = space // 2
20     return data_list
21 
22     unsort = [9,8,7,6,5,4,3,2,1]
23     print(*shell_sort(unsort)) 
24 
25 ----------------------------
26 
27 def shell_sort2(data_list):
28     '''
29     思想:分治策略
30     使用 while 循環
31     '''
32     length = len(data_list)
33     space  = length//2
34     while space > 0:
35         i = space
36         while i < length: #默認第一個位置的元素是已排序區間,因此下標從 1 開始
37             tmp = data_list[i] #待插入的數據
38             j = i
39             while j >= space and data_list[j - space] > tmp: #從已排序區間查找插入位置
40                 data_list[j] = data_list[j-space]  #元素向后移動,騰出插入位置                    
41                 j -= space
42             data_list[j] = tmp #插入操作
43             print(data_list)
44             i +=1
45         space = space // 2
46     return data_list
47 
48 
49     unsort = [9,8,7,6,5,4,3,2,1]
50     print(*shell_sort2(unsort)) 
希爾排序

 

 

算法分析

希爾排序的算法性能

 

時間復雜度

步長的選擇是希爾排序的重要部分。只要最終步長為 1 任何步長序列都可以工作。

算法最開始以一定的步長進行排序。然后會繼續以一定步長進行排序,最終算法以步長為 1 進行排序。當步長為 1 時,算法變為插入排序,這就保證了數據一定會被排序。

Donald Shell 最初建議步長選擇為 N/2 並且對步長取半直到步長達到 1。雖然這樣取可以比 O(N2)類的算法(插入排序)更好,但這樣仍然有減少平均時間和最差時間的余地。可能希爾排序最重要的地方在於當用較小步長排序后,以前用的較大步長仍然是有序的。

比如,如果一個數列以步長 5 進行了排序然后再以步長 3 進行排序,那么該數列不僅是以步長 3 有序,而且是以步長 5 有序。如果不是這樣,那么算法在迭代過程中會打亂以前的順序,那就不會以如此短的時間完成排序了。

已知的最好步長序列是由 Sedgewick 提出的(1, 5, 19, 41, 109,…),該序列的項來自這兩個算式。

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

算法穩定性

由上文的希爾排序算法演示圖即可知,希爾排序中相等數據可能會交換位置,所以希爾排序是不穩定的算法。

直接插入排序和希爾排序的比較

  • 直接插入排序是穩定的;而希爾排序是不穩定的。

  • 直接插入排序更適合於原始記錄基本有序的集合。

  • 希爾排序的比較次數和移動次數都要比直接插入排序少,當 N 越大時,效果越明顯。

  • 在希爾排序中,增量序列 gap 的取法必須滿足:**最后一個步長必須是 1 。**

  • 直接插入排序也適用於鏈式存儲結構;希爾排序不適用於鏈式結構。

 1 def shell_sort(alist):
 2     n = len(alist)
 3     gap = n // 2
 4     while gap >= 1:
 5         for j in range(gap, n):
 6             i = j
 7             while i > 0:
 8                 if alist[i] < alist[i-gap]:
 9                     alist[i], alist[i-1] = alist[i-1], alist[i]
10                     i -= gap
11                 else:
12                     break
13         gap //= 2
14 
15     if __name__ == "__main__":
16         li = [54,26,93,17,77,31,44,55,20]
17         print(li)
18         shell_sort(li)
19         print(li)
希爾排序簡化

 

簡單選擇排序

要點

簡單選擇排序是一種選擇排序。

選擇排序:每趟從待排序的記錄中選出關鍵字最小的記錄,順序放在已排序的記錄序列末尾,直到全部排序結束為止。

算法思想

  • 從待排序序列中,找到關鍵字最小的元素;

  • 如果最小元素不是待排序序列的第一個元素,將其和第一個元素互換;

  • 從余下的 N - 1 個元素中,找出關鍵字最小的元素,重復 1、2 步,直到排序結束。

如圖所示,每趟排序中,將當前**第 i 小的元素放在位置 i **上。

 

算法分析

簡單選擇排序算法的性能

 

時間復雜度

簡單選擇排序的比較次數與序列的初始排序無關。假設待排序的序列有 N 個元素,則**比較次數總是 N (N - 1) / 2 **。

而移動次數與序列的初始排序有關。當序列正序時,移動次數最少,為 0。

當序列反序時,移動次數最多,為 3N (N - 1) / 2。

所以,綜合以上,簡單排序的時間復雜度為 O(N2)。

空間復雜度

簡單選擇排序需要占用一個臨時空間,在交換數值時使用。

示例代碼

 1 def SelectSort(input_list):
 2     l = len(input_list)
 3     if l == 0:
 4         return []
 5     sorted_list = input_list
 6     for i in range(l):
 7         #默認第i個元素是每次的最小值的索引
 8         min_index = i
 9         #找到后面元素最小的索引
10         for j in range(i+1,l):
11             if sorted_list[min_index] >sorted_list[j]:
12                 min_index = j
13         #將找到的最小元素放入前面已經有序序列的末尾
14         temp = sorted_list[i]
15         sorted_list[i] = sorted_list[min_index]
16         sorted_list[min_index] = temp
17         print("%dth"%(i+1))
18         print(sorted_list)
19     return sorted_list
20                 
21 if __name__ == '__main__':
22     input_list = [50,123,543,187,49,30,0,2,11,100]
23     print("input_list:")
24     print(input_list)
25     sorted_list = SelectSort(input_list)
26     print("sorted_list:")
27     print(input_list)
28 
29 
30 input_list:
31 [50, 123, 543, 187, 49, 30, 0, 2, 11, 100]
32 1th
33 [0, 123, 543, 187, 49, 30, 50, 2, 11, 100]
34 2th
35 [0, 2, 543, 187, 49, 30, 50, 123, 11, 100]
36 3th
37 [0, 2, 11, 187, 49, 30, 50, 123, 543, 100]
38 4th
39 [0, 2, 11, 30, 49, 187, 50, 123, 543, 100]
40 5th
41 [0, 2, 11, 30, 49, 187, 50, 123, 543, 100]
42 6th
43 [0, 2, 11, 30, 49, 50, 187, 123, 543, 100]
44 7th
45 [0, 2, 11, 30, 49, 50, 100, 123, 543, 187]
46 8th
47 [0, 2, 11, 30, 49, 50, 100, 123, 543, 187]
48 9th
49 [0, 2, 11, 30, 49, 50, 100, 123, 187, 543]
50 10th
51 [0, 2, 11, 30, 49, 50, 100, 123, 187, 543]
52 sorted_list:
53 [0, 2, 11, 30, 49, 50, 100, 123, 187, 543]  
簡單選擇排序

 

 

 

堆排序

要點

在介紹堆排序之前,首先需要說明一下,堆是個什么玩意兒。

堆是一棵順序存儲的完全二叉樹。

其中每個結點的關鍵字都不大於其孩子結點的關鍵字,這樣的堆稱為小根堆。每個非葉子結點的值都要小於或者等於其左右孩子結點的值。

其中每個結點的關鍵字都不小於其孩子結點的關鍵字,這樣的堆稱為大根堆。每個非葉子結點的值都要大於或者等於其左右孩子結點的值。

舉例來說,對於 n 個元素的序列 {R0, R1, … , Rn} 當且僅當滿足下列關系之一時,稱之為堆:

  • Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)

  • Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)

其中 i=1,2,…,n/2 向下取整;

 

如上圖所示,序列 R{3, 8,15, 31, 25} 是一個典型的小根堆。

堆中有兩個父結點,元素 3 和元素 8。

元素 3 在數組中以 R[0] 表示,它的左孩子結點是 R[1],右孩子結點是 R[2]。

元素 8 在數組中以 R[1] 表示,它的左孩子結點是 R[3],右孩子結點是 R[4],它的父結點是 R[0]。可以看出,它們滿足以下規律:

設當前元素在數組中以 R[i] 表示,那么,

  • 它的左孩子結點是:R[2*i+1];

  • 它的右孩子結點是:R[2*i+2];

  • 它的父結點是:R[(i-1)/2];

  • R[i] <= R[2*i+1] 且 R[i] <= R[2i+2]。

算法思想

構建一個完全二叉樹(序列 -> 完全二叉樹 -> 大頂堆 -> 排序 - > 大頂堆 -> 排序 ...)
  • 首先,按堆的定義將數組 R[0..n]調整為堆(這個過程稱為創建初始堆),交換 R[0]和 R[n];

  • 然后,將 R[0..n-1]調整為堆,交換 R[0]和 R[n-1];

  • 如此反復,直到交換了 R[0]和 R[1]為止。

以上思想可歸納為兩個操作:

  • 根據初始數組去構造初始堆(構建一個完全二叉樹,保證所有的父結點都比它的孩子結點數值大)。

  • 每次交換第一個和最后一個元素,輸出最后一個元素(最大值),然后把剩下元素重新調整為大根堆。

當輸出完最后一個元素后,這個數組已經是按照從小到大的順序排列了。

先通過詳細的實例圖來看一下,如何構建初始堆。

設有一個無序序列 { 1, 3,4, 5, 2, 6, 9, 7, 8, 0 }。

 

構造了初始堆后,我們來看一下完整的堆排序處理:

還是針對前面提到的無序序列 { 1,3, 4, 5, 2, 6, 9, 7, 8, 0 } 來加以說明。

 

相信,通過以上兩幅圖,應該能很直觀的演示堆排序的操作處理。

核心代碼

 
 1 # 將列表打印成樹(堆排序輔助函數)
 2 import math
 3 
 4 def printTree(lst):
 5     treeLen  = len(lst)
 6     treeLay = math.ceil(math.log2(treeLen + 1)) # 樹層數
 7     index = 0
 8     treeWidth = 2 ** treeLay - 1    # 樹寬度
 9     for i in range(treeLay):
10         for j in range(2**i):
11             print('{:^{}}'.format(lst[index], treeWidth), end=' ')
12             index += 1
13             if index >= treeLen:
14                 break
15         treeWidth = treeWidth//2
16         print()
17 
18 lst = [1, 2, 3, 4, 5, 6, 7, 8, 9]
19 printTree(lst)
20 
21 # 調整當前節點
22 def heap_adjust(n, i, array:list):  # array:list 表示array變量的類型是list
23     '''
24     調整當前節點核心算放
25     :param n: 待比較數字的個數
26     :param i: 當前節點的下標
27     :param array: 待排序數據
28     :return:None
29     '''
30     while 2 * i <= n:   # 性質5,=n只有左子樹
31         # 孩子結點判斷2i為左孩子,2i+1為右孩子
32         lChild_index = 2 * i
33         maxChild_index = lChild_index # n=2i
34         if n > lChild_index and array[lChild_index + 1] > array[lChild_index]:  # n>2i說明還有右孩子
35             maxChild_index = lChild_index + 1   # n=2i+1
36         # 和子樹的根節點比較
37         if array[maxChild_index] > array[i]:
38             array[i], array[maxChild_index] = array[maxChild_index], array[i]   # 交換
39             i = maxChild_index # 被交換后,需要判斷是否還需要調整
40         else:
41             break
42         printTree(array)
43         print('--------------------------')
44 
45 # 構建大頂堆
46 def maxHeap(total, array:list):
47     for i in range(total//2, 0, -1):
48         heap_adjust(total, i, array)    # 調整當前結點
49     return array
50 
51 total = 9
52 origin = [0,30, 20, 80, 40, 50, 10, 60, 70, 90]
53 print('maxHeap is ')
54 printTree(maxHeap(total, origin))
55 
56 # 結論:第一層是最大值,第二層一定有個次大值
堆排序
 1 def HeapSort(input_list):
 2     
 3     #調整parent結點為大根堆
 4     def HeapAdjust(input_list,parent,length):
 5         
 6         temp = input_list[parent]
 7         child = 2*parent+1
 8         
 9         while child < length:
10             if child+1 <length and input_list[child] < input_list[child+1]:
11                 child +=1
12             
13             if temp > input_list[child]:
14                 break
15             input_list[parent] = input_list[child]
16             parent = child
17             child = 2*child+1
18         input_list[parent] = temp
19     
20     if input_list == []:
21         return []
22     sorted_list = input_list
23     length = len(sorted_list)
24     #最后一個結點的下標為length//2-1
25     #建立初始大根堆
26     for i in range(0,length // 2 )[::-1]:
27         HeapAdjust(sorted_list,i,length)
28     
29     for j in range(1,length)[::-1]:
30         #把堆頂元素即第一大的元素與最后一個元素互換位置
31         temp = sorted_list[j]
32         sorted_list[j] = sorted_list[0]
33         sorted_list[0] = temp
34         #換完位置之后將剩余的元素重新調整成大根堆
35         HeapAdjust(sorted_list,0,j)
36         print('%dth' % (length - j))
37         print(sorted_list)
38     return sorted_list
39     
40         
41 if __name__ == '__main__':
42     input_list = [50,123,543,187,49,30,0,2,11,100]
43     print("input_list:")
44     print(input_list)
45     sorted_list = HeapSort(input_list)
46     print("sorted_list:")
47     print(input_list)
48 
49 
50 
51 input_list:
52 [50, 123, 543, 187, 49, 30, 0, 2, 11, 100]
53 1th
54 [187, 123, 50, 49, 100, 30, 0, 2, 11, 543]
55 2th
56 [123, 100, 50, 49, 11, 30, 0, 2, 187, 543]
57 3th
58 [100, 49, 50, 2, 11, 30, 0, 123, 187, 543]
59 4th
60 [50, 49, 30, 2, 11, 0, 100, 123, 187, 543]
61 5th
62 [49, 11, 30, 2, 0, 50, 100, 123, 187, 543]
63 6th
64 [30, 11, 0, 2, 49, 50, 100, 123, 187, 543]
65 7th
66 [11, 2, 0, 30, 49, 50, 100, 123, 187, 543]
67 8th
68 [2, 0, 11, 30, 49, 50, 100, 123, 187, 543]
69 9th
70 [0, 2, 11, 30, 49, 50, 100, 123, 187, 543]
71 sorted_list:
72 [0, 2, 11, 30, 49, 50, 100, 123, 187, 543]
堆排序2

 

算法分析

堆排序算法的總體情況

 

時間復雜度

堆的存儲表示是順序的。因為堆所對應的二叉樹為完全二叉樹,而完全二叉樹通常采用順序存儲方式。

當想得到一個序列中第 k 個最小的元素之前的部分排序序列,最好采用堆排序。

因為堆排序的時間復雜度是 O(n+klog2n),若 k ≤ n/log2n,則可得到的時間復雜度為 O(n)。

算法穩定性

堆排序是一種不穩定的排序方法。

因為在堆的調整過程中,關鍵字進行比較和交換所走的是該結點到葉子結點的一條路徑,因此對於相同的關鍵字就可能出現排在后面的關鍵字被交換到前面來的情況。

 

歸並排序

要點

歸並排序是建立在歸並操作上的一種有效的排序算法,該算法是采用分治法(Divide and Conquer)的一個非常典型的應用。

將已有序的子序列合並,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合並成一個有序表,稱為二路歸並。

算法思想

將待排序序列 R[0…n-1] 看成是 n 個長度為 1 的有序序列,將相鄰的有序表成對歸並,得到 n/2 個長度為 2 的有序表;將這些有序序列再次歸並,得到 n/4 個長度為 4 的有序序列;如此反復進行下去,最后得到一個長度為 n 的有序序列。

綜上可知:

歸並排序其實要做兩件事:

  • “分解”——將序列每次折半划分。

  • “合並”——將划分后的序列段兩兩合並后排序。

我們先來考慮第二步,如何合並?

在每次合並過程中,都是對兩個有序的序列段進行合並,然后排序。

這兩個有序序列段分別為 R[low, mid] 和 R[mid+1, high]。

先將他們合並到一個局部的暫存數組R2 中,帶合並完成后再將 R2 復制回 R 中。

為了方便描述,我們稱 R[low, mid] 第一段,R[mid+1, high] 為第二段。

每次從兩個段中取出一個記錄進行關鍵字的比較,將較小者放入 R2 中。最后將各段中余下的部分直接復制到 R2 中。

經過這樣的過程,R2 已經是一個有序的序列,再將其復制回 R 中,一次合並排序就完成了。

假如我們有一個n個數的數列,下標從0到n-1
首先是分開的過程
1 我們按照 n//2 把這個數列分成兩個小的數列
2 把兩個小數列 再按照新長度的一半 把每個小數列都分成兩個更小的,一直這樣重復,一直到每一個數分開了
比如: 6 5 4 3 2 1
第一次 n=6 n//2=3 分成 6 5 4 3 2 1
第二次 n=3 n//2=1 分成 6 5 4 3 2 1
第三次 n=1的部分不分了
n=2 n//2=1 分成 5 4 2 1
之后是合並排序的過程:
3 分開之后我們按照最后分開的兩個數比較大小形成正確順序后組合綁定
  剛剛舉得例子 最后一行最后分開的數排序后綁定 變成 4 5 1 2
  排序后倒數第二行相當於把最新分開的數排序之后變成 6 4 5 3 12
4 對每組數據按照上次分開的結果,進行排序后綁定
  6 和 4 5(兩個數綁定了) 進行排序
  3 和 1 2(兩個數綁定了) 進行排序
  排完后 上述例子第一行待排序的 4 5 6 1 2 3 兩組數據
5 對上次分開的兩組進行排序
  拿着 4 5 6 1 2 3兩個數組,進行排序,每次拿出每個數列中第一個(最小的數)比較,把較小的數放入結果數組。再進行下一次排序。
  每個數組拿出第一個數,小的那個拿出來放在第一位 1 拿出來了, 變成4 5 6 2 3
  每個數組拿出第一個書比較小的那個放在下一個位置 1 2被拿出來, 待排序 4 5 6 2
  每個數組拿出第一個書比較小的那個放在下一個位置 1 2 3 被拿出來, 待排序 4 5 6
  如果一個數組空了,說明另一個數組一定比排好序的數組最后一個大 追加就可以結果 1 2 3 4 5 6
  相當於我們每次拿到兩個有序的列表進行合並,分別從兩個列表第一個元素比較,把小的拿出來,在拿新的第一個元素比較,把小的拿出來
  這樣一直到兩個列表空了 就按順序合並了兩個列表

掌握了合並的方法,接下來,讓我們來了解如何分解。

在某趟歸並中,設各子表的長度為 gap,則歸並前 R[0…n-1] 中共有 n/gap 個有序的子表:R[0…gap-1], R[gap…2gap-1], … , R[(n/gap)gap … n-1]。

調用 Merge 將相鄰的子表歸並時,必須對表的特殊情況進行特殊處理。

若子表個數為奇數,則最后一個子表無須和其他子表歸並(即本趟處理輪空):若子表個數為偶數,則要注意到最后一對子表中后一個子表區間的上限為 n-1。

核心代碼

 1 def merge_sort( li ):
 2     #不斷遞歸調用自己一直到拆分成成單個元素的時候就返回這個元素,不再拆分了
 3     if len(li) == 1:
 4         return li
 5 
 6     #取拆分的中間位置
 7     mid = len(li) // 2
 8     #拆分過后左右兩側子串
 9     left = li[:mid]
10     right = li[mid:]
11 
12     #對拆分過后的左右再拆分 一直到只有一個元素為止
13     #最后一次遞歸時候ll和lr都會接到一個元素的列表
14     # 最后一次遞歸之前的ll和rl會接收到排好序的子序列
15     ll = merge_sort( left )
16     rl =merge_sort( right )
17 
18     # 我們對返回的兩個拆分結果進行排序后合並再返回正確順序的子列表
19     # 這里我們調用拎一個函數幫助我們按順序合並ll和lr
20     return merge(ll , rl)
21 
22 #這里接收兩個列表
23 def merge( left , right ):
24     # 從兩個有順序的列表里邊依次取數據比較后放入result
25     # 每次我們分別拿出兩個列表中最小的數比較,把較小的放入result
26     result = []
27     while len(left)>0 and len(right)>0 :
28         #為了保持穩定性,當遇到相等的時候優先把左側的數放進結果列表,因為left本來也是大數列中比較靠左的
29         if left[0] <= right[0]:
30             result.append( left.pop(0) )
31         else:
32             result.append( right.pop(0) )
33     #while循環出來之后 說明其中一個數組沒有數據了,我們把另一個數組添加到結果數組后面
34     result += left
35     result += right
36     return result
37 
38 if __name__ == '__main__':
39     li = [5,4 ,3 ,2 ,1]
40     li2 = merge_sort(li)
41     print(li2)
歸並排序

 

 

算法分析

歸並排序算法的性能

時間復雜度

歸並排序的形式就是一棵二叉樹,它需要遍歷的次數就是二叉樹的深度,而根據完全二叉樹的可以得出它的時間復雜度是 O(n*log2n)。

空間復雜度

由前面的算法說明可知,算法處理過程中,需要一個大小為 n 的臨時存儲空間用以保存合並序列。

算法穩定性

在歸並排序中,相等的元素的順序不會改變,所以它是穩定的算法。

歸並排序和堆排序、快速排序的比較

  • 若從空間復雜度來考慮:首選堆排序,其次是快速排序,最后是歸並排序。

  • 若從穩定性來考慮,應選取歸並排序,因為堆排序和快速排序都是不穩定的。

  • 若從平均情況下的排序速度考慮,應該選擇快速排序。

示例代碼

 

 

基數排序、桶排序、計數排序

基數排序一般用於長度相同的元素組成的數組。首先按照最低有效數字進行排序,然后由低位向高位進行。基數排序可以看做是進行多趟桶排序。每個有效數字都在0-9之間,很適合桶排序,建10個桶很方便

[0,1)和[1,+∞)的桶排序是一種非常快速的排序,留置一個數組S,里面含有M個桶,初始化為0。然后遍歷數組A,讀入Ai時,S[Ai]增一。所有輸入被讀進后,掃描數組S得出排好序的表。該算法時間花費O(M+N),空間上不能原地排序。

計數排序假設n個輸入元素中每一個都是介於0到k之間的整數,此處k為某個整數。當k=O(n)時,計數排序的運行時間為Θ(n)。

對每一個數的元素x,確定出小於x的元素個數。有了這一信息就可以把x直接放到最終輸出數組中的位置上。

要點

基數排序與本系列前面講解的七種排序方法都不同,它不需要比較關鍵字的大小。

它是根據關鍵字中各位的值,通過對排序的 N 個元素進行若干趟“分配”與“收集”來實現排序的。

不妨通過一個具體的實例來展示一下,基數排序是如何進行的。

設有一個初始序列為: R {50, 123, 543, 187, 49, 30, 0 ,  2 , 11, 100}。

我們知道,任何一個阿拉伯數,它的各個位數上的基數都是以 0~9 來表示的。

所以我們不妨把 0~9 視為 10 個桶。

我們先根據序列的個位數的數字來進行分類,將其分到指定的桶中。例如:R[0] = 50,個位數上是 0,將這個數存入編號為 0 的桶中。

分類后,我們在從各個桶中,將這些數按照從編號 0 到編號 9 的順序依次將所有數取出來。

這時,得到的序列就是個位數上呈遞增趨勢的序列。

按照個位數排序:{50, 30, 0, 100, 11, 2, 123, 543 , 187, 49}。

接下來,可以對十位數、百位數也按照這種方法進行排序,最后就能得到排序完成的序列。

算法分析

基數排序的性能

 

時間復雜度

通過上文可知,假設在基數排序中,r 為基數,d 為位數。則基數排序的時間復雜度為 O(d(n+r))。

我們可以看出,基數排序的效率和初始序列是否有序沒有關聯。

空間復雜度

在基數排序過程中,對於任何位數上的基數進行“裝桶”操作時,都需要 n+r 個臨時空間。

算法穩定性

在基數排序過程中,每次都是將當前位數上相同數值的元素統一“裝桶”,並不需要交換位置。所以基數排序是穩定的算法。

示例代碼

 1 class bucketSort(object):
 2     def insertSort(self,a):
 3         n=len(a)
 4         if n<=1:
 5             pass
 6         for i in range(1,n):
 7             key=a[i]
 8             j=i-1
 9             while key<a[j] and j>=0:
10                 a[j+1]=a[j]
11                 j-=1
12             a[j+1]=key
13     def sort(self,a):
14         n=len(a)
15         s=[[] for i in xrange(n)]
16         for i in a:
17             s[int(i*n)].append(i)
18         for i in s:
19             self.insertSort(i)
20         return [i for j in s for i in j]
21     def __call__(self,a):
22         return self.sort(a)
23                        
24 if __name__=='__main__':
25     from random import random
26     from timeit import Timer
27     a=[random() for i in xrange(10000)]
28     def test_bucket_sort():
29         bucketSort()(a)
30     def test_builtin_sort():
31         sorted(a)
32     tests=[test_bucket_sort,test_builtin_sort]
33     for test in tests:       
34         name=test.__name__
35         t=Timer(name+'()','from __main__ import '+name)
36         print t.timeit(1)
[0,1)的桶排序

 

 1 import random
 2 class bucketSort(object):
 3     def _max(self,oldlist):
 4         _max=oldlist[0]
 5         for i in oldlist:
 6             if i>_max:
 7                 _max=i
 8         return _max
 9     def _min(self,oldlist):
10         _min=oldlist[0]
11         for i in oldlist:
12             if i<_min:
13                 _min=i
14         return _min
15     def sort(self,oldlist):
16         _max=self._max(oldlist)
17         _min=self._min(oldlist)
18         s=[0 for i in xrange(_min,_max+1)]
19         for i in oldlist:
20             s[i-_min]+=1
21         current=_min
22         n=0
23         for i in s:
24             while i>0:
25                 oldlist[n]=current
26                 i-=1
27                 n+=1
28             current+=1
29     def __call__(self,oldlist):
30         self.sort(oldlist)
31         return oldlist
32 if __name__=='__main__':
33     a=[random.randint(0,100) for i in xrange(10)]
34     bucketSort()(a)
35     print a
[1,+∞)的桶排序

 

1 import random
2 def radixSort():
3     A=[random.randint(1,9999) for i in xrange(10000)]  
4     for k in xrange(4):  #4輪排序      
5         s=[[] for i in xrange(10)]
6         for i in A:
7             s[i/(10**k)%10].append(i)
8         A=[a for b in s for a in b]
9     return A
基數排序

 

 1 def countingSort(alist,k):
 2     n=len(alist)
 3     b=[0 for i in xrange(n)]
 4     c=[0 for i in xrange(k+1)]
 5     for i in alist:
 6         c[i]+=1
 7     for i in xrange(1,len(c)):
 8         c[i]=c[i-1]+c[i]
 9     for i in alist:
10         b[c[i]-1]=i
11         c[i]-=1
12     return b
13 if __name__=='__main__':
14     a=[random.randint(0,100) for i in xrange(100)]
15     print countingSort(a,100)
計數排序

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM