快速排序总结

快速排序(Quick Sort)凭借 O(n log n) 的平均时间复杂度、原地排序的空间优势,以及实际应用中极高的运行效率,成为处理大规模数据排序的 "首选方案"。

一、快速排序的核心原理

核心是 **"选基准、分区间、递归排序"**------ 通过一趟排序将数组分为 "小于基准" 和 "大于基准" 的两个子区间,再对两个子区间重复该过程,直到所有子区间长度为 1(天然有序)。

1.1 三大核心步骤(以升序排序为例)

假设待排序数组为nums,排序区间为[left, right](初始为[0, len(nums)-1]),完整流程如下:

  1. 选择基准(Pivot):从数组中选一个元素作为 "基准"(如区间第一个元素、最后一个元素、中间元素,或随机元素);
  2. 分区(Partition) :将数组重新排列,使所有小于基准 的元素移到基准左侧,所有大于基准 的元素移到基准右侧(等于基准的元素可左可右),最终基准落在 "正确的排序位置" 上(记为pivot_idx);
  3. 递归排序 :对基准左侧的子区间[left, pivot_idx-1]和右侧的子区间[pivot_idx+1, right]分别重复步骤 1-2,直到子区间长度为 0 或 1(无需排序)。

1.2 关键:分区(Partition)过程的实现

分区是快速排序的 "灵魂",直接决定算法的效率。这里以 " Lomuto 分区法 "(简单易懂,适合入门)为例,拆解分区过程:

  • 目标:将基准(假设选区间最后一个元素nums[right])放到正确位置,左侧元素≤基准,右侧元素≥基准;
  • 步骤:
    1. 初始化 "小于基准区间的右边界"i = left - 1(初始时 "小于基准的区间" 为空);
    2. 遍历区间[left, right-1],对每个元素nums[j]
      • nums[j] ≤ 基准:将i加 1,交换nums[i]nums[j](扩展 "小于基准的区间");
    3. 遍历结束后,i+1即为基准的正确位置,交换nums[i+1]nums[right],返回i+1(基准索引)。
分区过程示例(数组[3, 1, 4, 1, 5],基准选最后一个元素5):
  1. 初始:left=0, right=4,基准pivot=5i=-1
  2. 遍历j=0nums[j]=3):3≤5 → i=0,交换nums[0]nums[0](无变化);
  3. 遍历j=1nums[j]=1):1≤5 → i=1,交换nums[1]nums[1](无变化);
  4. 遍历j=2nums[j]=4):4≤5 → i=2,交换nums[2]nums[2](无变化);
  5. 遍历j=3nums[j]=1):1≤5 → i=3,交换nums[3]nums[3](无变化);
  6. 遍历结束:交换nums[4]i+1=4)和nums[4](基准),返回4(基准5已在正确位置)。

1.3 时间复杂度与空间复杂度

场景 时间复杂度 空间复杂度 说明
平均情况 O(n log n) O(log n) 每次分区将数组分为两个近似等长的子区间,递归深度为 log n(递归栈空间)
最坏情况 O(n²) O(n) 数组已有序 / 逆序,每次分区选到 "最大 / 最小元素" 为基准,递归深度为 n
最好情况 O(n log n) O(log n) 每次分区选到 "中位数" 为基准,子区间完全等长
原地排序优化 - O(1) 用迭代代替递归(手动维护栈),消除递归栈空间消耗

**为什么实际应用中快速排序比归并排序快?**归并排序时间复杂度稳定为 O (n log n),但需要 O (n) 的额外空间,且存在大量数组拷贝操作;快速排序是原地排序(除递归栈外无额外空间),缓存命中率更高,实际运行时常数项更小,因此处理大规模数据时更快。

二、基础实现: Lomuto 分区法与 Hoare 分区法

快速排序的分区实现有两种主流方式:Lomuto 分区法(简单但效率略低)和 Hoare 分区法(高效,Tony Hoare 原版实现)。下面分别给出代码实现,并对比两者差异。

2.1 Lomuto 分区法(选最后一个元素为基准)

适合入门,逻辑简单,但交换次数较多,且最坏情况(有序数组)下效率低。

python 复制代码
def quick_sort_lomuto(nums, left, right):
    """
    快速排序(Lomuto分区法):升序排序
    :param nums: 待排序数组
    :param left: 排序区间左边界
    :param right: 排序区间右边界
    """
    # 递归终止条件:区间长度≤1(无需排序)
    if left >= right:
        return
    
    # 1. 分区:返回基准的正确索引
    pivot_idx = partition_lomuto(nums, left, right)
    
    # 2. 递归排序左子区间和右子区间
    quick_sort_lomuto(nums, left, pivot_idx - 1)
    quick_sort_lomuto(nums, pivot_idx + 1, right)

def partition_lomuto(nums, left, right):
    """Lomuto分区:选nums[right]为基准"""
    pivot = nums[right]  # 基准元素
    i = left - 1         # 小于基准区间的右边界(初始为空)
    
    # 遍历区间[left, right-1],将小于基准的元素移到左侧
    for j in range(left, right):
        if nums[j] <= pivot:
            i += 1
            # 交换nums[i]和nums[j],扩展小于基准的区间
            nums[i], nums[j] = nums[j], nums[i]
    
    # 将基准移到正确位置(i+1)
    nums[i + 1], nums[right] = nums[right], nums[i + 1]
    return i + 1  # 返回基准索引

# 测试案例
if __name__ == "__main__":
    test_nums = [3, 1, 4, 1, 5, 9, 2, 6]
    quick_sort_lomuto(test_nums, 0, len(test_nums) - 1)
    print("Lomuto分区法排序结果:", test_nums)  # 输出:[1, 1, 2, 3, 4, 5, 6, 9]

2.2 Hoare 分区法(选第一个元素为基准)

Hoare 分区法是快速排序的原版实现,通过 "双指针从两端向中间扫描" 实现分区,交换次数更少,效率比 Lomuto 高约 30%,但逻辑稍复杂。

核心逻辑

  • 左指针ileft开始向右移,直到找到nums[i] > 基准

  • 右指针jright开始向左移,直到找到nums[j] < 基准

  • 交换nums[i]nums[j],重复上述步骤,直到i >= j

  • 最后交换nums[left](基准)和nums[j],使基准落在正确位置。

    python 复制代码
    def quick_sort_hoare(nums, left, right):
        """
        快速排序(Hoare分区法):升序排序
        :param nums: 待排序数组
        :param left: 排序区间左边界
        :param right: 排序区间右边界
        """
        if left >= right:
            return
        
        pivot_idx = partition_hoare(nums, left, right)
        quick_sort_hoare(nums, left, pivot_idx - 1)
        quick_sort_hoare(nums, pivot_idx + 1, right)
    
    def partition_hoare(nums, left, right):
        """Hoare分区:选nums[left]为基准"""
        pivot = nums[left]  # 基准元素
        i = left            # 左指针(从左向右扫)
        j = right           # 右指针(从右向左扫)
        
        while i < j:
            # 1. 右指针向左移,找到第一个小于基准的元素(注意:必须先移动右指针)
            while i < j and nums[j] >= pivot:
                j -= 1
            
            # 2. 左指针向右移,找到第一个大于基准的元素
            while i < j and nums[i] <= pivot:
                i += 1
            
            # 3. 交换两个指针指向的元素
            nums[i], nums[j] = nums[j], nums[i]
        
        # 4. 将基准移到正确位置(i=j处)
        nums[left], nums[j] = nums[j], nums[left]
        return j  # 返回基准索引
    
    # 测试案例
    if __name__ == "__main__":
        test_nums = [3, 1, 4, 1, 5, 9, 2, 6]
        quick_sort_hoare(test_nums, 0, len(test_nums) - 1)
        print("Hoare分区法排序结果:", test_nums)  # 输出:[1, 1, 2, 3, 4, 5, 6, 9]

    关键注意点 :Hoare 分区法中,必须先移动右指针 再移动左指针,否则可能导致基准位置错误(例如数组[2, 1, 3],若先移左指针会跳过1,最终基准无法正确归位)。

    三、快速排序的三大优化

    基础版快速排序在某些场景下存在明显缺陷(如有序数组下时间复杂度退化为 O (n²)),通过以下三大优化,可将其性能提升至 "工业级" 水平。

    3.1 优化 1:随机选择基准(避免最坏情况)

    问题根源 :基础版快速排序固定选 "第一个 / 最后一个元素" 为基准,若数组已有序 / 逆序,每次分区都会选到 "最大 / 最小元素",导致子区间长度为n-10,递归深度变为n,时间复杂度退化为 O (n²)。优化方案随机选择基准 ------ 从当前区间[left, right]中随机选一个元素作为基准,再与 "第一个 / 最后一个元素" 交换,后续分区逻辑不变。效果:随机基准能极大降低 "选到极端值" 的概率,使平均时间复杂度稳定在 O (n log n),最坏情况几乎不会出现。

    python 复制代码
    import random
    
    def quick_sort_random_pivot(nums, left, right):
        """快速排序(随机基准优化)"""
        if left >= right:
            return
        
        # 优化1:随机选择基准,与nums[right]交换(后续用Lomuto分区法)
        random_idx = random.randint(left, right)
        nums[random_idx], nums[right] = nums[right], nums[random_idx]
        
        # 分区+递归排序
        pivot_idx = partition_lomuto(nums, left, right)
        quick_sort_random_pivot(nums, left, pivot_idx - 1)
        quick_sort_random_pivot(nums, pivot_idx + 1, right)
    
    # 测试有序数组(基础版会退化,优化版无压力)
    if __name__ == "__main__":
        sorted_nums = [1, 2, 3, 4, 5, 6, 7, 8]
        quick_sort_random_pivot(sorted_nums, 0, len(sorted_nums)-1)
        print("随机基准优化排序结果:", sorted_nums)  # 输出:[1, 2, 3, 4, 5, 6, 7, 8]

3.2 优化 2:小数组用插入排序(减少递归开销)

问题根源 :快速排序的递归调用存在函数栈开销,对于长度较小的子区间(如n ≤ 10),递归的 "overhead"(额外开销)甚至超过排序本身的时间,而插入排序在小数组上的实际效率更高(插入排序对近乎有序的数组也有优势)。优化方案 :当子区间长度right - left + 1 ≤ 阈值(通常取 10~20)时,改用插入排序;仅对长度大于阈值的子区间继续用快速排序。效果:减少约 50% 的递归调用次数,整体效率提升 10%~20%。

python 复制代码
def insertion_sort(nums, left, right):
    """插入排序:对nums[left..right]进行升序排序"""
    for i in range(left + 1, right + 1):
        key = nums[i]  # 当前待插入元素
        j = i - 1
        # 找到key的插入位置
        while j >= left and nums[j] > key:
            nums[j + 1] = nums[j]
            j -= 1
        nums[j + 1] = key

def quick_sort_hybrid(nums, left, right):
    """快速排序(混合插入排序优化)"""
    # 优化2:小数组用插入排序(阈值设为10)
    if right - left + 1 <= 10:
        insertion_sort(nums, left, right)
        return
    
    # 优化1:随机基准
    random_idx = random.randint(left, right)
    nums[random_idx], nums[right] = nums[right], nums[random_idx]
    
    pivot_idx = partition_lomuto(nums, left, right)
    quick_sort_hybrid(nums, left, pivot_idx - 1)
    quick_sort_hybrid(nums, pivot_idx + 1, right)

# 测试包含小数组的场景
if __name__ == "__main__":
    test_nums = [5, 3, 8, 6, 2, 7, 1, 4, 0, 9, 11, 10]
    quick_sort_hybrid(test_nums, 0, len(test_nums)-1)
    print("混合插入排序优化结果:", test_nums)  # 输出:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

3.3 优化 3:三数取中(解决重复元素问题)

问题根源 :若数组中存在大量重复元素(如[2, 1, 2, 2, 3, 2]),基础版快速排序会将 "等于基准的元素" 全部归到一侧,导致子区间长度失衡,效率下降;此外,随机基准在重复元素较多时,仍可能选到 "极端值"。优化方案三数取中 ------ 从区间的 "左端点、中点、右端点" 三个元素中选 "中位数" 作为基准,确保基准尽可能处于区间中间位置。效果:在重复元素较多的场景下,子区间长度更均衡,时间复杂度稳定在 O (n log n),同时避免了随机基准的 "不确定性"。

python 复制代码
def median_of_three(nums, left, right):
    """三数取中:返回left、mid、right三个位置中值的索引"""
    mid = left + (right - left) // 2  # 避免溢出
    
    # 排序三个位置的元素,使nums[left] ≤ nums[mid] ≤ nums[right]
    if nums[left] > nums[mid]:
        nums[left], nums[mid] = nums[mid], nums[left]
    if nums[left] > nums[right]:
        nums[left], nums[right] = nums[right], nums[left]
    if nums[mid] > nums[right]:
        nums[mid], nums[right] = nums[right], nums[mid]
    
    # 返回中位数索引(mid),并与nums[right-1]交换
相关推荐
Haooog2 小时前
111.二叉树的最小深度(二叉树算法题)
java·数据结构·算法·二叉树
地平线开发者2 小时前
模型插入 NV12 预处理节点精度问题排查流程
算法·自动驾驶
我要学习别拦我~2 小时前
逻辑回归中的成本损失函数全解析:从数学推导到实际应用
算法·机器学习·逻辑回归
元亓亓亓3 小时前
LeetCode热题--200. 岛屿数量--中等
算法·leetcode·职场和发展
学海一叶3 小时前
Agent开发02-关键思想(ReAct、ReWOO、Reflexion、LLM Compiler等)
人工智能·算法·llm·agent·plan
Learn Beyond Limits4 小时前
Initializing K-means|初始化K-means
人工智能·python·算法·机器学习·ai·kmeans·吴恩达
我想吃余4 小时前
【0基础学算法】前缀和刷题日志(一):前缀与后缀的交织
算法·leetcode
dragoooon344 小时前
[优选算法专题三二分查找——NO.18在排序数组中查找元素的第一个和最后一个位置]
数据结构·c++·算法·leetcode·学习方法
未知陨落4 小时前
LeetCode:72.每日温度
算法·leetcode