在此整理出幾種經典的排序算法:
- 插入排序:直接插入排序、折半插入排序、希爾排序
- 交換排序:冒泡排序、快速排序
- 選擇排序:簡單選擇排序
- 歸併排序
- 堆排序
一、插入排序
1. 直接插入排序
介紹:將原始數組分成有序區[0, i-1]
和無序區[i, n-1]
兩塊,每次將無序區的第一個元素nums[i]
和有序區的元素nums[i-1]~nums[0]
從後往前比較,插入到有序區中合適的位置。
複雜度:時間複雜度,空間複雜度
穩定性:不涉及到元素的交換,所以穩定
def InsertSort(self, nums, n):
"""
依次將無序區中第一個元素插入到有序區的對應位置,從後往前比較
時間複雜度n^2, 空間複雜度1
"""
for i in range(1, len(nums)):
if nums[i] < nums[i-1]: # 無序區第一個元素小於有序區最後一個元素,需要插入
tmp = nums[i] # 取出無序區第一個元素
j = i - 1
while j >= 0 and nums[j] >= tmp:
nums[j+1] = nums[j] # 後移一個位置
j -= 1
nums[j + 1] = tmp
return nums
2. 折半插入排序
介紹:直接插入排序,將無序區的首元素插入到有序區的合適位置,可以使用二分查找提高查找效率。具體來講,二分查找是查找是以nums[i]
爲target,查找有序區第一個大於target的位置,那麼這個位置就是需要插入的位置。
複雜度:時間複雜度,空間複雜度
穩定性:注意二分查找的判斷條件,當查找的條件是第一個大於target的元素,此時是穩定的。
def BiInsertSort(self, nums, n):
"""
在直接插入排序的基礎上用二分查找來查找無序區元素需要插入的具體位置
二分查找,找第一個大於該無序區元素的位置
時間複雜度nlogn
"""
for i in range(1, len(nums)):
if nums[i] < nums[i-1]:
tmp = nums[i]
# 二分查找左邊界:第一個大於target的位置
left, right = 0, i - 1
while left <= right: # 搜索區間閉區間[]
mid = left + (right - left) // 2
if nums[mid] < tmp:
left = mid + 1
elif nums[mid] > tmp:
right = mid - 1
elif nums[mid] == tmp:
left = mid + 1
if left >= n:
pos = -1
pos = left
for j in range(i - 1, pos - 1, -1):
nums[j + 1] = nums[j]
nums[pos] = tmp # 先後移,再填這個位置
return nums
3. 希爾排序
介紹:可以理解爲是直接插入排序的一種並行操作,即將原數組按照不同的步長增量分成若干組,如根據下標分成[0, 3, 6]、[1, 4, 7]、[2, 5, 8]
三組,再在每個組內進行直接插入排序。經過第一輪之後組內是有序的,第二輪再減少步長,重新分組[0, 2, 4, 8]、[1, 3, 5, 7]
,組內再次進行直接插入排序。最後一輪步長減少到1,進行最後一次的直接插入排序。希爾排序一般來說,初始步長爲,每次都減少爲原來的,直到爲1時停止。
複雜度:時間複雜度,空間複雜度
穩定性:組內的插入排序是穩定的,但是組間互相獨立,所以組間可能造成不穩定的情況。因此是不穩定的。
def HillSort(self, nums, n):
"""
在直接插入排序的基礎上,進行對不同增量基於下標的分組,每組之內進行直接插入排序,最後到增量爲1時停止
在開始時,分組較多,那麼每組之內直接插入排序的複雜度就很低,當增量變小分組變多時,雖然每組的數變多了,但是由於之前
已經變得比較有序,所以移動的次數較少
時間複雜度n^3/2
"""
dist = n // 2 # 初始增量設置爲長度的一半
while dist > 0:
for i in range(dist, n): # 從每個分組的第二個元素開始進行直接插入排序
tmp = nums[i] # 無序區第一個元素
j = i - dist # 有序區最後一個元素
while j >= 0 and nums[j] >= tmp: # 在組內從後往前找到第一個小於tmp的位置
nums[j + dist] = nums[j]
j -= dist
nums[j + dist] = tmp
dist //= 2
return nums
二、交換排序
4. 冒泡排序
介紹:將原數組豎向排列,分成上下兩部分,爲有序區[0, i-1]
和無序區[i, n-1]
。每次都從無序區的最底部j~[n-1, i]
開始,通過交換將無序區的最小元素交換到無序區的首位。
複雜度:時間複雜度,空間複雜度
穩定性:當兩個相鄰元素相等時,不會發生交換,所以是穩定的。
def BubbleSort(self, nums, n):
"""
通過從下往上比較相鄰元素,交換,把無序區的最小元素移到第一個
時間複雜度n^2
"""
for i in range(n): # i代表無序區第一個元素的位置,無序區[i, n-1]
for j in range(n-1, i, -1): # 從下往上
if nums[j] < nums[j - 1]: # 下面的元素小於上面的元素,發生交換
nums[j], nums[j-1] = nums[j-1], nums[j]
return nums
5. 快速排序
介紹:定義一趟劃分,根據基準pivot
將數組分成前後兩部分,其中前面部分元素都小於pivot
,後面都大於等於pivot
。然後上一趟劃分過後的兩部分,分別對每個子序列再次進行劃分。
複雜度:一趟劃分複雜度爲,遞歸子樹的高度爲,所以時間複雜度爲;空間複雜度爲
穩定性:在一趟劃分中,如果pivot=nums[left]
,那麼分治法會將右邊第一個小於pivot
的元素放到首位,會改變元素的相對順序。如[5,3,4,3,6,7]
,一趟劃分後後面的3
會被移動到首位。所以不穩定。
def QuickSort(self, nums, n):
"""
定義劃分partition,根據Pivot劃分成兩塊
對左右兩部分遞歸調用partition
時間複雜度nlogn
不穩定 [5,3,4,3,6,7] 中樞3和5交換,改變了3的相對順序
"""
def partition(left, right):
pivot = nums[left] # 基準都選爲第一個
i, j = left, right
while i < j:
while i < j and nums[j] >= pivot:
j -= 1
nums[i] = nums[j]
while i < j and nums[i] < pivot:
i += 1
nums[j] = nums[i]
nums[i] = pivot
return i
def helper(left, right):
if left < right:
pivot_index = partition(left, right)
helper(left, pivot_index - 1)
helper(pivot_index + 1, right)
helper(0, n-1)
return nums
三、選擇排序
6. 簡單選擇排序
介紹:將原始數組分成有序區[0, i-1]
和無序區[i, n-1]
兩塊,每次從無序區[i, n-1]
中通過遍歷的方式選取出最小的元素,和無序區的首元素交換。
複雜度:時間複雜度,空間複雜度
穩定性:每次都是將無序區的最小元素和無序區的首元素交換,會改變元素的相對順序,所以不穩定。
def SelectSort(self, nums, n):
"""
每次以遍歷的方式找到無序區中最小元素,放到有序區的後面——最小元素和無序區第一個元素交換
時間複雜度n^2
不穩定 [5,2,2]爲例
"""
for i in range(n): # i爲無序區第一個元素的位置,無序區[i,n-1]
minarg = -1 # 最小元素下標
minn = 0x3f3f3f3f
for j in range(i, n):
if nums[j] < minn:
minarg = j
minn = nums[j]
nums[minarg], nums[i] = nums[i], nums[minarg] # 最小元素移到無序區第一個位置
return nums
四/7、歸併排序
介紹:將原數組遞歸的兩兩合併成爲一個最終有序數組的過程。子問題就是合併兩個有序數組
複雜度:,空間複雜度
穩定性:因爲每次都是合併相鄰的數組成爲一個有序數組,所以是穩定的。
def MergeSort(self, nums, n):
"""
對原始數組進行相鄰的兩兩分組,相鄰分組合並起來,依次合併,整個過程是二叉樹的倒形態
時間複雜度nlogn(一趟歸併n,二路歸併一共要logn次)
穩定
"""
def merge(left, right):
"""
合併兩個有序數組Left,right,返回合併後的數組
"""
res = []
i, j = 0, 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
res.append(left[i])
i += 1
elif left[i] >= right[j]:
res.append(right[j])
j += 1
if i < len(left):
res += left[i:]
if j < len(right):
res += right[j:]
return res
def helper(ary): # 對ary進行歸併排序,得到歸併排序後的數組
if len(ary) == 1:
return ary
num = len(ary) // 2
left = helper(ary[:num])
right = helper(ary[num:])
return merge(left, right)
nums = helper(nums)
return nums
五/8、堆排序
介紹:首先根據原數組構建大根堆,此時樹的根元素爲當前最大元素,然後將根節點和樹中最右下的元素交換,就可以刪除根節點了,然後再調整使其滿足大根堆。重複,直到這個大根堆的長度爲1,此時完成排序。
複雜度:每次調整堆,調整次,所以時間複雜度,空間複雜度爲
穩定性:涉及到堆元素的交換和調整,不穩定
def HeapSort(self, nums, n):
"""
先構建一個大根堆。
然後每次將堆頂和堆中最右下的節點交換,這樣大的堆頂就被移到了數組後面。
然後調整除最後一個元素外的數組,使其還是大根堆
不穩定
"""
def max_heapify(ary, start, end):
"""
將ary[start:end]調整成爲大根堆
"""
root = start # 數組的第一個元素總是堆頂,即root
while True:
child = 2 * root + 1 # 左子節點的序號
# 下面取子節點比較大的那個child序號
if child > end:
break
if child + 1 <= end and ary[child + 1] > ary[child]: # 存在右子節點,且右子節點大於左子節點
child = child + 1
if ary[root] < ary[child]:
ary[root], ary[child] = ary[child], ary[root]
root = child
else: # 當前已是大根堆,無需更新
break
first = n // 2 - 1
for start in range(first, -1, -1):
max_heapify(nums, start, n-1) # 根據原始數組構建大根堆
for end in range(n-1, 0, -1):
nums[0], nums[end] = nums[end], nums[0]
max_heapify(nums, 0, end - 1)
return nums
最後給出一張對比圖