歸并排序的原理
采用分治思想,每次將數組從中間分成兩部分,分別對兩部分進行排序,直到不能繼續分解為止
merge_sort.png
def merge_sort(array):
"""
歸并排序:適合大規模數據
:param array:
"""
def _merge_sort(start, end):
if start >= end:
return
mid = (start+end) // 2
_merge_sort(start, mid)
_merge_sort(mid+1, end)
_merge(start, mid, end)
def _merge(start, mid, end):
i = start
j = mid + 1
temp = []
while i <= mid and j <= end:
if array[i] < array[j]:
temp.append(array[i])
i += 1
else:
temp.append(array[j])
j += 1
remaining_start = i
remaining_end = mid
if j <= end:
remaining_start = j
remaining_end = end
while remaining_start <= remaining_end:
temp.append(array[remaining_start])
remaining_start += 1
for idx in range(end-start+1):
array[start+idx] = temp[idx]
count = len(array)
_merge_sort(0, count-1)
歸并排序的性能分析
- 是否是穩定的排序算法,要看merge函數,如果從start->mid和mid->end兩個區間的存在值相同的元素,那么在合并的時候可以將start->mid先放入數組,這樣就保證了值相同的元素前后順序不變,所以是一個穩定的排序算法
- 關于時間復雜度的分析,這里涉及到了遞歸調用,
如果我們定義求解問題a的事件是T(a),求解問題b、c的時間分別是T(b)和T(c),那么可以得到這樣的遞推公式:
T(a) = T(b) + T(c) + K
K是將兩個子問題合并所消耗的時間。
假設對n個元素進行歸并排序需要的時間是T(n),那分解成兩個子數組排序的的時間就是T(n/2)。merge合并兩個子數組的時間復雜度是O(n)。
那么歸并排序的時間復雜度的計算公式就是:
T(1) = C; n=1
T(n) = 2*T(n/2) + n; n>1
進一步分解得到:
T(n) = 2*T(n/2) + n
= 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*m
= 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
= 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
...
= 2^k * T(n/2^k) + k*n
當T(n/2^k) = T(1)時,也就是n/2^k=1,得到k=log2(n),將k帶入上面的公式,得到T(n) = Cn + nlog2(n)。
所以 T(n) = O(nlogn)
- 空間復雜度。每次合并兩個子數組的時候,都會創建一個臨時數組,合并結束即銷毀。所以臨時空間最大不會超過n個數據的大小,為O(n)。
快速排序的原理
和歸并排序相似,都是用到了分治思想,但是實現卻是完全不同。
快速排序是要在start和end之間找一個任意的元素pivot(分區點),遍歷數組,將小于pivot的元素放在左邊,大于pivot的元素放在右邊,結束后,整個數組就被分為了三個部分,小于pivot的start->pivot_idx-1,pivot,大于pivot的pivot_idx+1->end。
quick_sort.png
def quick_sort(array):
"""
快速排序
:param array:
"""
def _quick_sort(start, end):
if start >= end:
return
pivot_idx = _partition(start, end)
_quick_sort(start, pivot_idx-1)
_quick_sort(pivot_idx+1, end)
def _partition(start, end):
pivot = array[end]
i = start
for j in range(start, end):
if array[j] < pivot:
array[i], array[j] = array[j], array[i]
i += 1
array[i], array[end] = array[end], array[i]
return i
count = len(array)
if count <= 1:
return
_quick_sort(0, count-1)
歸并排序的分治思想是先分,后治,先拆分到最后,在合并子數組。而快速排序更像是先治后分,先將比pivot小和大的元素分別排到左右兩邊,在拆分子數組繼續找pivot繼續排,直到最后不能排為止。
merge_quick_sort.png
快速排序的性能分析
- 快速排序是一種原地(交換部分沒有用到額外的空間)、不穩定的排序算法
- 如果每次分區操作,都能把數組分成大小相等的兩個小區間,那快排的時間復雜度遞推公式跟歸并是相同的,所以快排的時間復雜度也是O(nlogn)。如果排序的數組已經是有序,那么每次分區得到的兩個區間都是不均等的,需要進行大約n次分區操作,每次分區平均掃描大于n/2的元素,這種情況下,時間復雜度就退化成了O(n^2)。
總結
快排和歸并都是通過分治思想、遞歸調用方式來實現,但是歸并排序雖然是一種在任何情況下時間復雜度都比較穩定的排序算法,但是也存在致命缺點,即不是原地排序算法,空間復雜度比較高(O(n)),因此也沒有快排應用的廣泛。
快排的時間復雜度最快情況是O(n2),但是平均時間復雜度是O(nlogn),而且退化到O(n2)的概率非常小,也可以合理地選擇pivot來避免這種情況。