排序就是將一組「無序」的記錄序列調整爲「有序」的記錄序列。html
列表排序:將無序列表變爲有序列表。python
輸入:列表算法
輸出:有序列表數組
兩種基本的排序方式:升序和降序。數據結構
python內置的排序函數:sort()。app
名稱框架 |
複雜度dom |
說明ide |
備註函數 |
冒泡排序 |
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 個箱子裏 ( 分配 ) ,而後按序號依次將各非空的箱子首尾鏈接起來 ( 收集 ) 。 |
分配排序的一種:經過" 分配 " 和 " 收集 " 過程來實現排序。 |
列表每兩個相鄰的數,若是前面比後面大,則交換這兩個數。
一趟排序完成後,則無序區減小一個數,有序區增長一個數。
代碼關鍵點:趟、無序區範圍。
這樣排序一趟後,最大的數9,就到了列表最頂成爲了有序區,下面的部分則仍是無序區。而後在無序區不斷重複這個過程,每完成一趟排序,無序區減小一個數,有序區增長一個數。圖示最後一張圖要開始第六趟排序,排序從第0趟開始計數。剩一個數的時候不須要排序了,所以整個排序排了n-1趟。
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] """
n是列表的長度,算法中也沒有發生循環折半的過程,具有兩層關於n的循環,所以它的時間複雜度是O(n2)。
若是在一趟排序過程當中沒有發生交換就能夠認定已經排好序了。所以可作以下優化:
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] """
對比前面排序的次數少了不少,算法獲得了優化~
一趟遍歷完記錄最小的數,放到第一個位置;再一趟遍歷記錄剩餘列表中的最小的數,繼續放置。
算法關鍵點:有序區和無序區、無序區最小數的位置。
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)。
並且前面冒泡排序是原地排序不須要開啓一個新的列表,二這個版本的選擇排序不是原地排序,多佔了一分內存。
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)。
元素被分爲有序區和無序區兩部分。初始時手裏(有序區)只有一張牌,每次(從無序區)摸一張牌,插入到手裏已有牌的正確位置,直到無序區變空。
一開始手裏的牌只有5
第一張摸到的牌是7,比5大插到5的右邊:
第二張摸到的牌是4,須要將5和7的位置向右挪,將4插到最前面:
後面的狀況依次類推。
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)。
準備好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. """
快速排序思路:取一個元素p(第一個元素),使元素p歸位;列表被p分爲兩部分,左邊都比p小,右邊都比p大;遞歸完成排序。
算法關鍵點:歸位、遞歸。
5要歸位,先用一個變量將5存起來,兩個箭頭表示當前列表的left和right:
列表左邊有了一個空位,從右邊開始找一個比5小的數填入:
此時右邊有了一個空位,右邊是給比5大的數準備的,從左邊開始找比5大的數填入:
同理,此時左邊又有了空位繼續從右邊開始找比5小的數填過去,以此類推
最後要找比5大的數放到右邊去,可是3<5,這時left和right重合了,此時說明位置已經在中間了,將5放回。
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歸位,整個列表纔沒有了重複的元素。
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歸位函數和快速排序遞歸框架完成了快速排序設計。
快速排序的時間複雜度:O(nlogn),每一層排序的複雜度是O(n),總共有logn層。
想給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)
# -*- 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)。
首先python有一個遞歸最大深度的問題,默認是999,修改遞歸最大深度方法:
import sys sys.setrecursionlimit(100000) # 修改遞歸最大深度
雖然能夠修改;並且遞歸會至關消耗一部分的系統資源。
其次快速排序有一個最壞狀況出現:倒序排列的數組,在這種狀況下,快速排序沒法兩邊同時排序,每次只能排序一個數字。所以在這種狀況下快速排序的時間複雜度是:O(n2)。
加入隨機化解決該問題:即再也不找第一個元素歸位,而是隨機找一個值與第一個元素交換,而後繼續執行快速排序,就能夠解決倒序例子時間複雜度特別高的狀況。可是這個方法不能徹底避免最壞狀況,好比每次隨機都剛好選中了最大的一個數,可是這種修改可讓最壞狀況沒法被設計出來,發生最壞狀況的機率也會很是很是小。
冒泡排序、選擇排序、插入排序的時間複雜度都是O(n2),且都是原地排序。
快速排序、堆排序、歸併排序這三種排序算法的時間複雜度都是O(nlogn)。 但有常數差別。
快速排序(速度最快)< 歸併排序 < 堆排序
快速排序:極端狀況下排序效率低。
歸併排序:須要額外的內存開銷。
堆排序:在快的排序算法中相對較慢。
遞歸須要用系統佔的空間,快速排序在平均狀況下須要遞歸logn層,因此平均狀況下須要消耗O(logn)的空間複雜度;最壞狀況下須要遞歸n層,所以須要消耗O(n)的時間複雜度。
歸併雖然也有遞歸,但他已經開了一個列表了佔用O(n),歸併遞歸須要的空間複雜度是O(logn)小於O(n),所以統計空間複雜度是O(n)。
假定在待排序的記錄序列中,存在多個具備相同的關鍵字的記錄,若通過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]以前,而在排序後的序列中,r[i]仍在r[j]以前,則稱這種排序算法是穩定的;不然稱爲不穩定的。
判斷是否算法是否穩定:挨着換的穩定,不挨着換的不穩定。
算法是否好寫,是否容易理解。