目錄
一、冒泡排序、選擇排序和插入排序:O(n*n)
1、冒泡排序
1、冒泡排序原理
冒泡排序對相鄰的兩個元素進行比較,看是否滿足大小關係要求,如果不滿足就讓他倆交換,如下圖所示。一次冒泡會讓至少一個元素移動到它應該在的位置,重複n次,就完成n個數據的排序。
2、Python實現冒泡排序
from typing import List
def bubble_sort(a: List[int]):
length = len(a)
if length <= 1:
return
for i in range(length):
made_swap = False
for j in range(length - i - 1):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
made_swap = True
if not made_swap:
break
3、冒泡排序性能分析
冒泡排序是穩定的排序算法。當相鄰的兩個元素大小相等的時候,我們不做交換,相同大小的數據在排序前後不會改變順序。
時間複雜度。最好的情況,數據有序,時間複雜度爲O(n)。最壞的情況,時間複雜度爲O(n*n)。平均情況,時間複雜度爲O(n*n)。
空間複雜度。冒泡排序只涉及相鄰數據的交換操作,時間複雜度爲O(1)。
2、選擇排序
1、選擇排序原理
選擇排序分爲已排序區間和未排序區間,每次從未排序區間中找到最小的元素,將其放到已排序區間的末尾。如下圖所示:
2、Python實現選擇排序
from typing import List
def selection_sort(a: List[int]):
length = len(a)
if length <= 1:
return
for i in range(length):
min_index = i
min_val = a[i]
for j in range(i, length):
if a[j] < min_val:
min_val = a[j]
min_index = j
a[i], a[min_index] = a[min_index], a[i]
3、選擇排序性能分析
選擇排序不是穩定的排序算法。選擇排序每次都要找未排序空間中最小值,並和前面的元素交換位置,破壞了穩定性。例如3,4,3,1
時間複雜度。最好情況、最壞情況和平均情況的時間複雜度都爲O(n*n)。
空間複雜度。選擇排序屬於原地排序,空間複雜度爲O(1)。
3、插入排序
1、插入排序原理
插入排序,將數組中的數據分爲兩個區間,已排序區間和未排序區間,初始已排序區間只有一個元素,就是數組的第一個元素。插入排序取未排序區間中的元素,在已排序區間中找到合適的位置將其插入,保證已排序區間數據一直有序。重複這個過程,直到未排序區間中元素爲空,算法結束。如下圖所示:
2、Python實現插入排序
from typing import List
def insertion_sort(a: List[int]):
length = len(a)
if length <= 1:
return
for i in range(1, length):
value = a[i]
j = i - 1
while j >= 0 and a[j] > value:
a[j + 1] = a[j]
j -= 1
a[j + 1] = value
3、插入排序性能分析
插入排序是穩定的排序算法。對於值相同的元素,我們可以選擇將後面出現的元素,插入到前面出現的元素後面,保持原有的前後順序不變。
空間複雜度。插入排序是原地排序算法,時間複雜度爲O(1)。
時間複雜度。最好情況,數組有序,不需要移動元素,時間複雜度爲O(n)。最壞情況爲O(n*n)。平均情況爲O(n*n)。與冒泡排序相比,插入排序運行時間更少,因爲每次移動元素,插入排序只需要1次賦值,而冒泡排序需要3次賦值才能交換元素。
二、歸併排序和快速排序:O(nlogn)
1、歸併排序
1、歸併排序原理
歸併排序的核心思想:如果排序一個數組,先把數組從中間分成前後兩部分,然後對前後兩部分分別排序,再將排序好的兩部分合併在一起,這樣整個數組就都有序了。如下圖所示:
歸併排序使用的分治思想,就是分而治之,將一個大問題分解成小的子問題來解決,一般用遞歸實現:
遞推公式:
merge_sort(p..r) = merge(merge_sort(p..q), merge_sort(q+1..r))
終止條件:
p >= r 不用再繼續分解
2、python實現歸併排序
from typing import List
def merge_sort(a: List[int]):
_merge_sort_between(a, 0, len(a)-1)
def _merge_sort_between(a: List[int], low: int, high: int):
if low < high:
mid = low + (high - low) // 2
_merge_sort_between(a, low, mid)
_merge_sort_between(a, mid+1, high)
_merge(a, low, mid, high)
def _merge(a: List[int], low: int, mid: int, high: int):
#a[low:mid],a[mid+1, high] are sorted
i, j = low, mid+1
tmp = []
while i <= mid and j <= high:
if a[i] <= a[j]:
tmp.append(a[i])
i += 1
else:
tmp.append(a[j])
j += 1
start = i if i <= mid else j
end = mid if i <= mid else high
tmp.extend(a[start: end + 1])
a[low:high+1] = tmp
if __name__ == "__main__":
a1 = [3, 6, 5, 8, 7]
merge_sort(a1)
print(a1)
3、歸併排序性能分析
歸併排序是穩定的排序算法。歸併排序穩定性關鍵要看merge函數,也就是兩個有序子數組合併成一個有序數組,值相同的元素在合併時保持順序不變。
時間複雜度。對n個元素進行歸併排序需要的時間是T(n),那分解成兩個子數組排序的時間是T(n/2),merge函數合併兩個有序子數組的時間是n,T(n)的計算過程如下:
T(n) = 2*T(n/2) + n
= 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2n
= 4*(2*T(n/8) + n/4) + 2n = 8*T(n/8) + 3n
= .....
= 2^k*T(n/2^k) + k*n
當T(n/2^k)=T(1)時,k=logn, T(n)=n + n*logn。所以歸併排序的時間複雜度爲O(nlogn).
空間複雜度。歸併排序在合併兩個有序數組爲一個有序數組時,需要藉助額外的存儲空間,空間複雜度爲O(n).(在任意時刻,cpu只會有一個函數在執行,也就只會有一個臨時的內存空間在使用)
2、快速排序
1、快排原理
快排核心思想:如果要排數組中下標從p到r之間的一組數據,我們選擇p到r之間的任意一個數據爲pivot(分區點)。遍歷p到r之間的數據,將小於pivot的放到左邊,大於pivot的放到右邊,pivot放在中間。數組p到r之間的數據分成三個部分,前面p到q-1之間是小於pivot的,中間是pivot, 後面的q+1到r之間是大於pivot的。如下圖所示:
快排利用的也是分治思想,可以用遞歸實現:
遞推公式:
quick_sort(p..r) = quick_sort(p..q-1) + quick_sort(q+1..r)
終止條件:
p >= r
2、python實現快速排序
from typing import List
import random
def quick_sort(a: List[int]):
_quick_sort_between(a, 0, len(a)-1)
def _quick_sort_between(a: List[int], low: int, high: int):
if low < high:
#get a random position as the pivot
k = random.randint(low, high) #防止數組有序,最壞情況發生
a[low], a[k] = a[k], a[low]
m = _partition(a, low, high)
_quick_sort_between(a, low, m-1)
_quick_sort_between(a, m+1, high)
def _partition(a: List[int], low: int, high: int):
pivot, j = a[low], low
for i in range(low+1, high+1):
if a[i] < pivot:
j += 1
a[j], a[i] = a[i], a[j]
a[low], a[j] = a[j], a[low]
return j
if __name__ == "__main__":
a1 = [4, 3, 2, 1]
quick_sort(a1)
print(a1)
a2 = [3, 4, 5, 6, 7]
quick_sort(a2)
print(a2)
a4 = [5, -1, 9, 3, 7, 8, 3, -2, 9]
quick_sort(a4)
print(a4)
3、快速排序性能分析
快排不是穩定的排序算法。快排中遍歷數組元素,將小於分區點的數放在左邊,大於分區點的數放在右邊。值相同元素的順序會發生改變。
時間複雜度:最好的情況如同歸併排序,時間複雜度爲O(nlogn).最壞的情況,數組是有序的,時間複雜度爲O(n^2).
空間複雜度:快排是原地排序算法,空間複雜度爲O(1)。因而對大規模數據排序時,快排比歸併排序更常用。
三、桶排序和基數排序:O(n)
1、桶排序
1、桶排序原理
桶排序核心思想,將要排序的數據分到幾個有序的桶裏,每個桶裏的數據再單獨進行快速排序,桶內排完序後,再把每個桶裏的數據按照順序依次取出,組成的序列就是有序的了。
2、桶排序性能分析
時間複雜度分析。如果要排序的數據有n個,我們把他們均勻地分到m個桶內,每個桶內有k=n/m個元素。每個桶內部使用快速排序,時間複雜度爲O(k*logk),m個桶排序的時間複雜度爲O(m*k*logk),因爲k=n/m,所以整個桶的時間複雜度爲O(n*log(n/m)).當桶的個數m接近數據個數n時,log(n/m)就是一個非常小的常量,桶排序的時間複雜度接近O(n).
3、適用場景
桶排序對要排序的數據非常苛刻。首先,要排序的數據需要容易地劃分成m個桶,並且桶與桶之間有天然的大小順序;其次,數據在各個桶之間的分佈式比較均勻的。
桶排序比較適合用在外部排序中,數據存儲在外部磁盤中,數據量比較大,內存有限,無法將數據全部加載在內存中。例如,我們有10GB的訂單數據,需要按照訂單金額進行排序,但內存只有4G,無法一次性將10G數據都加載到內存中。可以藉助桶排序解決這個問題:先掃描一遍文件,看訂單金額的數據範圍,假設最小是1元,最大是10萬元;將所有訂單根據金額劃分到100個桶裏,第一個桶我們存儲金額在1元到1000元之內的訂單,第二個桶存儲金額在1001到2000元之內的訂單,以此類推,每一個桶對應一個文件;理想情況,每個小文件大約100M的訂單數據,我們可以將這100個小文件依次放到內存中,用快排來排序;所有小文件都排好序,我們只需要按照文件編號,從小到大依次讀取每個小文件中的訂單數據,並將其寫入一個文件中,這個文件中存儲的就是按照金額從小到大排序的訂單數據。
2、基數排序
1、基數排序原理
基數排序按照每位來排序,排序算法需要是穩定的。如下圖所示:
2、基數排序性能分析
時間複雜度。根據每一位來排序,可以用桶排序,時間複雜度爲O(n),如果要排序的數據有k位,時間複雜度爲O(k*n),k爲常數時,基數排序的時間複雜度近似於O(n).
3、適用場景
要排序的數據可以分割出獨立的位來比較,而且位之間有遞進的關係,如果a數據的高位比b數據大,那剩下的地位就不用比較了。
總結:
|
穩定性 |
空間複雜度 |
最好 |
最壞 |
平均 |
冒泡排序 |
穩定 |
O(1) |
O(n) |
O(n*n) |
O(n*n) |
插入排序 |
穩定 |
O(1) |
O(n) |
O(n*n) |
O(n*n) |
選擇排序 |
不穩定 |
O(1) |
O(n*n) |
O(n*n) |
O(n*n) |
歸併排序 |
穩定 |
O(n) |
O(nlogn) |
O(nlogn) |
O(nlogn) |
快速排序 |
不穩定 |
O(1) |
O(nlogn) |
O(n*n) |
O(nlogn) |
桶排序 |
穩定 |
O(n) |
O(n) |
||
基數排序 |
穩定 |
O(n) |
O(n) |
參考資料:
極客時間:《數據結構與算法之美》