一、列表排序
排序就是將一組“無序”的記錄序列調整為“有序”的記錄序列。
列表排序:將無序列表變為有序列表。
輸入:列表
輸出:有序列表
兩種基本的排序方式:升序和降序。
python內置的排序函數:sort()。
二、常見排序算法
名稱 |
復雜度 |
說明 |
備注 |
冒泡排序 |
O(N*N) |
將待排序的元素看作是豎着排列的“氣泡”,較小的元素比較輕,從而要往上浮 |
|
插入排序 Insertion sort |
O(N*N) |
逐一取出元素,在已經排序的元素序列中從后向前掃描,放到適當的位置 |
起初,已經排序的元素序列為空 |
選擇排序 |
O(N*N) |
首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再從剩余未排序元素中繼續尋找最小元素,然后放到排序序列末尾。以此遞歸。 |
|
快速排序 Quick Sort |
O(n *log2(n)) |
先選擇中間值,然后把比它小的放在左邊,大的放在右邊(具體的實現是從兩邊找,找到一對后交換)。然后對兩邊分別使用這個過程(遞歸)。 |
|
堆排序HeapSort |
O(n *log2(n)) |
利用堆(heaps)這種數據結構來構造的一種排序算法。堆是一個近似完全二叉樹結構,並同時滿足堆屬性:即子節點的鍵值或索引總是小於(或者大於)它的父節點。 |
近似完全二叉樹 |
希爾排序 SHELL |
O(n1+£) 0<£<1 |
選擇一個步長(Step) ,然后按間隔為步長的單元進行排序.遞歸,步長逐漸變小,直至為1. |
|
箱排序 |
O(n) |
設置若干個箱子,把關鍵字等於 k 的記錄全都裝入到第k 個箱子里 ( 分配 ) ,然后按序號依次將各非空的箱子首尾連接起來 ( 收集 ) 。 |
分配排序的一種:通過" 分配 " 和 " 收集 " 過程來實現排序。 |
1、冒泡排序(Bubble Sort)
列表每兩個相鄰的數,如果前面比后面大,則交換這兩個數。
一趟排序完成后,則無序區減少一個數,有序區增加一個數。
代碼關鍵點:趟、無序區范圍。
(1)圖示說明
這樣排序一趟后,最大的數9,就到了列表最頂成為了有序區,下面的部分則還是無序區。然后在無序區不斷重復這個過程,每完成一趟排序,無序區減少一個數,有序區增加一個數。圖示最后一張圖要開始第六趟排序,排序從第0趟開始計數。剩一個數的時候不需要排序了,因此整個排序排了n-1趟。
(2)代碼示例
import random def bubble_sort(li): for i in range(len(li)-1): # 總共是n-1趟 for j in range(len(li)-i-1): # 每一趟都有箭頭,從0開始到n-i-1 if li[j] > li[j+1]: # 比對箭頭指向和箭頭后面的那個數的值 # 當箭頭所指數大於后面的數時交換位置, 升序排列;條件相反則為降序排列 li[j], li[j+1] = li[j+1], li[j] li = [random.randint(0, 10000) for i in range(30)] print(li) bubble_sort(li) print(li) """ [5931, 5978, 6379, 4217, 9597, 4757, 4160, 3310, 6916, 2463, 9330, 8043, 8275, 5614, 8908, 7799, 9256, 3097, 9447, 9327, 7604, 9464, 417, 927, 1720, 145, 6451, 7050, 6762, 6608] [145, 417, 927, 1720, 2463, 3097, 3310, 4160, 4217, 4757, 5614, 5931, 5978, 6379, 6451, 6608, 6762, 6916, 7050, 7604, 7799, 8043, 8275, 8908, 9256, 9327, 9330, 9447, 9464, 9597] """
如果要打印出每次排序結果:
import random def bubble_sort(li): for i in range(len(li)-1): # 總共是n-1趟 for j in range(len(li)-i-1): # 每一趟都有箭頭,從0開始到n-i-1 if li[j] > li[j+1]: # 比對箭頭指向和箭頭后面的那個數的值 # 當箭頭所指數大於后面的數時交換位置, 升序排列;條件相反則為降序排列 li[j], li[j+1] = li[j+1], li[j] print(li) li = [random.randint(0, 10000) for i in range(5)] print(li) bubble_sort(li) print(li) """ [1806, 212, 4314, 1611, 8355] [212, 1806, 1611, 4314, 8355] [212, 1611, 1806, 4314, 8355] [212, 1611, 1806, 4314, 8355] [212, 1611, 1806, 4314, 8355] [212, 1611, 1806, 4314, 8355] """
(3)算法時間復雜度
n是列表的長度,算法中也沒有發生循環折半的過程,具備兩層關於n的循環,因此它的時間復雜度是O(n2)。
(4)冒泡排序優化
如果在一趟排序過程中沒有發生交換就可以認定已經排好序了。因此可做如下優化:
import random def bubble_sort(li): for i in range(len(li)-1): # 總共是n-1趟 exchange = False for j in range(len(li)-i-1): # 每一趟都有箭頭,從0開始到n-i-1 if li[j] > li[j+1]: # 比對箭頭指向和箭頭后面的那個數的值 # 當箭頭所指數大於后面的數時交換位置, 升序排列;條件相反則為降序排列 li[j], li[j+1] = li[j+1], li[j] exchange = True # 如果發生了交換就置為true print(li) if not exchange: # 如果exchange還是False,說明沒有發生交換,結束代碼 return # li = [random.randint(0, 10000) for i in range(5)] li = [1806, 212, 4314, 1611, 8355] bubble_sort(li) """ [212, 1806, 1611, 4314, 8355] [212, 1611, 1806, 4314, 8355] [212, 1611, 1806, 4314, 8355] """
對比前面排序的次數少了很多,算法得到了優化~
2、選擇排序(Selection Sort)
一趟遍歷完記錄最小的數,放到第一個位置;再一趟遍歷記錄剩余列表中的最小的數,繼續放置。
算法關鍵點:有序區和無序區、無序區最小數的位置。
(1)簡單的選擇排序
def select_sort_simple(li): li_new = [] for i in range(len(li)): min_val = min(li) # 找到最小的數,也需要遍歷一邊O(n) li_new.append(min_val) li.remove(min_val) # 按值刪除,如果有重復的先刪除最左邊的,刪除之后,后面元素需要向前移動補位,因此也是O(n) return li_new li = [3, 2, 4, 1, 5, 6, 8, 7, 9] print(select_sort_simple(li)) """ [1, 2, 3, 4, 5, 6, 7, 8, 9] """
注意這里的remove操作和min操作都不是O(1)的操作,都需要進行遍歷,因此它的時間復雜度是O(n2)。
而且前面冒泡排序是原地排序不需要開啟一個新的列表,二這個版本的選擇排序不是原地排序,多占了一份內存。
(2)優化后的選擇排序
def select_sort(li): # 和冒泡排序類似,在n-1趟完成后,無序區只剩一個數,這個數一定是最大的 for i in range(len(li)-1): # i是第幾趟 min_loc = i # 最小值的位置 for j in range(i+1, len(li)): # 遍歷無序區,從i開始是自己跟自己比,因此從i+1開始 if li[j] < li[min_loc]: # 如果遍歷的這個數小於現在min_loc位置上的數 min_loc = j # 修改min_loc的index,循環完后,min_loc一定是無序區最小數的下標 li[i], li[min_loc] = li[min_loc], li[i] # 將i和min_loc對應的值進行位置交換 print(li) # 打印每趟執行完的排序,分析過程 li = [3, 2, 4, 1, 5, 6, 8, 7, 9] select_sort(li) # print(li) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
這里只有兩層循環,時間復雜度是O(n2)。
3、插入排序(Insertion Sort)
元素被分為有序區和無序區兩部分。初始時手里(有序區)只有一張牌,每次(從無序區)摸一張牌,插入到手里已有牌的正確位置,直到無序區變空。
(1)圖示說明
一開始手里的牌只有5
第一張摸到的牌是7,比5大插到5的右邊:
第二張摸到的牌是4,需要將5和7的位置向右挪,將4插到最前面:
后面的情況依次類推。
(2)代碼示例
def insert_sort(li): for i in range(1, len(li)): # i表示摸到牌的下標 tmp = li[i] # 摸到的牌 j = i - 1 # j指得是手里牌的下標 while li[j] > tmp and j >= 0: # 循環條件 """ 循環終止條件:如果手里最后一張牌 <= 摸到的牌 or j == -1 比如手里有牌457,新摸到一張6(index=3),當比對5與6時,5<6,滿足了循環終止條件,插到列表j+1處,即index=2處. 比如手里的牌是4567,新摸到一張3(index=4),一個個比對均比3大,到4與3比較時,由於比4小,再次循環j=-1,滿足終止條件插到列表j+1處,即最前面 """ li[j + 1] = li[j] # 通過循環條件,將手里的牌左移 j -= 1 # 手里的牌對比箭頭左移 li[j + 1] = tmp # 將摸到的牌插入有序區 print(li) # 打印每一趟排序過程 li = [3, 2, 4, 1, 5, 6, 9, 6, 8] print('原列表', li) insert_sort(li) print('排序結果', li)
這個循環主要是在找插入的位置。
時間復雜度:O(n2)。
(3)查看排序算法執行時間和效率
准備好cal_time.py:
import time def cal_time(func): def wrapper(*args, **kwargs): t1 = time.time() result = func(*args, **kwargs) t2 = time.time() print("%s running time: %s secs." % (func.__name__, t2 - t1)) return result return wrapper
檢查10000個隨機數字排序:
import random from cal_time import * @cal_time def insert_sort(li): for i in range(1, len(li)): # i表示摸到牌的下標 tmp = li[i] # 摸到的牌 j = i - 1 # j指得是手里牌的下標 while li[j] > tmp and j >= 0: # 循環條件 li[j + 1] = li[j] # 通過循環條件,將手里的牌左移 j -= 1 # 手里的牌對比箭頭左移 li[j + 1] = tmp # 將摸到的牌插入有序區 # print(li) # 打印每一趟排序過程 li = list(range(10000)) random.shuffle(li) insert_sort(li) """ insert_sort running time: 4.496495723724365 secs. """
4、快速排序(Quick Sort)
快速排序思路:取一個元素p(第一個元素),使元素p歸位;列表被p分為兩部分,左邊都比p小,右邊都比p大;遞歸完成排序。
算法關鍵點:歸位、遞歸。
(1)圖示說明
(2)元素歸位過程分析
5要歸位,先用一個變量將5存起來,兩個箭頭表示當前列表的left和right:
列表左邊有了一個空位,從右邊開始找一個比5小的數填入:
此時右邊有了一個空位,右邊是給比5大的數准備的,從左邊開始找比5大的數填入:
同理,此時左邊又有了空位繼續從右邊開始找比5小的數填過去,以此類推
最后要找比5大的數放到右邊去,但是3<5,這時left和right重合了,此時說明位置已經在中間了,將5放回。
(3)歸位代碼實現
def partition(li, left, right): """ 歸位函數 :param li: 列表 :param left: 左箭頭 :param right: 右箭頭 :return: """ tmp = li[left] while left < right: while left < right and li[right] >= tmp: # 從右邊找一個比tmp小的數放過來 # 注意由於循環條件是li[right] >= tep,在兩個箭頭相遇時不會退出循環,因此添加left<right條件 right -= 1 # 如果比tmp大則right往左走一步 li[left] = li[right] # 將右邊找的數插入到左邊空位處 print(li) # 打印排序過程 while left<right and li[left] <= tmp: # 從左邊找一個比tmp大的數放入右邊的空位 left += 1 # 如果比tmp小則left往右走一步 li[right] = li[left] # 將左邊的值寫入到右邊空位處 print(li) # 打印排序過程 # 循環終止條件:left>=right li[left] = tmp # 將tmp歸位 li = [5,7,4,6,3,1,2,9,8] print("原列表", li) partition(li, 0, len(li)-1) print("排序結果", li) """ 原列表 [5, 7, 4, 6, 3, 1, 2, 9, 8] [2, 7, 4, 6, 3, 1, 2, 9, 8] [2, 7, 4, 6, 3, 1, 7, 9, 8] [2, 1, 4, 6, 3, 1, 7, 9, 8] [2, 1, 4, 6, 3, 6, 7, 9, 8] [2, 1, 4, 3, 3, 6, 7, 9, 8] [2, 1, 4, 3, 3, 6, 7, 9, 8] 排序結果 [2, 1, 4, 3, 5, 6, 7, 9, 8] """
注意無論從左邊找還是從右邊找,都需要添加left<right條件,在箭頭相遇時跳出循環。還可以注意到每次寫入空位,並不是真正的空位,仍由原元素占位在空位出,直到tmp歸位,整個列表才沒有了重復的元素。
(4)快速排序代碼實現
def partition(li, left, right): """ 歸位函數 :param li: 列表 :param left: 左箭頭 :param right: 右箭頭 :return: """ tmp = li[left] while left < right: while left < right and li[right] >= tmp: # 從右邊找一個比tmp小的數放過來 # 注意由於循環條件是li[right] >= tep,在兩個箭頭相遇時不會退出循環,因此添加left<right條件 right -= 1 # 如果比tmp大則right往左走一步 li[left] = li[right] # 將右邊找的數插入到左邊空位處 print(li) # 打印排序過程 while left<right and li[left] <= tmp: # 從左邊找一個比tmp大的數放入右邊的空位 left += 1 # 如果比tmp小則left往右走一步 li[right] = li[left] # 將左邊的值寫入到右邊空位處 print(li) # 打印排序過程 # 循環終止條件:left>=right li[left] = tmp # 將tmp歸位 return left def quick_sort(li, left, right): """快速排序兩個關鍵:歸位、遞歸""" if left < right: # 至少有兩個元素 mid = partition(li, left, right) quick_sort(li, left, mid-1) quick_sort(li, mid+1, right) li = [5,7,4,6,3,1,2,9,8] quick_sort(li, 0, len(li)-1) print(li)
注意這里使用了partition歸位函數和快速排序遞歸框架完成了快速排序設計。
(5)快速排序的效率
快速排序的時間復雜度:O(nlogn),每一層排序的復雜度是O(n),總共有logn層。
(6)快速排序改寫
想給quick_sort添加裝飾器查看排序運行效率,但是遞歸函數不能添加裝飾器,因此需要做如下改寫:
from cal_time import * def partition(li, left, right):...... def _quick_sort(li, left, right): """快速排序兩個關鍵:歸位、遞歸""" if left < right: # 至少有兩個元素 mid = partition(li, left, right) _quick_sort(li, left, mid-1) _quick_sort(li, mid+1, right) @cal_time def quick_sort(li): _quick_sort(li, 0, len(li)-1)
(7)測試驗證快排和冒泡排序執行效率

# -*- coding:utf-8 -*- __author__ = 'Qiushi Huang' import random from cal_time import * import copy # 復制模塊 def partition(li, left, right): """ 歸位函數 :param li: 列表 :param left: 左箭頭 :param right: 右箭頭 :return: """ tmp = li[left] while left < right: while left < right and li[right] >= tmp: # 從右邊找一個比tmp小的數放過來 # 注意由於循環條件是li[right] >= tep,在兩個箭頭相遇時不會退出循環,因此添加left<right條件 right -= 1 # 如果比tmp大則right往左走一步 li[left] = li[right] # 將右邊找的數插入到左邊空位處 # print(li) # 打印排序過程 while left<right and li[left] <= tmp: # 從左邊找一個比tmp大的數放入右邊的空位 left += 1 # 如果比tmp小則left往右走一步 li[right] = li[left] # 將左邊的值寫入到右邊空位處 # print(li) # 打印排序過程 # 循環終止條件:left>=right li[left] = tmp # 將tmp歸位 return left def _quick_sort(li, left, right): """快速排序兩個關鍵:歸位、遞歸""" if left < right: # 至少有兩個元素 mid = partition(li, left, right) _quick_sort(li, left, mid-1) _quick_sort(li, mid+1, right) @cal_time def quick_sort(li): _quick_sort(li, 0, len(li)-1) @cal_time def bubble_sort(li): for i in range(len(li)-1): # 總共是n-1趟 exchange = False for j in range(len(li)-i-1): # 每一趟都有箭頭,從0開始到n-i-1 if li[j] > li[j+1]: # 比對箭頭指向和箭頭后面的那個數的值 # 當箭頭所指數大於后面的數時交換位置, 升序排列;條件相反則為降序排列 li[j], li[j+1] = li[j+1], li[j] exchange = True # 如果發生了交換就置為true # print(li) if not exchange: # 如果exchange還是False,說明沒有發生交換,結束代碼 return li = list(range(10000)) random.shuffle(li) li1 = copy.deepcopy(li) # 深拷貝 li2 = copy.deepcopy(li) quick_sort(li1) bubble_sort(li2) """ quick_sort running time: 0.03162503242492676 secs. bubble_sort running time: 10.773478269577026 secs. """ print(li1) # [0, 1, 2, 3, 4,..., 9997, 9998, 9999] print(li2)
對比運行時間,可以發現針對10000個元素的數組排序,快速排序的效率比冒泡排序高了幾百倍。
時間復雜度O(nlogn)和O(n2)在數量越大的情況下,效率相差將越來越大。
快速排序的最好情況時間復雜度是O(n),一般情況時間復雜度是O(nlogn),最壞情況時間復雜度是O(n2)。
(8)快速排序存在的問題
首先python有一個遞歸最大深度的問題,默認是999,修改遞歸最大深度方法:
import sys sys.setrecursionlimit(100000) # 修改遞歸最大深度
雖然可以修改;而且遞歸會相當消耗一部分的系統資源。
其次快速排序有一個最壞情況出現:倒序排列的數組,在這種情況下,快速排序無法兩邊同時排序,每次只能排序一個數字。因此在這種情況下快速排序的時間復雜度是:O(n2)。
加入隨機化解決該問題:即不再找第一個元素歸位,而是隨機找一個值與第一個元素交換,然后繼續執行快速排序,就可以解決倒序例子時間復雜度特別高的情況。但是這個方法不能完全避免最壞情況,比如每次隨機都恰好選中了最大的一個數,但是這種修改可以讓最壞情況無法被設計出來,發生最壞情況的概率也會非常非常小。
5、堆排序(Heap-Sort)
6、歸並排序(Merge-Sort)
三、排序總結
1、冒泡排序、選擇排序、插入排序
冒泡排序、選擇排序、插入排序的時間復雜度都是O(n2),且都是原地排序。
2、快速排序、堆排序、歸並排序
快速排序、堆排序、歸並排序這三種排序算法的時間復雜度都是O(nlogn)。 但有常數差異。
(1)一般情況下,就運行時間來比較:
快速排序(速度最快)< 歸並排序 < 堆排序
(2)三種排序算法的缺點:
快速排序:極端情況下排序效率低。
歸並排序:需要額外的內存開銷。
堆排序:在快的排序算法中相對較慢。
3、六種排序算法對比總結
(1)遞歸占用空間
遞歸需要用系統占的空間,快速排序在平均情況下需要遞歸logn層,所以平均情況下需要消耗O(logn)的空間復雜度;最壞情況下需要遞歸n層,因此需要消耗O(n)的時間復雜度。
歸並雖然也有遞歸,但他已經開了一個列表了占用O(n),歸並遞歸需要的空間復雜度是O(logn)小於O(n),因此統計空間復雜度是O(n)。
(2)排序算法穩定性
假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱為不穩定的。
判斷是否算法是否穩定:挨着換的穩定,不挨着換的不穩定。
(3)代碼復雜度
算法是否好寫,是否容易理解。