从入门到精通:快速排序的核心原理、实现与优化

在排序算法的世界里,快速排序绝对是"明星选手"------它凭借平均O(n log n)的时间复杂度、原地排序的特性,成为实际开发中最常用的排序算法之一,也是面试中高频考察的重点。无论是处理大规模数据,还是应对算法笔试,掌握快速排序的原理、实现和优化技巧,都能让你事半功倍。

今天,我们就从"是什么、怎么实现、如何优化、用在哪"四个维度,彻底搞懂快速排序,全程搭配Python代码示例,新手也能轻松跟上节奏~

一、快速排序的核心思想:分而治之,化繁为简

快速排序由计算机科学家托尼·霍尔(C.A.R. Hoare)于1960年提出,本质是一种基于"分治思想"的排序算法,核心逻辑可以用一句话概括:选一个基准,分左右两堆,递归排序,像整理文件夹一样简单高效。

具体拆解为3个关键步骤,通俗易懂不绕弯:

  1. 选择基准(pivot):从待排序数组中,任意选择一个元素作为"基准"------可以是第一个、最后一个,也可以是中间元素,甚至是随机元素,基准的选择会直接影响排序效率。

  2. 分区操作(partition):重新排列数组,让所有小于基准的元素都排在基准左边,所有大于基准的元素都排在基准右边,基准元素最终会落在它"应有的位置"(排序后正确的位置)上。

  3. 递归排序:对基准左边的子数组和右边的子数组,重复上面的"选基准、分区"操作,直到每个子数组只剩下1个元素(此时子数组本身就是有序的),整个排序过程完成。

举个直观的例子:对数组 [3, 6, 8, 10, 1, 2, 1] 进行快速排序,步骤如下:

  • 选基准:假设选择第一个元素3作为基准;

  • 分区:将数组分为 [1, 2, 1](小于3)和 [6, 8, 10](大于3),基准3落在中间的正确位置;

  • 递归:分别对 [1, 2, 1] 和 [6, 8, 10] 重复操作,最终得到有序数组 [1, 1, 2, 3, 6, 8, 10]。

这里要注意一个关键:快速排序是"原地排序"(不需要额外开辟大量空间存储子数组),空间复杂度主要来自递归调用栈,这也是它比归并排序更节省空间的核心优势。

二、快速排序的Python实现:从简单到优化

快速排序的实现有多种方式,我们从"最易理解"的基础版本入手,再逐步优化,兼顾可读性和效率。

2.1 基础版本(易懂但不够高效)

这个版本的逻辑最直观,通过列表推导式拆分左右子数组,递归调用自身,代码简洁,适合新手入门理解原理,但会额外开辟空间,不是严格意义上的原地排序。

cs 复制代码
#include <stdio.h>
#include <stdlib.h>

// 基础版本(易懂但不够高效,非原地排序)
void quick_sort_basic(int arr[], int len, int *result, int *index) {
    // 递归终止条件:数组长度≤1
    if (len <= 1) {
        if (len == 1) {
            result[(*index)++] = arr[0]; // 将单个元素存入结果数组
        }
        return;
    }
    // 选择第一个元素作为基准
    int pivot = arr[0];
    int less[len], greater[len];
    int less_len = 0, greater_len = 0;
    
    // 拆分:小于等于基准的放入less,大于基准的放入greater
    for (int i = 1; i < len; i++) {
        if (arr[i] <= pivot) {
            less[less_len++] = arr[i];
        } else {
            greater[greater_len++] = arr[i];
        }
    }
    
    // 递归排序左右子数组,拼接结果
    quick_sort_basic(less, less_len, result, index);
    result[(*index)++] = pivot;
    quick_sort_basic(greater, greater_len, result, index);
}

// 测试代码
int main() {
    int arr[] = {3, 6, 8, 10, 1, 2, 1};
    int len = sizeof(arr) / sizeof(arr[0]);
    int result[len];
    int index = 0;
    
    quick_sort_basic(arr, len, result, &index);
    
    printf("排序后的数组: ");
    for (int i = 0; i < len; i++) {
        printf("%d ", result[i]); // 输出:1 1 2 3 6 8 10
    }
    printf("\n");
    return 0;
}

优点:代码简洁、逻辑清晰,能快速理解快速排序的核心流程;

缺点:每次拆分都会开辟新的列表,空间复杂度较高(O(n)),且在数据量较大时,效率会受影响。

2.2 优化版本(原地排序,高效实用)

实际开发中,我们更常用"原地分区"的实现方式,通过双指针交换元素,避免额外空间开销,这也是面试中最常考察的写法(基于Lomuto分区方案)。

cs 复制代码
#include <stdio.h>

// 分区函数:返回基准元素的最终位置,实现原地分区(Lomuto分区方案)
int partition(int arr[], int low, int high) {
    // 选择数组最后一个元素作为基准(简化实现)
    int pivot = arr[high];
    // i指向"小于基准区域"的最后一个位置,初始为low-1(表示该区域为空)
    int i = low - 1;
    
    // 遍历从low到high-1的元素,调整分区
    for (int j = low; j < high; j++) {
        // 如果当前元素≤基准,就加入"小于基准区域"
        if (arr[j] <= pivot) {
            i++; // 扩大小于基准的区域
            // 交换元素
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    
    // 将基准元素放到它的最终位置(i+1)
    int temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;
    
    return i + 1; // 返回基准位置
}

// 递归函数:对low到high区间的元素进行排序
void quick_sort_recursive(int arr[], int low, int high) {
    if (low < high) {
        // 获得基准位置,拆分左右子数组
        int pivot_index = partition(arr, low, high);
        // 递归排序左子数组(基准左边)
        quick_sort_recursive(arr, low, pivot_index - 1);
        // 递归排序右子数组(基准右边)
        quick_sort_recursive(arr, pivot_index + 1, high);
    }
}

// 快速排序入口函数
void quick_sort(int arr[], int len) {
    quick_sort_recursive(arr, 0, len - 1);
}

// 测试代码
int main() {
    int arr[] = {3, 6, 8, 10, 1, 2, 1};
    int len = sizeof(arr) / sizeof(arr[0]);
    
    quick_sort(arr, len);
    
    printf("排序后的数组: ");
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]); // 输出:1 1 2 3 6 8 10
    }
    printf("\n");
    return 0;
}

这个版本的核心是原地分区:通过i和j两个指针,遍历数组并交换元素,不需要开辟新的列表,空间复杂度降低到O(log n)(主要来自递归调用栈),效率大幅提升。

2.3 进阶优化:随机化快速排序

上面的优化版本中,我们固定选择数组最后一个元素作为基准,这会存在一个问题:如果数组已经有序(或逆序),每次分区都会极度不平衡,导致时间复杂度退化到O(n²)(最坏情况)。

解决办法很简单:随机选择基准,减少最坏情况的发生概率,这就是随机化快速排序,也是实际应用中最常用的优化手段之一。

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 分区函数:随机选择基准,实现原地分区
int partition(int arr[], int low, int high) {
    // 初始化随机种子(仅首次调用时初始化)
    static int init = 0;
    if (!init) {
        srand((unsigned int)time(NULL));
        init = 1;
    }
    
    // 随机选择一个基准位置,与最后一个元素交换
    int pivot_index = low + rand() % (high - low + 1);
    int temp = arr[pivot_index];
    arr[pivot_index] = arr[high];
    arr[high] = temp;
    
    // 后续分区逻辑和之前一致
    int pivot = arr[high];
    int i = low - 1;
    for (int j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            i++;
            temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;
    
    return i + 1;
}

// 递归函数
void quick_sort_recursive(int arr[], int low, int high) {
    if (low < high) {
        int pivot_index = partition(arr, low, high);
        quick_sort_recursive(arr, low, pivot_index - 1);
        quick_sort_recursive(arr, pivot_index + 1, high);
    }
}

// 随机化快速排序入口
void quick_sort_randomized(int arr[], int len) {
    quick_sort_recursive(arr, 0, len - 1);
}

// 测试代码
int main() {
    int arr[] = {1, 2, 3, 4, 5, 6}; // 有序数组,测试最坏情况优化
    int len = sizeof(arr) / sizeof(arr[0]);
    
    quick_sort_randomized(arr, len);
    
    printf("排序后的数组: ");
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]); // 输出:1 2 3 4 5 6
    }
    printf("\n");
    return 0;
}

三、快速排序的性能分析:优势与局限

要真正掌握快速排序,必须理解它的性能特点------没有完美的算法,只有适合的场景,快速排序也不例外。

3.1 时间复杂度

  • 最好情况:每次分区都能将数组均匀分成两部分(平衡划分),时间复杂度为O(n log n)。例如,数组 [4,2,6,1,3,5,7] 选中间值4作为基准,每次分区左右子数组长度相近。

  • 平均情况:对于随机分布的数组,快速排序的平均时间复杂度为O(n log n),这也是它的核心优势------实际运行速度比同为O(n log n)的归并排序、堆排序更快(常数因子更小)。

  • 最坏情况:每次分区都极度不平衡(如有序数组固定选首/尾元素为基准),时间复杂度退化为O(n²)。但通过随机化基准、三数取中法等优化,可大幅降低这种情况的发生概率。

3.2 空间复杂度

快速排序的空间复杂度主要来自递归调用栈:

  • 最好/平均情况:递归深度为log n,空间复杂度为O(log n);

  • 最坏情况:递归深度为n,空间复杂度为O(n)(可通过尾递归优化降至O(log n))。

注意:原地排序版本的快速排序,不需要额外开辟空间存储子数组,仅占用递归栈空间,比归并排序(O(n)空间)更节省内存。

3.3 稳定性

快速排序是不稳定排序------即排序后,相等元素的相对位置可能会发生改变。

举个反例:数组 [3(红), 2, 1, 3(蓝)],选择红3作为基准,分区后会变成 [2, 1, 3(蓝), 3(红)],原本在后面的蓝3,排序后跑到了红3前面,破坏了原始相对位置。

如果你的场景要求"相等元素保持原始顺序"(如排序带有相同分数的学生信息),则不适合用快速排序,可选择稳定的排序算法(如归并排序、插入排序)。

四、快速排序的进阶优化技巧(面试加分项)

除了随机化基准,还有几个实用的优化技巧,能进一步提升快速排序的效率,尤其适合应对大规模数据或面试中的深度提问。

4.1 三数取中法(优化基准选择)

随机化基准虽然能降低最坏情况概率,但仍有不确定性。三数取中法是更稳定的基准选择方式:取数组首、尾、中间三个元素的中位数作为基准,确保基准尽可能接近数组的中间值,减少分区不平衡的可能。

cs 复制代码
#include <stdio.h>

// 返回三个数的中位数
int median(int a, int b, int c) {
    if ((a - b) * (c - a) >= 0) {
        return a;
    } else if ((b - a) * (c - b) >= 0) {
        return b;
    } else {
        return c;
    }
}

// 三数取中优化的分区函数
int partition_optimized(int arr[], int low, int high) {
    // 三数取中选择基准(首、尾、中间元素)
    int mid = low + (high - low) / 2; // 避免溢出
    int pivot = median(arr[low], arr[mid], arr[high]);
    
    // 找到基准的索引,并交换到数组末尾
    int pivot_index;
    for (pivot_index = low; pivot_index <= high; pivot_index++) {
        if (arr[pivot_index] == pivot) {
            break;
        }
    }
    int temp = arr[pivot_index];
    arr[pivot_index] = arr[high];
    arr[high] = temp;
    
    // 后续分区逻辑和之前一致
    pivot = arr[high];
    int i = low - 1;
    for (int j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            i++;
            temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;
    
    return i + 1;
}

// 可直接调用该分区函数替换之前的partition,实现三数取中优化

4.2 小数组切换插入排序

当递归到小数组(通常认为长度<15)时,快速排序的递归开销会超过插入排序的效率------插入排序在小数组上的实际运行速度更快。因此,可在递归过程中判断子数组长度,小于阈值时切换为插入排序。

4.3 三路分区(处理大量重复元素)

如果数组中存在大量重复元素,标准快速排序会将重复元素分到同一侧,导致分区不平衡。三路分区将数组分为三部分:小于基准、等于基准、大于基准,等于基准的元素无需再递归排序,可将时间复杂度降至O(n)(全等元素时)。

五、快速排序的应用场景与面试考点

5.1 应用场景

快速排序的核心优势是"平均效率高、原地排序、缓存友好",因此适合以下场景:

  • 处理大规模随机分布的数据(如海量日志排序、用户ID排序);

  • 内存有限的场景(原地排序,节省内存);

  • 大多数编程语言的标准库排序(如C++的qsort、Java的Arrays.sort),底层都基于快速排序优化实现。

注意:不适合有序/逆序数据(未优化版本)、要求稳定排序的场景。

5.2 面试高频考点

快速排序是算法面试的"常客",常见考点包括:

  • 手写快速排序(原地分区版本,必掌握);

  • 快速排序的时间/空间复杂度分析,以及最坏情况的触发条件;

  • 快速排序的优化技巧(随机化、三数取中、三路分区);

  • 基于快速排序的延伸算法------快速选择(用于求解Top K问题,时间复杂度O(n))。

六、总结:快速排序的核心要点

看到这里,相信你已经彻底搞懂了快速排序的来龙去脉,最后用几句话总结核心要点,帮你快速记忆:

  1. 核心思想:分治思想,选基准→分区→递归排序;

  2. 核心优势:平均O(n log n)时间复杂度、原地排序、实际运行速度快;

  3. 关键实现:原地分区(双指针)、递归终止条件(子数组长度≤1);

  4. 优化方向:随机化基准、三数取中、小数组切换插入排序、三路分区;

  5. 局限:不稳定排序、最坏情况时间复杂度O(n²)(可优化规避)。

快速排序的魅力在于,它既有简洁的核心逻辑,又有丰富的优化空间,既能满足新手入门理解,也能应对进阶的面试和开发需求。建议大家动手敲一遍代码,亲自调试排序过程,感受"分而治之"的算法思想------只有实践,才能真正掌握。

最后,留一个小练习:用快速排序实现Top K问题(找出数组中第K大的元素),欢迎在评论区留下你的代码~

相关推荐
weixin_649555672 小时前
C语言程序设计第四版(何钦铭、颜晖)第十章函数与程序结构之统计完全平方数
c语言·数据结构·算法
沈阳信息学奥赛培训2 小时前
深搜算法 6300:Grid Path Construction(2418)
算法
2401_891482172 小时前
C++中的状态模式
开发语言·c++·算法
Magic--2 小时前
选择排序:原理、实现与优化
数据结构·算法·排序算法
qq_417695052 小时前
基于C++的区块链实现
开发语言·c++·算法
We་ct2 小时前
LeetCode 74. 搜索二维矩阵:两种高效解题思路
前端·算法·leetcode·矩阵·typescript·二分查找
2401_894241922 小时前
基于C++的反射机制探索
开发语言·c++·算法
cui_ruicheng2 小时前
C++ 数据结构进阶:unordered_map 与 unordered_set源码分析与实现
数据结构·c++·算法·哈希算法
C蔡博士2 小时前
最小生成树(MST)详解:定义、算法与核心性质
算法·贪心算法·图论·时间复杂度