排序算法經過長時間演變,大體可以分為兩類:內排序和外排序。在排序過程中,全部記錄存放在內存,則成為內排序;如果排序過程中需要使用外存,則稱為外排序,本文講的都屬於內排序。
內排序有可以分為以下幾類:
(1)插入排序:直接插入排序、二分法插入排序、希爾排序
(2)選擇排序:直接選擇排序、堆排序
(3)交換排序:冒泡排序、快速排序
(4)歸並排序
(5)基數排序
排序方法 | 時間復雜度(平均) | 時間復雜度(最壞) | 時間復雜度(最好) | 空間復雜度 | 穩定性 | 復雜性 |
直接插入排序 | O(n2) | O(n2) | O(n) | O(1) | 穩定 | 簡單 |
希爾排序 | O(nlog2n) | O(n2) | O(n1.3) | O(1) | 不穩定 | 較復雜 |
直接選擇排序 | O(n2) | O(n2) | O(n2) | O(1) | 不穩定 | 簡單 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不穩定 | 較復雜 |
冒泡排序 | O(n2) | O(n2) | O(n) | O(1) | 穩定 |
簡單 |
快速排序 | O(nlog2n) | O(n2) | O(nlog2n) | O(nlog2n) | 不穩定 | 較復雜 |
歸並排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 穩定 |
較復雜 |
基數排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(n+r) | 穩定 |
較復雜 |
一、插入排序
•思想:每步將一個待排序的記錄,按其順序碼大小插入到前面已經排序的字序列的合適位置,直到全部插入排序完為止。
•關鍵問題:在前面已經排好序的序列中找到合適的插入位置。
•方法:
直接插入排序
- 插入排序的最好情況是數組已經有序,此時只需要進行n-1次比較,時間復雜度為O(n)
- 最壞情況是數組逆序排序,此時需要進行n(n-1)/2次比較以及n-1次賦值操作(插入)
- 平均來說插入排序算法的復雜度為O(n2)
- 空間復雜度上,直接插入法是就地排序,空間復雜度為(O(1))
二分插入排序
- 最壞情況:每次都在有序序列的起始位置插入,則整個有序序列的元素需要后移,時間復雜度為O(n2)
- 最好情況:待排序數組本身就是正序的,每個元素所在位置即為它的插入位置,此時時間復雜度僅為比較時的時間復雜度,為O(log2n)
- 平均情況:O(n2)
- 空間復雜度上,二分插入也是就地排序,空間復雜度為(O(1))。
希爾排序
- 增量排序的時間復雜度依賴於所取增量序列的函數,但是到目前為止還沒有一個最好的增量序列.有人在大量的實驗后得出結論;當n在某個特定的范圍后希爾排序的比較和移動次數減少至n^1.3 不管增量序列如何取值,都應該滿足最后一個增量值為1。
- 有文獻指出,當增量序列為d[k]=2^(t-k+1)時,希爾排序的時間復雜度為O(n^1.5), 其中t為排序趟數。
- 空間復雜度上,二分插入也是就地排序,空間復雜度為(O(1))。
1.直接插入排序
(1)基本思想:每步將一個待排序的記錄,按其順序碼大小插入到前面已經排序的字序列的合適位置(從后向前找到合適位置后),直到全部插入排序完為止。(是一種最簡單的排序方法,其基本操作是將一條記錄插入到已排好的有序表中,從而得到一個新的、記錄數量增1的有序表。)
(2)實例
(3)python實現

def InsertSort(list_): for i in range(1,len(list_)): #從列表的第二個元素開始找 j = i-1 #先定位第i個元素的前一個元素 if list_[j] > list_[i]: #如果前一個元素比第i個元素大 temp = list_[i] #前一個元素比i元素大,所以i元素需要往前插入,故先把list_[i]賦值給一個臨時變量 list_[j+1] = list_[j] #i元素插入后,其前一個元素即j元素肯定向后位移一位 # 繼續往前尋找,如果有比臨時變量大的數字,則后移一位,直到找到比臨時變量小的元素或者達到列表第一個元素 j -= 1 while j >= 0 and list_[j] > temp: list_[j+1] = list_[j] j -= 1 list_[j+1] = temp #只要找到比temp小的元素,就將temp插入到其后一位(原本其后面一位元素已經右移走掉了), #或者i元素前面所有元素都比它大,則j循環到-1時,temp被賦值給了list_[0]即達到列表首位 return list_
2.二分插入排序
(1)基本思想:二分法插入排序的思想和直接插入一樣,只是找合適的插入位置的方式不同,這里是按二分法找到合適的位置,可以減少比較的次數。
(2)實例
(3)python實現
二分排序算法只對於事先排好序的算法有效故在使用二分排序算法之前要對樣本序列進行排序,具體可描述為以下步驟:
1.從第一個元素開始,該元素可以被認為已經被排序
2.取出下一個元素,在已經排序好的元素序列中(即該被取出元素之前的所有元素)二分查找到第一個比它小的元素的位置
3.將被取出元素插入到該元素的位置后
4.重復
3.希爾排序
(1)基本思想:希爾排序又叫“縮小增量排序”,先取一個小於n的整數d1作為第一個增量,把文件的全部記錄分成d1個組。所有距離為d1的倍數的記錄放在同一個組中。先在各組內進行直接插入排序,然后取第二個增量d2。其是插入排序改良的算法,希爾排序步長從大到小調整,第一次循環后面元素逐個和前面元素按間隔步長進行比較並交換,直至步長為1,步長選擇是關鍵。
(2)實例
(3)python實現

def ShellSort(list_): dk = int(len(list_)) #dk是增量,即步長 while dk > 0: dk = dk//2 #增量遞減 for i in range(dk): #對應增量的該輪排序進行中 for j in range(i,int(len(list_)),dk): temp = list_[j] while j > 0 and list_[j-dk] > temp: list_[j] = list_[j-dk] j -= dk list_[j] = temp return list_
二、選擇排序
•思想:每趟從待排序的記錄序列中選擇關鍵字最小的記錄放置到已排序表的最前位置,直到全部排完。
•關鍵問題:在剩余的待排序記錄序列中找到最小關鍵碼記錄。
•方法:
直接選擇排序
- 選擇排序第一輪內循環比較n-1次,然后是n-2次、n-3次........最后一輪內循環比較1次,共(n-1)+(n-2)+....+3+2+1=(n-1+1)n/2=n^2/2,其時間復雜度為O(n2)
- 空間復雜度就是在交換元素時那個臨時變量所占的內存空間,空間復雜度為O(1)
堆排序
- 堆排序的時間復雜度主要由兩部分組成:初始化建堆和每次彈出堆頂元素后重新建堆的過程
-
初始化建堆過程的時間復雜度O(n):假設堆的高度為k,則從倒數第二層右邊的節點開始,這一層的節點都要進行子節點比較然后選擇是否交換,倒數第三層類似,一直到第一層(即層數從k-1到1);那么總的時間為(2^(i-1))*(k-i),其中i表示第i層(范圍是k-1到1),2^(i-1)表示該層上有多少元素,(k-i)表示子樹上要比較的次數,即S = 2^(k-2)*1 + 2^(k-3)*2 + 2^(k-4)*3 + ... + 2^1*(k-2) + 2^0*(k-1),使用錯位相減法(用常數2來輔助轉換,兩邊都乘以2再減去原等式)得到S = 2^(K-1) + 2^(K-2) + 2^(K-3) + ... + 2 - (K-1),忽略最后一項常數項就是等比數列,即S=2^k-2-(k-1)=2^k-k-1,又因為k為完全二叉樹的深度,所以有 2^k <= n < 2^k-1,可以認為k = logn,綜上所述S = n - logn -1,所以時間復雜度為O(n)
- 彈出堆頂元素后重建堆過程的時間復雜度O(nlogn):循環n-1次,每次都從跟節點往下循環查找所以每一次時間都是logn,總時間為(n-1)*logn = nlogn - logn
- 故堆排序的時間復雜度為O(n) + O(nlogn) = O(nlogn)
- 堆排序是接地排序,所以空間復雜度為常數O(1)
1.直接選擇排序
(1)基本思想:在要排序的一組數中,選出最小的一個數與第一個位置的數交換;然后在剩下的數當中再找最小的與第二個位置的數交換,如此循環到倒數第二個數和最后一個數比較為止。
(2)實例
(3)python實現

def SelectSort(list_): #保存當前最小的,初始化的時候,認為當前為最小,向后搜索比它小的元素 for i in range(len(list_)-1): temp = i #把當前元素腳標賦給臨時變量 for j in range(i+1,len(list_)): #將當前元素與后續元素依次比較 if list_[j] < list_[temp]: #入后后面元素比當前元素還要小 temp = j #交換腳標 list_[i],list_[temp] = list_[temp],list_[i] #交換值 return list_
2.堆排序
(1)基本思想:
堆排序是一種樹形選擇排序,是對直接選擇排序的有效改進。
堆的定義下:具有n個元素的序列 (h1,h2,…,hn),當且僅當滿足(hi>=h2i,hi>=2i+1)或(hi<=h2i,hi<=2i+1) (i=1,2,…,n/2)時稱之為堆。在這里只討論滿足前者條件的堆。由堆的定義可以看出,堆頂元素(即第一個元素)必為最大項(大頂堆)。完全二叉樹可以很直觀地表示堆的結構。堆頂為根,其它為左子樹、右子樹。 (可以延伸到前序遍歷、中序遍歷、后序遍歷)
思想:初始時把要排序的數的序列看作是一棵順序存儲的二叉樹,調整它們的存儲序,使之成為一個堆,這時堆的根節點的數最大。然后將根節點與堆的最后一個節點交換。然后對前面(n-1)個數重新調整使之成為堆。依此類推,直到只有兩個節點的堆,並對它們作交換,最后得到有n個節點的有序序列。從算法描述來看,堆排序需要兩個過程,一是建立堆,二是堆頂與堆的最后一個元素交換位置。所以堆排序有兩個函數組成。一是建堆的滲透函數,二是反復調用滲透函數實現排序的函數。
難點有(1)如何把一個序列生成大根堆
(2)輸出堆頂元素后,如何使剩下的元素生成一個大根堆
(2)實例
(3)python實現
根據堆排序的思想不難得知堆排序核心由兩部分組成,即“建堆”和“調整堆”,因此代碼主要包括這兩個過程的定義

def max_heapify(heap,heapSize,root): # 調整列表中的元素並保證以root為根的堆是一個大根堆 ''' 給定某個節點的下標root,這個節點的父節點、左子節點、右子節點的下標都可以被計算出來。 父節點:(root-1)//2 左子節點:2*root + 1 右子節點:2*root + 2 即:左子節點 + 1 ''' left = 2*root + 1 right = left + 1 larger = root if left < heapSize and heap[larger] < heap[left]: larger = left if right < heapSize and heap[larger] < heap[right]: larger = right if larger != root: # 如果做了堆調整則larger的值等於左節點或者右節點的值,這個時候做堆調整操作 heap[larger], heap[root] = heap[root], heap[larger] # 遞歸的對子樹做調整 max_heapify(heap, heapSize, larger) def build_max_heap(heap): # 構造一個堆,將堆中所有數據重新排序 heapSize = len(heap) for i in range((heapSize -2)//2,-1,-1): # 自底向上建堆 max_heapify(heap, heapSize, i) def heap_sort(heap): # 將根節點取出與最后一位做對調,對前面len-1個節點繼續進行堆調整過程。 build_max_heap(heap) # 調整后列表的第一個元素就是這個列表中最大的元素,將其與最后一個元素交換,然后將剩余的列表再遞歸的調整為最大堆 for i in range(len(heap)-1, -1, -1): heap[0], heap[i] = heap[i], heap[0] max_heapify(heap, i, 0) return heap
三、交換排序
•思想:利用交換元素的位置進行排序,每次兩兩比較待排序的元素,直到全部排完。
•關鍵問題:排序時要厘清需要進行幾輪排序。
•方法:
冒泡排序
- 最壞情況:冒泡排序要進行n-1輪排序循環,每輪排序循環中序列都是非正序的,則每輪排序循環中要進行n-i次比較(1<=i<=n-1),即其外循環執行n-1次,內循環最多執行n次,最少執行1次,由於內循環執行次數是線性的,故內循環平均執行(n+1)/2次,時間復雜度計算為((n-1)(n+1))/2=(-1)/2 ,時間復雜度為O(n2)
- 最好情況:待排序數組本身就是正序的,一輪掃描即可完成排序,此時時間復雜度僅為比較時的時間復雜度,為O(n)
- 平均情況:O(n2)
- 空間復雜度就是在交換元素時那個臨時變量所占的內存空間,最優的空間復雜度就是開始元素順序已經排好了,則空間復雜度為0,最差的空間復雜度就是開始元素逆序排序了,則空間復雜度為O(n),平均的空間復雜度為O(1)
快速排序
- 最好情況:是每輪划分都將待排序列正好分為兩部分,那么每部分需要的時間為上一輪的1/2。如果排序n個元素的序列,其遞歸樹深度為[logn]+1即僅需遞歸logn次,需要總時間為T(n)的話,第一次需要掃描整個序列,做n次比較,然后將序列一分為二,這兩部分各自還需要T(n/2)的時間,依次划分下去:T(n) = 2*T(n/2)+n T(n) = 2*(2*(T(n/4)+n/2)+n = 4*T(n/4)+2n 等等,且T(1) = 0,所以T(n) = n*T(1) + n*logn = O(nlogn)
- 最壞情況:當待排序列為有序序列(正序或倒序),每次划分后得到的情況是一側有1個元素,另一側是其余元素,則最終要進行n-1輪循環,且第i次循環要進行n-i次比較,總比較次數為n-1 + n-2 + ... + 1 = n(n-1)/2,即時間復雜度為O(n2)
- 空間復雜度待補充。。
1.冒泡排序
(1)基本思想:在要排序的一組數中,對當前還未排好序的范圍內的全部數,自上而下對相鄰的兩個數依次進行比較和調整,讓較大的數往下沉,較小的往上冒。即:每當兩相鄰的數比較后發現它們的排序與排序要求相反時,就將它們互換。依次比較相鄰的兩個數,將小數放在前面,大數放在后面。即在第一輪比較中:首先比較第1個和第2個數,將小數放前,大數放后;然后比較第2個數和第3個數,將小數放前,大數放后,如此繼續,直至比較最后兩個數,將小數放前,大數放后。重復第一輪的步驟,直至全部排序完成。
(2)實例
(3)python實現
第一輪比較完成后,確保了最后一個數是數組中最大的一個數,所以第二輪比較時,最后一個數不參與比較;
第二輪比較完成后,倒數第二個數也一定是數組中第二大的數,所以第三輪比較時,最后兩個數不參與比較;
依次類推,每一輪需要比較的次數-1;

def bub_sort(s_list): for i in range(len(s_list)-1): for j in range(len(s_list)-1-i): if s_list[j] > s_list[j+1]: s_list[j],s_list[j+1] = s_list[j+1],s_list[j] return s_list
2.快速排序
(1)基本思想:選擇一個基准元素,通常選擇第一個元素或者最后一個元素,通過一輪掃描,將待排序列分成兩部分,一部分比基准元素小,一部分大於等於基准元素,此時基准元素在其排好序后的正確位置,然后再用同樣的方法遞歸地排序划分的兩部分,直到各區間只有一個數。
(2)實例
(3)python實現

def QuickSort(list_): if len(list_) <= 1: return list_ # 左邊數組 left = [] # 右邊數組 right = [] # 基准數 base = list_.pop() # 對原數組進行划分 for x in list_: if x < base: left.append(x) else: right.append(x) # 遞歸調用 return QuickSort(left) + [base] + QuickSort(right)
四、歸並排序
- 時間復雜度:歸並排序主要分為拆分和對有序數組進行排序,拆分操作的時間復雜度為logn,排序的復雜度為n,所以歸並排序的時間復雜度為O(nlogn)
- 歸並排序的空間復雜度就是那個臨時數組和遞歸時壓如棧的數據占用的空間:n + logn,所以空間復雜度為O(n)
(1)基本思想:歸並(Merge)排序法是將兩個(或兩個以上)有序表合並成一個新的有序表,即把待排序序列分為若干個子序列,每個子序列是有序的。然后再把有序子序列合並為整體有序序列。歸並排序中第二步,對兩個有序數組排序法則非常簡單,同時對兩個數組的第一個位置比較大小,將小的放入一個空數組,然后被放入空數組的那個位置的指針往后移一個,然后繼續和另一個數組的上一個位置進行比較,以此類推。直到最后任何一個數組先出棧完,就將另外一個數組里的所有元素追加到新數組后面。
歸並排序和快速排序有那么點異曲同工之妙,快速排序:是先把數組粗略的排序成兩個子數組,然后遞歸再粗略分兩個子數組,直到子數組里面只有一個元素,那么就自然排好序了,可以總結為先排序再遞歸;歸並排序:先什么都不管,把數組分為兩個子數組,一直遞歸把數組划分為兩個子數組,直到數組里只有一個元素,這時候才開始排序,讓兩個數組間排好序,依次按照遞歸的返回來把兩個數組進行排好序,到最后就可以把整個數組排好序。
(2)實例
(3)python實現

def merge(a, b): c = [] h = j = 0 while j < len(a) and h < len(b): if a[j] < b[h]: c.append(a[j]) j += 1 else: c.append(b[h]) h += 1 if j == len(a): for i in b[h:]: c.append(i) else: for i in a[j:]: c.append(i) return c def MergeSort(list_): if len(list_) <= 1: return list_ middle = int(len(list_)/2) left = MergeSort(list_[:middle]) right = MergeSort(list_[middle:]) return merge(left, right)
五、基數排序
- 時間復雜度:給定n個d位數(即d個關鍵碼,關鍵碼的取值范圍為r),基數排序需要比較元素的每一位,則復雜度為O(d(n+r)),其中一輪循環分配時間復雜度為O(n),一輪循環收集時間復雜度為O(r),共需要d次循環來進行分配收集,即時間復雜度為O(d(n+r))
(1)基本思想:將所有待比較數值(正整數)統一為同樣的數位長度,數位較短的數前面補零。然后,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以后,數列就變成一個有序序列。
(2)實例
(3)python實現

def RadixSort(list_): i = 0 #初始為個位排序 n = 1 #最小的位數置為1(包含0) max_num = max(list_) #得到帶排序數組中最大數 while max_num > 10**n: #得到最大數是幾位數 n += 1 while i < n: bucket = {} #用字典構建桶 for x in range(10): bucket.setdefault(x, []) #將每個桶置空 for x in list_: #對每一位進行排序 radix =int((x / (10**i)) % 10) #得到每位的基數 bucket[radix].append(x) #將對應的數組元素加入到相應位基數的桶中 j = 0 for k in range(10): if len(bucket[k]) != 0: #若桶不為空 for y in bucket[k]: #將該桶中每個元素 list_[j] = y #放回到數組中 j += 1 i += 1 return list_