快速排序(quick sort)的特點是分塊排序,也叫划分交換排序(partition-exchange sort)
代碼實現方式可以有這么幾種:
- 拼接結果
- 左右相互交換
- 快慢指針
1. 拼接結果
# Python3
class Solution:
def quicksort(self, nums):
# 當為 0 個或 1 個時,肯定有序,直接返回
if len(nums) < 2:
return nums
else:
# 選擇第一位作為中位數
mid = nums[0]
less = [num for num in nums[1:] if num <= mid]
greater = [num for num in nums[1:] if num > mid]
return self.quicksort(less) + [mid] + self.quicksort(greater)
這種方式最直觀,最好理解,但效率不高。為了找出大於和小於中位數的元素,循環遍歷了 2 次
做一點小小的修改,改為一次遍歷:
class Solution:
def quicksort(self, nums):
if len(nums) < 2:
return nums
else:
mid = nums[0]
less, greater = self.partition(nums, mid)[0], self.partition(nums, mid)[1]
return self.quicksort(less) + [mid] + self.quicksort(greater)
def partition(self, nums, mid):
less, greater = [], []
for num in nums[1:]:
if num <= mid:
less.append(num)
else:
greater.append(num)
return less, greater
優化后,運行時間降低了,但空間使用還很高,每次遞歸都額外需要 2 個平均長度為 ¼ n 的數組
1 + 2 ... + n-1 + n = ((n + 1) * n ) / 2
平均值 = ((n + 1) * n ) / 2 / n = (n + 1) / 2
兩個數組平分平均值: (n + 1) / 2 / 2 ≈ 1/4 n
2. 左右相互交換
其實可以不使用額外空間,直接操作原數組。選擇一個基准值,將小於它和大於它的元素相互交換。
class Solution:
def quicksort(self, nums):
self.quick_sort(nums, 0, len(nums) - 1)
def quick_sort(self, nums, start, end):
# end - start < 1
if start >= end:
return
# 每次使用最后一個數作為基准值
pivot_index = end
pivot = nums[pivot_index]
left, right = start, end - 1
while left < right:
# 左邊跳過所有小於基准值的元素
while nums[left] <= pivot and left < right:
left += 1
# 右邊跳過所有大於基准值的元素
while nums[right] > pivot and left < right:
right -= 1
# 交換
nums[left], nums[right] = nums[right], nums[left]
# 此時左右指針重合(left == right),其指向元素可能大於基准值
if nums[left] > pivot:
nums[left], nums[pivot_index] = nums[pivot_index], nums[left]
# 使 left 始終作為較大區間的第 1 個元素
else:
left += 1
self.quick_sort(nums, start, left - 1)
# pivot 不一定在中間,所以包含 left
self.quick_sort(nums, left, end)
使用此種方式,最好要將開頭(或末尾)的元素設為基准值。如果使用中間元素,最后也交換到開頭(或末尾),否則將考慮大量場景。
排序過程:
[6 5 3 1 8 7 2 4]
↑ ↑ ^
[2 5 3 1 8 7 6 4]
↑ ↑ ^
[2 1 3 5 8 7 6 4]
↑↑ ^
[2 1 3 4 8 7 6 5]
^
[2 1 3][4 8 7 6 5]
nums[left] <= pivot 時:
[6 7 3 4 8 1 2 5]
↑ ↑ ^
[2 7 3 1 8 1 6 5]
↑ ↑ ^
[2 1 3 4 8 7 6 5]
↑↑ ^
[2 1 3 4][8 7 6 5]
3. 快慢指針
上面這種方式其實使用兩個相向指針,也可以使用同向快慢指針實現元素交換。
class Solution:
def quicksort(self, nums):
import random
def quick_sort(left, right):
# right - left < 1
if left >= right:
return
# 隨機選擇一個元素作為 pivot
pivot_index = random.randint(left, right)
pivot = nums[pivot_index]
# 1. 將中位數與末尾數交換,便於操作
nums[pivot_index], nums[right] = nums[right], nums[pivot_index]
# 2. 使用快慢指針,將所有小於中位數的元素移動到左邊
store_index = left
for i in range(left, right):
if nums[i] <= pivot:
nums[store_index], nums[i] = nums[i], nums[store_index]
store_index += 1
# 3. store_index 位置元素肯定大於等於 pivot,所以交換
nums[right], nums[store_index] = nums[store_index], nums[right]
# 因為 pivot 在中間,所以減 1
quick_sort(left, store_index - 1)
# 因為 pivot 在中間,所以加 1
quick_sort(store_index + 1, right)
quick_sort(0, len(nums) - 1)
排序過程:
[6 5 3 1 8 7 2 4]
↑↑ ^
[6 5 3 1 8 7 2 4]
↑ ↑ ^
[3 5 6 1 8 7 2 4]
↑ ↑ ^
[3 1 6 5 8 7 2 4]
↑ ↑ ^
[3 1 2 5 8 7 6 4]
↑ ^
[3 1 2 4 8 7 6 5]
^
[3 1 2][4][8 7 6 5]
隨機選擇可以增加每次選擇的基准值為中位數的幾率
時間復雜度
最壞時間復雜度
每次基准值都是最大 (或最小)值時,所需遞歸次數最多,有兩種情況:
- 數組有序時,每次使用最后 1 位(或第 1 位)作為基准值
1 2 3 4 5 6 7 8
^
1 2 3 4 5 6 7 [8]
^
1 2 3 4 5 6 [7]
^
1 2 3 4 5 [6]
^
1 2 3 4 [5]
^
1 2 3 [4]
^
1 2 [3]
^
1 [2]
^
[1]
- 隨機選擇時,每次選擇到最大(或最小)的一位
6 7 3 4 8 1 2 5
^
6 7 3 4 1 2 5 [8]
^
6 3 4 1 2 5 [7] 8
^
3 4 1 2 5 [6] 7 8
^
3 4 1 2 [5] 6 7 8
^
3 1 2 [4] 5 6 7 8
^
1 2 [3] 4 5 6 7 8
^
1 [2] 3 4 5 6 7 8
^
[1] 2 3 4 5 6 7 8
此時遞歸次數為 n + 1,平均每次排序 ½ n 個數。所以最壞時間復雜度:O(n^2)。
最好時間復雜度
如果每次選擇中位數作為基准值,遞歸次數會減少么?其實不會減少,但遞歸中遍歷的次數會減少。如果每層遍歷看成 n 次的話,可以用下面的這個圖表示:
所以最好時間復雜度為:O(n * log n)
平均時間復雜度
最壞時間復雜度的情況很少見,所以平均時間復雜度就是最好時間復雜度 O(n * log n)
空間復雜度
每次遞歸均會使用額外空間,所以空間復雜度跟遞歸次數有關。
最壞時間復雜度時,最壞空間復雜度也為 O(n)。最好時間復雜度時時,雖然遞歸沒有減少,但當只有 1 個或 0 個元素時,沒有使用額外空間,直接返回,所以最好空間復雜度為 O(log n)。平均時間復雜度也為 O(log n)。
第 1 種實現因為使用額外數組,最壞空間復雜度為 O(n^2),最好空間復雜度為 O(n * log n),
測試代碼
import logging
logging.basicConfig(level=logging.INFO)
def main():
# nums = [3, 2, 1, 5, 6, 4]
# 針對第 1 種
print(Solution().quicksort(nums))
# 針對第 2、3 種
# Solution().quicksort(nums)
# print(nums)
if __name__ == '__main__':
main()
測試用例
[3, 2, 1, 5, 6, 4]
[3, 2, 1, 5, 6, 4, 4, 1]
[6, 5, 3, 1, 8, 7, 2, 4]
[]
