目錄
LOW B 三人組
NB 三人組
其他
列表排序即將無需列表變為有序,Python的內置函數為sort()。應用的場景主要有:各種榜單、各種表格、給二分查找用、 其他算法用等等。
有關列表排序的算法有很多,主要分為:
- low B三人組: 冒泡排序、 選擇排序、 插入排序
-
NB三人組: 快速排序、 堆排序、 歸並排序
-
其他排序算法: 計數排序、 希爾排序、 桶排序
算法排序的關鍵點在於有序區和無序區,我們將一個待排序的列表定為無序區,依次取出其中的元素進行排序,用於存放已排好序的元素的區域稱為有序區
為了更形象的表示出每個排序算法的用時,我們先寫一個用於計算時間的裝飾器預備上

#在timewrap.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
Low B 三人組
Low B三人組分別指冒泡排序、 選擇排序、 插入排序
冒泡排序(Bubble Sort)的思想(這里用升序舉例,即排序后的結果為從小到大)是將一個待排序的列表理解為垂直結構,索引為0的元素在最下面。然后從索引為0的位置的元素開始,一次向上比較,若大於上面一個元素則兩個元素交換位置(可以理解為下面的泡泡冒了上來),直到遇到比它大的元素或到達最頂端(即該元素為列表中的最大值)后停止。若該數到達最頂端,則繼續由索引為0的元素重復上述冒泡運動;若遇到更大的元素,則由該大元素向上冒。冒泡排序總的平均時間復雜度為 ,空間復雜度:O(1)
-
比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
-
對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最后一對。在這一點,最后的元素應該會是最大的數。
-
針對所有的元素重復以上的步驟,除了最后一個。
-
持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較。
冒泡排序是把小的元素往前調或者把大的元素往后調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。所以,如果兩個元素相等,是不會發生交換的;如果兩個相等的元素沒有相鄰,那么即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前后順序並沒有改變,所以冒泡排序是一種穩定排序算法。
文字擼不明白的可看原理圖,如下:
知道了原理后我們來寫代碼
def bubble_sort(li): for i in range(len(li)-1):#i是索引,表示趟數,第i趟時無序區(0,len(li)-i) for j in range(len(li)-i-1):#j是除去i個元素后的列表的索引(循環進行了幾次就說明有幾個元素已經被排好序) if li[j] > li[j+1]: li[j], li[j+1] = li[j+1], li[j]
如果冒泡排序執行了一趟而沒有交換發生,說明該列表已經是有序狀態,可以直接結束算法。所以我們可以將上述代碼進行優化:
import random from timewrap import * @cal_time def bubble_sort_2(li): for i in range(len(li) - 1): # i 表示趟數 # 第 i 趟時: 無序區:(0,len(li) - i) change = False for j in range(0, len(li) - i - 1): if li[j] > li[j+1]: li[j], li[j+1] = li[j+1], li[j] change = True if not change: return li = list(range(10000)) bubble_sort_2(li)#bubble_sort_2 running time: 0.0010001659393310547 secs. print(li)#0~9999已排好序
選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理是每一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,直到全部待排序的數據元素排完。 選擇排序是不穩定的排序方法(比如序列[5, 5, 3]第一次就將第一個[5]與[3]交換,導致第一個5挪動到第二個5后面)。選擇排序總的平均時間復雜度為 ,空間復雜度:O(1)
思想:一趟遍歷記錄最小的數,放到第一個位置; 再一趟遍歷記錄剩余列表中最小的數,繼續放置;
選擇排序是給每個位置選擇當前元素最小的,比如給第一個位置選擇最小的,在剩余元素里面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個元素不用選擇了,因為只剩下它一個最大的元素了。那么,在一趟選擇,如果一個元素比當前元素小,而該小的元素又出現在一個和當前元素相等的元素后面,那么交換后穩定性就被破壞了。比較拗口,舉個例子,序列5 8 5 2 9,我們知道第一遍選擇第1個元素5會和2交換,那么原序列中兩個5的相對前后順序就被破壞了,所以選擇排序是一個不穩定的排序算法。
選擇排序代碼如下:
import random from timewrap import * @cal_time def select_sort(li): for i in range(len(li) - 1): # i 表示趟數,也表示無序區開始的位置 min_loc = i # 最小數的位置 for j in range(i + 1, len(li) - 1):#去除已經歸為的最小數 if li[j] < li[min_loc]: min_loc = j li[i], li[min_loc] = li[min_loc], li[i] li = list(range(10000)) select_sort(li)#select_sort running time: 9.220226049423218 secs. print(li)#0~9999已排好序
思路:列表被分為有序區和無序區兩個部分。最初有序區只有一個元素。 每次從無序區選擇一個元素,插入到有序區的位置,直到無序區變空。插入排序總的平均時間復雜度為 ,空間復雜度:O(1)
可以理解為撲克牌抓牌的過程
基本代碼如下:
import random from timewrap 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: #循環終止條件: 1. li[j] <= tmp; 2. j == -1 li[j+1] = li[j] j -= 1 li[j+1] = tmp li = list(range(10000)) insert_sort(li)#insert_sort running time: 0.003001689910888672 secs. print(li)#0~9999已排好序
NB 三人組
NB三人組分別是: 快速排序、 堆排序、 歸並排序
快速排序(Quicksort)是對冒泡排序的一種改進。它的基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然后再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
原理圖如下:
示例代碼如下:
import random from timewrap import * import copy import sys sys.setrecursionlimit(100000)#修改遞歸最大深度,默認為997 def partition(li, left, right): # ri = random.randint(left, right) # li[left], li[ri] = li[ri], li[left] tmp = li[left] while left < right: while left < right and li[right] >= tmp: right -= 1#找下一個 li[left] = li[right]#while條件不成立,說明右邊比temp小,右邊數與temp的位置交換 while left < right and li[left] <= tmp: left += 1 li[right] = li[left]#while條件不成立,說明左邊比temp大,左邊數與temp的位置交換 li[left] = 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): return _quick_sort(li, 0, len(li)-1) li = list(range(10000)) # random.shuffle(li)#為防止最壞情況發生,最好先用這局代碼完全打亂列表順序 quick_sort(li) print(li)
快速排序的最壞情況
快排的運行時間依賴於划分是否平衡,而平衡與否又依賴於用戶划分的主元素。
- 如果划分是平衡的,那么快速排序算法性能與歸並排序一樣。
- 如果划分時不平衡的,那么快速排序的性能就接近於插入排序了
因此,快排的最壞情況的發生與快速排序中主元素的選擇是有重大的關系;當主元素是最小元素或最大元素時會使快排性能最差
堆排序(Heapsort)是指利用堆積樹(堆)這種數據結構所設計的一種排序算法,它是選擇排序的一種。可以利用數組的特點快速定位指定索引的元素。
堆的時間復雜度是O(N*logN),空間復雜度是O(1),且是一種不穩定的排序方式。
在了解堆排序之前我們首先要掌握有關完全二叉樹的知識點,二叉樹博客地址:http://www.cnblogs.com/zhuminghui/p/8409508.html
堆、是一個完全二叉樹的數據類型,堆根據數據結構的不同可以分為大根堆和小根堆
大根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點大
小根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點小
大根堆 小根堆
堆排序的核心就是要構造堆,將數據構造成堆經過以下步驟就可以得到有序的數據:
- 建立堆
- 得到堆頂元素,為最大元素
- 去掉堆頂,將堆最后一個元素放到堆頂,
- 此時可通過一次調整重新使堆有序。
- 堆頂元素為第二大元素。 重復步驟3,直到堆變空。
假設我們有這樣一個數據結構:
首先我們要構造堆:
然后挨個出數(注意每次都要構造堆):
用代碼實現:
from timewrap import * import random def sift(li, low, high): """ 構造堆的過程 :param li: :param low: 堆根節點的位置 :param high: 堆最后一個節點的位置 :return: """ i = low # 父親的位置 j = 2 * i + 1 # 孩子的位置 tmp = li[low] # 最原來的根的值 while j <= high: if j + 1 <= high and li[j+1] > li[j]: # 如果右孩子存在並且右孩子更大 j += 1 if tmp < li[j]: # 如果最原來的根的值比孩子小 li[i] = li[j] # 把孩子向上移動一層 i = j j = 2 * i + 1 else: break li[i] = tmp# 最原來的根的值放到對應的位置上(葉子節點) @cal_time def heap_sort(li): n = len(li) # 1. 建堆 for i in range(n//2-1, -1, -1): sift(li, i, n-1) # 2. 挨個出數 for j in range(n-1, -1, -1): # j表示堆最后一個元素的位置 li[0], li[j] = li[j], li[0] # 堆的大小少了一個元素 (j-1) sift(li, 0, j-1) li = list(range(10000)) random.shuffle(li) heap_sort(li)#heap_sort running time: 0.07304835319519043 secs. print(li)#0~9999已排好序
Python中內置的堆排序模塊
在Python中堆排序有一個內置模塊——heapq模塊,利用它我們可以快速實現一個堆排序
import heapq, random li = [5,8,7,6,1,4,9,3,2] heapq.heapify(li)#將列表轉化為堆 print(li)#[1, 2, 4, 3, 8, 7, 9, 5, 6] print(heapq.heappop(li))#彈出堆的最小值 1 print(heapq.heappop(li))#彈出堆的最小值 2 heapq.heappush(li,10)#插入一個值 print(li)#[3, 5, 4, 6, 8, 7, 9, 10]

import heapq, random def heap_sort(li): heapq.heapify(li) n = len(li) new_li = [] for i in range(n): new_li.append(heapq.heappop(li)) return new_li li = list(range(10000)) random.shuffle(li) li = heap_sort(li) print(li)#從小到大排序 #內置方法直接一行代碼解決問題 print(heapq.nsmallest(100, li))#從小到大排序 print(heapq.nlargest(100, li))#從大到小排序
堆排序例題
現在有n個數,設計算法找出前k大的數(k<n)。
思路:取列表前k個元素(假設k=5)建立一個小根堆。堆頂就是目前這k個數中最小的數。 依次向后遍歷原列表,對於列表中的元素,如果小於堆頂,則忽略該元素;如果大於堆頂,則將堆頂更換為該元素,並且對堆進行一次調整,使得堆頂永遠為目前k個數中的最小數。直到遍歷完列表所有元素后,倒序彈出堆頂。

li=[6,8,1,9,3,0,7,2,4,5] def topk(li,k): heap=li[0:k] for i in range(k//2-1,-1,-1): sift(heap,i,k-1) for i in range(k,len(li)): if li[i] > heap[0]: heap[0]=li[i] sift(heap,0,k-1) for i in range(k-1,-1,-1): heap[0],heap[i]=heap[i],heap[0] sift(heap,0,i-1)
歸並排序(MERGE-SORT)是建立在歸並操作上的一種有效的排序算法,該算法是采用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合並,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合並成一個有序表,稱為二路歸並
歸並過程為:比較a[i]和b[j]的大小,若a[i]≤b[j],則將第一個有序表中的元素a[i]復制到r[k]中,並令i和k分別加上1;否則將第二個有序表中的元素b[j]復制到r[k]中,並令j和k分別加上1,如此循環下去,直到其中一個有序表取完,然后再將另一個有序表中剩余的元素復制到r中從下標k到下標t的單元。歸並排序的算法我們通常用遞歸實現,先把待排序區間[s,t]以中點二分,接着把左邊子區間排序,再把右邊子區間排序,最后把左區間和右區間用一次歸並操作合並成有序的區間[s,t]。。
一次歸並的代碼如下:
def merge(li, low, mid, high): i = low j = mid + 1 ltmp = [] while i <= mid and j <= high:#列表被分為了[low:mid+1],[mid+1:high]兩部分 #分別取兩段的小的部分 if li[i] < li[j]: ltmp.append(li[i]) i += 1 else: ltmp.append(li[j]) j += 1 while i <= mid:#右取完了段 ltmp.append(li[i]) i += 1 while j <= high:#左段取完了 ltmp.append(li[j]) j += 1 li[low:high+1] = ltmp
有時列表的復雜度會比較大,這時我們就需要做好幾次歸並操作才能使得列表有序,這時我們可以用到遞歸。
基本思路:
分解:將列表越分越小,直至分成一個元素。
終止條件:一個元素是有序的。
合並:將兩個有序列表歸並,列表越來越大。
使用遞歸使得列表有序:
import random from timewrap import * import copy import sys def merge(li, low, mid, high): i = low j = mid + 1 ltmp = [] while i <= mid and j <= high:#列表被分為了[low:mid+1],[mid+1:high]兩部分 #分別取兩段的小的部分 if li[i] < li[j]: ltmp.append(li[i]) i += 1 else: ltmp.append(li[j]) j += 1 while i <= mid:#右取完了段 ltmp.append(li[i]) i += 1 while j <= high:#左段取完了 ltmp.append(li[j]) j += 1 li[low:high+1] = ltmp def _merge_sort(li, low, high): if low < high: # 至少兩個元素 mid = (low + high) // 2 _merge_sort(li, low, mid) _merge_sort(li, mid+1, high) merge(li, low, mid, high) print(li[low:high+1]) @cal_time def merge_sort(li): # 因為函數要進行遞歸,無法直接安裝飾器,所以在外面加個殼。 # 不使用裝飾器的話不用寫這個函數,直接用上面的函數就可以 return _merge_sort(li, 0, len(li)-1) li = list(range(16)) random.shuffle(li) merge_sort(li) print(li)
NB 三人組小結
- 三種排序算法的時間復雜度都是O(nlogn)
- 一般情況下,就運行時間而言: 快速排序 < 歸並排序 < 堆排序
- 三種排序算法的缺點:
-
- 快速排序:極端情況下排序效率低
- 歸並排序:需要額外的內存開銷
- 堆排序:在快的排序算法中相對較慢
-
前面六種算法的復雜度總結
其他排序算法
這里補充兩個排序算法——希爾排序和計數算法
基本思想:
首先取一個整數d1=n/2,將元素分為d1個組,每組相鄰量元素之間距離為d1,在各組內進行直接插入排序; 取第二個整數d2=d1/2,重復上述分組排序過程,直到di=1,即所有元素在同一組內進行直接插入排序。
擼不懂文字的看圖:
基本代碼實現:
def shell_sort(li): d = len(li) // 2#d1 while d > 0: for i in range(d, len(li)): tmp = li[i] j = i - d#j=1 2 3... while li[j] > tmp and j >= 0: li[j+d] = li[j]#交換 j -= d li[j+d] = tmp d = d >> 1# y>>x 符號表示將y轉化成二進制數后砍掉最后x位,效果與 y/= x 一樣
計數排序是一個非基於比較的排序算法,該算法於1954年由 Harold H. Seward 提出。它的優勢在於在對一定范圍內的整數排序時,它的復雜度為Ο(n+k)(其中k是整數的范圍),快於任何比較排序算法。
當然計數排序是一種犧牲空間換取時間的算法,而且當O(k)>O(n*log(n))的時候其效率反而不如基於比較的排序(基於比較的排序的時間復雜度在理論上的下限是O(n*log(n)), 如歸並排序,堆排序)
基本代碼實現:
import random from timewrap import * @cal_time def count_sort(li, max_num = 100): count = [0 for i in range(max_num+1)]#[0,0,0,0,0,0,...] for num in li: count[num]+=1#li中每有一個元素,就在count中下標為該元素的位置加一,最后得到的就是下標位置(表示li的元素值)是幾(表示li中該元素的個數) li.clear()#清空li for i, val in enumerate(count): for _ in range(val): li.append(i)#將count中不為0的元素的索引值一個一個加到li中,得到的li就是排好序的li li = [random.randint(0,100) for i in range(100000)] count_sort(li)