快速排序的幾種實現方式


快速排序(quick sort)的特點是分塊排序,也叫划分交換排序(partition-exchange sort)

代碼實現方式可以有這么幾種:

  1. 拼接結果
  2. 左右相互交換
  3. 快慢指針

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 位)作為基准值
 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]
  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]

[]

延伸閱讀


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM