常見排序方法的時間與空間複雜度
平均情況時間複雜度
最壞和最好情況是極端情況,發生的概率並不大。爲了更有效的表示平均情況下的時間複雜度,引入另一個概念:平均情況時間複雜度,全稱叫加權平均時間複雜度或者期望時間複雜度。(引入各自情況發生的概率再具體分析)
多數情況下,我們不需要區分最好、最壞、平均情況時間複雜度。只有同一塊代碼在不同情況下時間複雜度有量級差距,我們纔會區分3種情況,爲的是更有效的描述代碼的時間複雜度。
均攤時間複雜度
應用場景:均攤時間複雜度和攤還分析應用場景較爲特殊,對一個數據進行連續操作,大部分情況下時間複雜度都很低,只有個別情況下時間複雜度較高。而這組操作其存在前後連貫的時序關係。
計算:我們將這一組操作放在一起分析,將高複雜度均攤到其餘低複雜度上,所以一般均攤時間複雜度就等於最好情況時間複雜度。
舉例 : 有一個長度爲n的數組,如果數組沒滿,就往裏插入一個數,如果數組滿了,就遍歷求和.那麼絕大多數情況下都是O(1),只有最後一次是O(n),均攤以後就是O(1)。
// array 表示一個長度爲 n 的數組
// 代碼中的 array.length 就等於 n
int[] array = new int[n];
int count = 0;
void insert(int val) {
if (count == array.length) {
int sum = 0;
for (int i = 0; i < array.length; ++i) {
sum = sum + array[i];
}
array[0] = sum;
count = 1;
}
array[count] = val;
++count;
}
一、插入排序
通過構建有序序列,對於未排序的序列,在已排序的序列中從後向前掃描,找到相應位置插入。
1)從第一個元素開始,該元素可以被認爲已經被排序。
2)取出下一個元素,在已經排序的元素序列中從後向前掃描。
3)如果該元素(已排序)大於新元素,將該元素移到下一位置。
4)重複步驟3),直到找到已排序的元素小於或者等於新元素的位置。
5)將新元素插入到該位置後。
6)重複步驟2)~5)。
def InsertSort(myList):
#獲取列表長度
length = len(myList)
for i in range(1,length):
#設置當前值前一個元素的標識
j = i - 1
temp = myList[i]
#繼續往前尋找,如果有比臨時變量大的數字,則後移一位,直到找到比臨時變量小的元素或者達到列表第一個元素
while j>=0 and myList[j] > temp:
myList[j+1] = myList[j]
j = j-1
#將臨時變量賦值給合適位置
myList[j+1] = temp
myList = [49,38,65,97,76,13,27,49]
InsertSort(myList)
print(myList)
二、冒泡排序
1)比較相鄰的元素,如果第一個比第二個大,就交換他們兩個。
2)對每一對相鄰元素做同樣的工作,從開始第一對到結尾最後一對。這步做完後,最後的元素會是最大的元素。
3)持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
def bubble_sort(lists):
# 冒泡排序
count = len(lists)
for i in range(0, count - 1):
for j in range(0, count - 1 - i):
if lists[j] > lists[j + 1]:#if語句的判斷是包含在時間複雜度的計算中。
a = lists[j]
lists[j] = lists[j+1]
lists[j+1] = a
return lists
myList = [49,38,65,97,76,13,27,49]
bubble_sort(myList)
print(myList)
優化算法(在最優算法下冒泡排序的最優時間複雜度爲O(n))
def bubble_sort(items):
for i in range(len(items) - 1):
flag = False
for j in range(len(items) - 1 - i):
if items[j] > items[j + 1]:
items[j], items[j + 1] = items[j + 1], items[j]
flag = True
if not flag:
break
return items
三、選擇排序
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
import sys
def select_sort(lists):
for i in range(len(lists)):
min_idx = i
for j in range(i+1, len(lists)):
if lists[min_idx] > lists[j]: #if語句的判斷是包含在時間複雜度的計算中。
min_idx = j
lists[i], lists[min_idx] = lists[min_idx], lists[i]
return lists
四、希爾排序
利用插入排序的簡單,克服插入排序每次只能交換相鄰兩個元素的缺點
排序思想:
1) 定義增量序列DM > DM-1 > … > D1 = 1
2)對每個DK進行“ DK ”間隔排序(k = M , M-1 , … 1)
原始希爾排序:
注:“DK”間隔有序的數列,在執行“DK-1”間隔排序後,仍然保持“DK”間隔有序的。增量元素不互質,小增量可能在後面的排序過程中不起作用。
Hibbard 增量序列 – DK = 2k - 1(保證了增量元素不互質) , 最壞情況下的時間複雜度爲O(N3/2).
def shell_sort(alist):
"""希爾排序"""
n = len(alist)
gap = n // 2
while gap >= 1:
for j in range(gap, n):
i = j
while (i - gap) >= 0:
if alist[i] < alist[i - gap]:
alist[i], alist[i - gap] = alist[i - gap], alist[i]
i -= gap
else:
break
gap //= 2
if __name__ == '__main__':
alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print("原列表爲:%s" % alist)
shell_sort(alist)
print("新列表爲:%s" % alist)
五、堆排序
堆是具有以下性質的完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆。
堆排序基本思路:
1)將無序序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆;
2)將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
3)重新調整結構,使剩餘元素滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。
圖解見鏈接:https://www.cnblogs.com/chengxiao/p/6129630.html
注:
a. 完全二叉樹:對於深度爲K的,有n個結點的二叉樹,當且僅當其每一個結點都與深度爲K的滿二叉樹中編號從1至n的結點一一對應時稱之爲完全二叉樹。
b.堆的定義:
1)任意節點小於(或大於)它的所有後裔,最小元(或最大元)在堆的根上(堆序性)。
2)堆總是一棵完全樹。即除了最底層,其他層的節點都被元素填滿,且最底層儘可能地從左到右填入。
c.時間複雜度的推算:主要在初始化堆過程和每次選取最大數後重新建堆的過程。利用等比數列求和公式計算初始化建堆過程時間爲O(n)。更改堆元素後重建堆時間爲O(nlogn),所以總的時間複雜度爲O(nlogn)。 因爲堆排序是就地排序,空間複雜度爲常數:O(1)。
#_*_coding:utf-8_*_
import time,random
def sift_down(arr, node, end):
root = node
#print(root,2*root+1,end)
while True:
# 從root開始對最大堆調整
child = 2 * root +1 #left child
if child > end:
#print('break',)
break
#print("v:",root,arr[root],child,arr[child])
#print(arr)
# 找出兩個child中較大的一個
if child + 1 <= end and arr[child] < arr[child + 1]: #如果左邊小於右邊
child += 1 #設置右邊爲大
if arr[root] < arr[child]:
# 最大堆小於較大的child, 交換順序
tmp = arr[root]
arr[root] = arr[child]
arr[child]= tmp
# 正在調整的節點設置爲root
#print("less1:", arr[root],arr[child],root,child)
root = child #
#[3, 4, 7, 8, 9, 11, 13, 15, 16, 21, 22, 29]
#print("less2:", arr[root],arr[child],root,child)
else:
# 無需調整的時候, 退出
break
#print(arr)
print('-------------')
def heap_sort(arr):
# 從最後一個有子節點的孩子開始調整最大堆
first = len(arr) // 2 -1
#生成最大堆
for i in range(first, -1, -1):
sift_down(arr, i, len(arr) - 1)
#[29, 22, 16, 9, 15, 21, 3, 13, 8, 7, 4, 11]
print('--------end---',arr)
# 將最大的放到堆的最後一個, 堆-1, 繼續調整排序
for end in range(len(arr) -1, 0, -1):
arr[0], arr[end] = arr[end], arr[0]
sift_down(arr, 0, end - 1)
#print(arr)
def main():
# [7, 95, 73, 65, 60, 77, 28, 62, 43]
# [3, 1, 4, 9, 6, 7, 5, 8, 2, 10]
#l = [3, 1, 4, 9, 6, 7, 5, 8, 2, 10]
#l = [16,9,21,13,4,11,3,22,8,7,15,27,0]
array = [16,9,21,13,4,11,3,22,8,7,15,29]
#array = []
#for i in range(2,5000):
# #print(i)
# array.append(random.randrange(1,i))
print(array)
start_t = time.time()
heap_sort(array)
end_t = time.time()
print("cost:",end_t -start_t)
print(array)
#print(l)
#heap_sort(l)
#print(l)
if __name__ == "__main__":
main()
六、快速排序
-
快速排序採用了一種分治的策略,通常稱其爲分治法。分治法的基本思想是:將原問題分解爲若干個規模更小但結構與原問題相似的子問題。遞歸地解這些子問題,然後將這些子問題的解組合爲原問題的解。
-
利用分治法可將快速排序分爲三步:
1)在數據集之中,選擇一個元素作爲”基準”(pivot)。
2)所有小於”基準”的元素,都移到”基準”的左邊;所有大於”基準”的元素,都移到”基準”的右邊。這個操作稱爲分區 (partition) 操作,分區操作結束後,基準元素所處的位置就是最終排序後它的位置。
3)對”基準”左邊和右邊的兩個子集,不斷重複第一步和第二步,直到所有子集只剩下一個元素爲止。 -
遞歸算法的時間複雜度求法:代入法(代入法首先要對這個問題的時間複雜度做出預測,然後將預測帶入原來的遞歸方程,如果沒有出現矛盾,則是可能的解,最後用數學歸納法證明),迭代法, 差分方程法。
-
遞歸算法的時間複雜度:
a.在最優情況下,Partition每次都劃分得很均勻,如果排序n個關鍵字,快速排序算法的時間複雜度爲O(nlogn)。
-
藉助遞歸樹求解遞歸算法的時間複雜度(歸併排序,快速排序, 斐波那契數列, 全排列):https://www.jianshu.com/p/6fa5a8ddd65f
def quick_sort_standord(array,low,high):
''' realize from book "data struct" of author 嚴蔚敏
'''
if low < high:
key_index = partion(array,low,high)
quick_sort_standord(array,low,key_index)
quick_sort_standord(array,key_index+1,high)
def partion(array,low,high):
key = array[low]#選擇一個元素作爲”基準”
while low < high:
while low < high and array[high] >= key:
high -= 1
if low < high:
array[low] = array[high]
while low < high and array[low] < key:
low += 1
if low < high:
array[high] = array[low]
array[low] = key
return low
if __name__ == '__main__':
array2 = [9,3,2,1,4,6,7,0,5]
print(array2)
quick_sort_standord(array2,0,len(array2)-1)
print(array2)
七、歸併排序
a. 採用分治法
分割:遞歸地把當前序列平均分割成兩半。
集成:在保持元素順序的同時將上一步得到的子序列集成到一起(歸併)。
b. 歸併操作:遞歸法(Top-down)
1)申請空間s,該空間用來存放合併後的序列。
2)比較兩個排序序列起始位置指向的數,將較小的元素刪除(pop)並添加到s中(append)。
3)重複步驟2直到其中一序列爲空。
4)將另一序列剩下的所有元素直接複製到合併序列尾。
c.利用遞歸樹方便求得時間複雜度爲O(nlogn)
https://zh.wikipedia.org/wiki/歸併排序#Python
# Recursively implementation of Merge Sort
#歸併
def merge(left, right):
result = []
while left and right:
if left[0] <= right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
if left:
result += left
if right:
result += right
return result
#劃分
def merge_sort(L):
if len(L) <= 1:
# When D&C to 1 element, just return it
return L
mid = len(L) // 2
left = L[:mid]
right = L[mid:]
left = merge_sort(left)
right = merge_sort(right)
# conquer sub-problem recursively
return merge(left, right)
# return the answer of sub-problem
if __name__ == "__main__":
test = [1, 4, 2, 3.6, -1, 0, 25, -34, 8, 9, 1, 0]
print("original:", test)
print("Sorted:", merge_sort(test))
八、桶排序
桶排序(Bucket sort)或所謂的箱排序,是一個排序算法,工作的原理是將數組分到有限數量的桶裏。每個桶再個別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序),最後依次把各個桶中的記錄列出來即得到有序序列。
def bucket_sort(array):
if not array:
return False
max_len = max(array)+1
book = [0 for x in range(0,max_len)]
for i in array:
book[i] += 1
return [i for i in range(0,max_len) for j in range(0,book[i])]
def main():
array = [5,4,4,74,90,2]
array = bucket_sort(array)
print(array)
if __name__ == '__main__':
main()
缺點:無法排負數,無法排小數,book所佔的空間由輸入數組的最大值確定
針對存在負數的情況
def bucket_sort(array):
if not array:
return False
offset = min(array)
max_len = max(array) - offset + 1
book = [0 for x in range(0,max_len)]
for i in array:
book[i - offset] += 1
return [i + offset for i in range(0,max_len) for j in range(0,book[i])]
def main():
array = [5,4,4,-2,-9,74,90,2]
array = bucket_sort(array)
print(array)
if __name__ == '__main__':
main()
針對存在小數與負數的情況
# 可對小數排序
def bucket_sort(array):
if not array:
return False
# 保留兩位小數
accuracy = 100.
offset = int(min(array) * accuracy)
max_len = int(max(array) * accuracy - offset + 1)
book = [0 for x in range(0,max_len)]
for i in array:
book[int(i * accuracy - offset)] += 1
return [(i + offset) / accuracy for i in range(0,max_len) for j in range(0,book[i])]
def main():
array = [5,4,4,-2,-9,74,90,2]
array = bucket_sort(array)
print(array)
if __name__ == '__main__':
main()
sort(),sorted()
底層實現就是歸併排序,速度比我們自己寫的歸併排序要快很多(10~20倍),所以說我們一般排序都儘量使用sorted和sort。最壞與平均情況下的時間複雜度爲O(nlogn)。
注:
- 快排對越混亂的數據,排序效果越好,對一個基本有序的序列排序卻更復雜(它要交換很多次才能排好)。因爲這樣會導致每次軸劃分出的兩個子序列,一個趨近於1的數量級,一個趨近於n數量級,那麼遞歸快排就近似總是對n做排序,時間複雜度O(n²),而且非常不符合快排的思想。比較好的情況是每次遞歸大致平分成兩個n/2數量級的子序列,時間複雜度O(nlogn)。
- 對基本有序的序列比較適合適用冒泡排序。
- 若n較小(如n≤50),可採用直接插入或直接選擇排序。當記錄規模較小時,直接插入排序較好;否則因爲直接選擇移動的記錄數少於直接插入,應選直接選擇排序爲宜。
- 若文件初始狀態基本有序(指正序),則應選用直接插人、冒泡或隨機的快速排序爲宜。
- 若n較大,則應採用時間複雜度爲O(nlgn)的排序方法:快速排序、堆排序或歸併排序。
- 穩定性判斷:假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱爲不穩定的。
- 有大量重複元素時使用三分快排:
提出的算法是: 對於每次切分:從數組的左邊到右邊遍歷一次,維護三個指針lt,gt,i,其中lt指針使得元素(arr[0]-arr[lt-1])的值均小於切分元素;gt指針使得元素(arr[gt+1]-arr[N-1])的值均大於切分元素;i指針使得元素(arr[lt]-arr[i-1])的值均等於切分元素,(arr[i]-arr[gt])的元素還沒被掃描,切分算法執行到i>gt爲止。
每次切分之後,位於gt指針和lt指針之間的元素的位置都已經被排定,不需要再去移動了。之後將(lo,lt-1),(gt+1,hi)分別作爲處理左子數組和右子數組的遞歸函數的參數傳入,遞歸結束,整個算法也就結束。
- 排序算法動態演示:https://www.cnblogs.com/onepixel/p/7674659.html
- TOP k的解法:
a) 用堆排來解決Top K :先建立一個包含K個元素的大頂堆,然後遍歷集合,如果集合的元素比堆頂元素小(說明它目前應該在K個最小之列),那就用該元素來替換堆頂元素,同時維護該堆的性質,那在遍歷結束的時候,堆中包含的K個元素是不是就是我們要找的最小的K個元素。
速記: 最小的K個用最大堆,最大的K個用最小堆。
堆排時間複雜度爲n*logK。
速記: 堆排的時間複雜度是nlogn,這裏相當於只對前Top K個元素建堆排序,想法不一定對,但一定有助於記憶。
不會佔用太多的內存空間(事實上,一次只讀入一個數,內存只要求能容納前K個數即可)。這也決定了它特別適合處理海量數據。
b) 用快速排序來解決Top K:我們知道,分治函數會返回一個position,在position左邊的數都比第position個數小,在position右邊的數都比第position大。我們不妨不斷調用分治函數,直到它輸出的position = K-1,此時position前面的K個數(0到K-1)就是要找的前K個數。
快排時間複雜度爲n。
既然要交換元素的位置,那麼所有元素必須要讀到內存空間中,所以它會佔用比較大的空間,至少能容納整個數組;數據越多,佔用的空間必然越大,海量數據處理起來相對吃力。