1. 排序算法分類
排序算法可以分爲 外部排序 和 內部排序:
(1)外部排序 (External sorting)是指能夠處理極大量數據的排序算法。
通常來說,外排序處理的數據不能一次裝入內存,只能放在讀寫較慢的外存儲器(通常是硬盤)上。外排序通常採用的是一種“排序-歸併”的策略。
在排序階段,先讀入能放在內存中的數據量,將其排序輸出到一個臨時文件,依此進行,將待排序數據組織爲多個有序的臨時文件。而後在歸併階段將這些臨時文件組合爲一個大的有序文件,也即排序結果。
(2)內部排序還可以細分:
需要額外內存空間(out-place,即空間複雜度不是)的算法有:桶排序,基數排序,歸併排序; 其他算法一般只需常量的內存空間(in-place,原地排序)。
以下介紹9種內部排序:
2. 冒泡法
冒泡法的步驟:從左到右遍歷,遍歷的元素和後一個比較,如果前一個比後一個大,則交換;第一次遍歷後,最大的元素在最右的位置。以相同的方式遍歷,次大的元素放置在倒數第2的位置;直到需要比較的元素只有1個爲止。
import numpy as np
import time
def bubbleSort(arr):
# 冒泡法
for i in range(len(arr)-1, 0, -1):
for j in range(i):
if arr[j] > arr[j+1]:
arr[j],arr[j+1] = arr[j+1],arr[j]
return arr
def get_arr(num=10):
# 生成隨機的大小爲num的數組
np.random.seed(1)
arr = np.random.randint(0,num,(num,))
return arr
if __name__ == "__main__":
# 冒泡法
arr = get_arr()
print('arr',arr)
arr = bubbleSort(arr)
print(arr)
運行結果:
arr [5 8 9 5 0 0 1 7 6 9]
#排序後
arr [0 0 1 5 5 6 7 8 9 9]
3. 插入排序
插入排序,是把當前的數插入到前面排序好的序列相應位置,使得序列保持單調性。
def swap(arr, i, j):
arr[i], arr[j] = arr[j], arr[i]
def insertionSort(arr):
for i in range(len(arr)):
for j in range(i, 0, -1):
if arr[j] < arr[j-1]:
swap(arr, j, j-1)
else:
break
return arr
arr = insertionSort(arr)
4. 希爾排序
希爾排序對插入排序改進,定義一個步長序列,不再只是相鄰的兩個元素進行比較。
步長序列 | 最壞情況下複雜度 |
---|---|
def shellSort(arr):
# 希爾排序
gap = len(arr) // 2
while gap > 0:
for i in range(gap, len(arr)):
j = i
while (j >= gap) and (arr[j] < arr[j-gap]):
swap(arr, j, j-gap)
j = j-gap
gap = gap//2
return arr
5. 選擇排序
選擇排序是,遍歷一遍,找到最小的元素,然後交換到最左邊,重複這個過程。和冒泡法相比,它遍歷一遍,只交換一次。
def selectSort(arr):
for i in range(len(arr)):
min_idx = i
for j in range(i+1, len(arr)):
if arr[j] < arr[min_idx]:
min_idx = j
swap(arr, min_idx, i)
return arr
6. 堆排序
堆排序使用二叉樹的數據結構,需要滿足,父節點值不小於兒子節點。
堆排序,通過建立一個最大堆,即根節點的值最大,然後把根節點和最後一個葉節點交換;剩下的數重新建立一個最大堆,獲得次大值,繼續和葉節點交換;重複這個過程,直到堆的大小爲1。
def heapSort(arr):
def siftDown(arr, index, length=None):
'''sift_down'''
if length is None:
length = len(arr)
# 最大堆;子節點總小於父節點, O(n)
while True:
left = 2*index + 1
right = 2*index + 2
max_idx = left
if left >= length:
break
if right<length and arr[right] > arr[left]:
max_idx = right
if arr[index] < arr[max_idx]:
arr[index], arr[max_idx] = arr[max_idx],arr[index]
index = max_idx
else:
break
# 初始化最大堆 O(n)
for idx in range(len(arr)//2-1, -1, -1):
siftDown(arr, idx, len(arr))
# 堆排序,交換最大值和最後一個葉節點, O(nlogn)
for length in range(len(arr)-1, -1, -1):
arr[length], arr[0] = arr[0], arr[length]
siftDown(arr, 0, length)
return arr
7. 快速排序
快速排序是常用的排序算法。思想是,選擇一個基準,遍歷一遍,把數分成兩堆,比基準小的放在數組左邊,比基準大的放在數組右邊,最後,交換基準到兩者的分界處;對基準左邊和右邊的子數組重複進行快速排序,直到子數組只有1個元素。
def swap(arr, i, j):
arr[i], arr[j] = arr[j], arr[i]
def select_base(arr,low,high):
# 取三個數的中位數作爲基準
mid = (low + high) // 2
if (arr[low] - arr[mid])*(arr[low] - arr[high]) < 0:
return low
if (arr[mid] - arr[low])*(arr[mid] - arr[high]) < 0:
return mid
return high
def qsort(arr, low, high):
left, right = low, high
if left < right:
while left < right:
pivot = select_base(arr, low, high)
# 基準放在最左邊low
swap(arr, low, pivot)
# 從右到左遍歷,選擇比基準小的元素
while left < right and arr[right] >= arr[low]:
right -= 1
# 從左到右遍歷,選擇比基準大的元素
while left < right and arr[left] <= arr[low]:
left += 1
swap(arr, left, right)
# 最終 left = right,該位置元素不大於基準
swap(arr, low, right)
qsort(arr, low, right-1)
qsort(arr,right+1, high)
def quickSort(arr):
qsort(arr,0,len(arr)-1)
quickSort(arr)
直到一提是,常見選擇基準可以取最左邊的元素,但是可能最左邊的元素是數組的最小值或者最大值,導致左右子數組的數量相差懸殊,排序效率低的情形。因此,取三個數的平均值作爲基準。
此外,在遍歷數組時,這裏使用了雙指針兩個方向進行遍歷,但如單鏈表只有一個方向進行遍歷,實現的話定義兩個指針,一個指針記錄兩堆的分界,一個指針向前遍歷直到找到比基準小的值,然後和分界後一個元素交換,更新兩堆的分界。
8. 桶排序和計數排序
桶排序是,把值映射函數後,得到索引,根據索引放入相應的有大小排序的桶,然後桶內的元素進行排序(如插入排序),然後合併所有的桶,獲得排序後的序列。
如果桶的數量足夠多,和序列的範圍一樣,那麼,每個桶放入和桶本身值一樣的數值,記錄數量。最後遍歷所有的桶,按照數量來打印桶的值,得到排序後的序列,這也叫計數排序。
def bucketSort(arr, radix=10):
'''radix,基數代表一個桶最大容納多少個不同的數;
當radix=1, 桶排序相當於計數排序.'''
def get_index(val, min_a):
return (val - min_a) // radix
max_a = max(arr)
min_a = min(arr)
bucket_num = (max_a - min_a)//radix + 1
bucket = [[] for _ in range(bucket_num)]
# 放入桶中
for val in arr:
index = get_index(val, min_a)
bucket[index].append(val)
index = 0
for bk in bucket:
# 桶內進行排序
bk = insertionSort(bk)
for val in bk:
arr[index] = val
index += 1
return arr
這裏桶排序有點像哈希表,都是經過散列函數(映射函數),把值放入相應的鍵(桶)中。和哈希表不同之處,在於桶排序要求的映射函數必須是單調函數,這樣纔有意義,哈希表的散列函數需要滿足不同的值映射後的值儘可能不一樣。
9. 基數排序
基數排序是,先根據各個數的低位,進行排序;排序後的數組,根據次低位進行排序,重複直到完成最高位的比較,輸出排序的序列。
def radixSort(arr,radix=10):
'''基數排序,radix爲基數'''
K = len(str(max(arr)))
for i in range(K):
bucket = [[] for _ in range(10)]
for val in arr:
if len(str(val))-i >= 1:
num = int(str(val)[len(str(val))-i-1])
bucket[num].append(val)
else:
bucket[0].append(val)
# 合併桶
del arr
arr = []
for bk in bucket:
arr.extend(bk)
return arr
這裏爲了方便,使用了10爲基數。基數代表值由什麼進製表示。radix 可以取2,當時取位的時候,也得先把值轉化成2進制:
val%(radix**i)//(radix**(i-1))
這裏 代表取值 val 在 radix 進制下的第幾位。
10. 歸併排序
歸併排序是通過不斷的劃分成兩個子數組,直到子數組只有1個值(相當於排序好了),然後不斷合併兩個排序好的子數組,直到合併成原來大小的數組。
def merge(arr, left, mid, right):
l = left
r = mid+1
n_arr = [0 for _ in range(right-left+1)]
index = 0
while l <= mid and r<=right:
if arr[l] > arr[r]:
n_arr[index] = arr[r]
r += 1
else:
n_arr[index] = arr[l]
l += 1
index += 1
while l <= mid:
n_arr[index] = arr[l]
index += 1
l += 1
while r <= right:
n_arr[index] = arr[r]
index += 1
r += 1
# 拷貝排序完成的arr
for i in range(len(n_arr)):
arr[left+i] = n_arr[i]
def mSort(arr, left, right):
if left < right:
mid = (left+right)//2
mSort(arr, left, mid)
mSort(arr, mid+1, right)
merge(arr, left, mid, right)
def mergeSort(arr):
mSort(arr, 0, len(arr)-1)
return arr
arr = mergeSort(arr)
11. 總結
不同排序方法的複雜度:
實踐測試,選擇大小爲10000
的隨機數組,經過這幾種算法排序,花費的時間:
比如:
import time
# 冒泡法
arr = get_arr()
start = time.time()
arr = bubbleSort(arr)
end = time.time()
print('冒泡法use {:.4f}'.format(end-start))
運行結果:
# O(n + k)
桶排序use 0.0160
# O(n*k)
基數排序use 0.0748
# O(nlogn)
歸併排序use 0.0917
# O(nlogn)
快排use 0.1123
# O(nlogn)
堆排序use 0.1207
# O(nlogn)
希爾排序use 0.1466
# O(n^2)
選擇排序use 11.3514
# O(n^2)
插入排序use 17.5552
# O(n^2)
冒泡法use 21.4438
此外,排序算法的穩定性,是能保證排序前2個相等的數其在序列的前後位置順序和排序後它們兩個的前後位置順序相同。比如,如果Ai = Aj,Ai原來在位置前,排序後Ai還是要在Aj位置前。
因此,類選擇排序(選擇,希爾,堆以及快排)都是不穩定排序。
參考: