基础排序算法介绍(二)快速排序

一 快速排序

快速排序是一种高效且应用广泛的排序算法,它巧妙地运用了“分而治之”的策略。下面这张表格能帮你快速把握它的核心特征,之后我们会深入探讨其工作原理和实现细节。

1.1 快速排序特性总结

特性 说明
核心思想 分治法:通过一趟排序将待排记录分割成独立的两部分,其中一部分的所有元素均比另一部分的元素小,然后分别对这两部分递归地进行排序,以达到整个序列有序
时间复杂度 平均:O(n log n);最坏(如数组已有序):O(n²);最好(每次划分均分):O(n log n)
空间复杂度 平均递归深度 O(log n);最坏情况递归深度 O(n)。主要是递归(或模拟递归)使用的栈空间
稳定性 不稳定。在划分过程中,相等元素的相对位置可能发生变化
主要优势 平均性能非常高效,是通常情况下最快的排序算法之一;原地排序,平均空间复杂度低
主要劣势 最坏情况时间复杂度高;不稳定

1.2 算法工作原理

快速排序的运作过程可以清晰地分为“分区”和“递归”两大阶段。下图以数组 [6, 2, 7, 3, 8, 9]为例,展示了其核心的分区操作以及后续的递归过程:

图片

1.2.1 选择基准值​​

从待排序序列中选取一个元素作为“基准”(pivot)。选择策略多样,如第一个元素、最后一个元素、中间元素或随机元素等。上图示例中选择了第一个元素 6作为基准。
​​

1.2.2 分区操作​​

这是快速排序的核心步骤,目标是重新排列数组,使得所有比基准值小的元素都放在基准前面,所有比基准值大的元素都放在基准后面。操作完成后,基准值就处于其最终的正确位置上。具体过程通常使用双指针(如上图中的 i和 j)从数组两端向中间扫描,交换不符合条件的元素,直到指针相遇。

1.2.3 递归排序

经过一趟分区操作后,基准值已经就位,数组被分成两个子数组:左子数组(所有元素小于基准值)和右子数组(所有元素大于基准值)。然后,递归地对左子数组和右子数组重复上述过程,直到子数组的长度为1或0(自然有序),此时整个数组便有序了。

1.3 复杂度分析

1.3.1 时间复杂度​​:

  • 最好情况 O(n log n)​​:每次划分都能将数组均匀分成两部分。
  • ​​最坏情况 O(n²)​​:每次划分产生的两个子数组大小极度不平衡,例如一个包含 n-1 个元素,另一个为空。这在数组已有序且始终选择第一个或最后一个元素作为基准时会发生。
  • 平均情况 O(n log n)​​:对于随机排列的数组,快速排序的平均性能很好。
  • 通过随机选择基准或“三数取中”等优化方法,可以大幅降低最坏情况出现的概率

1.3.2 空间复杂度​​:主要是递归调用栈的空间。

  • ​最好情况 O(log n)​​:对应递归树的深度。
  • 最坏情况 O(n)​​:对应退化的递归树(如数组已有序)。

1.3.3 稳定性​​:

快速排序是​​不稳定​​的。在分区过程中,由于涉及远距离的元素交换,可能导致相等元素的相对顺序发生变化.

1.4 使用场景

  • 大规模数据排序​​:当平均时间复杂度为 O(n log n) 时,快速排序通常是处理大量随机数据的最优选择之一,性能优异。
  • ​​对缓存友好​​:由于快速排序是原地排序(主要操作在原始数组上进行),其内存访问模式通常具有良好的局部性,能有效利用CPU缓存。
  • ​​通用排序需求​​:是许多编程语言标准库(如C的qsort,C++的std::sort)的默认或备选实现算法。
  • 注意事项​​:若数据量很小或基本有序,且未做优化,性能可能不如简单排序(如插入排序)。在要求稳定性的场景下需谨慎使用。

1.5 代码实现

以下是快速排序的一个典型C语言实现,采用了“挖坑填数”的分区方法,并选择第一个元素作为基准。

1.5.1 c 语言实现

#include <stdio.h>

// 分区操作函数,返回基准值的最终位置
int Partition(int arr[], int low, int high) {
    int pivot = arr[low]; // 选择第一个元素为基准值
    while (low < high) {
        // 从右向左找第一个小于pivot的元素
        while (low < high && arr[high] >= pivot) {
            --high;
        }
        arr[low] = arr[high]; // 将小于pivot的元素移到左边low的位置
        // 从左向右找第一个大于pivot的元素
        while (low < high && arr[low] <= pivot) {
            ++low;
        }
        arr[high] = arr[low]; // 将大于pivot的元素移到右边high的位置
    }
    arr[low] = pivot; // 基准值归位
    return low;       // 返回基准值的位置
}

// 快速排序递归函数
void QuickSort(int arr[], int low, int high) {
    if (low < high) { // 递归终止条件:子数组长度为1或0
        int pivotPos = Partition(arr, low, high); // 获取基准值位置
        QuickSort(arr, low, pivotPos - 1);  // 递归排序左子数组
        QuickSort(arr, pivotPos + 1, high); // 递归排序右子数组
    }
}

// 打印数组函数
void PrintArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

// 主函数测试
int main() {
    int arr[] = {6, 2, 7, 3, 8, 9};
    int n = sizeof(arr) / sizeof(arr[0]);
    printf("排序前的数组: \n");
    PrintArray(arr, n);
    
    QuickSort(arr, 0, n - 1);
    
    printf("排序后的数组: \n");
    PrintArray(arr, n);
    return 0;
}

1.5.2 ​代码关键点解释​​:

  • Partition函数是核心,它通过双指针 low和 high的移动和赋值完成一轮分区,使得基准值 pivot找到其正确位置,并返回该位置索引。
  • QuickSort函数是递归主体。先调用 Partition分区,然后基于基准值位置递归调用自身排序左右两个子区间。
  • 优化提示:若要避免最坏情况,可在 Partition函数开始前,随机交换第一个元素与 low到 high间的一个随机位置的元素,或使用“三数取中法”选择基准值。

1.6 技术对比

排序算法技术特性对比

算法 时间复杂度 空间复杂度 稳定性 适用场景 优势 劣势
快速排序 平均: O(n log n)
最坏: O(n²)
O(log n) 大规模通用数据 - 平均性能最佳
- 原地排序
- 缓存友好
- 最坏情况差
- 不稳定
归并排序 平均: O(n log n)
最坏: O(n log n)
O(n) 需要稳定性、外部排序 - 性能稳定
- 稳定排序
- 适合链表
- 需要额外空间
- 常数因子较大
堆排序 平均: O(n log n)
最坏: O(n log n)
O(1) 内存受限、实时系统 - 最坏情况有保证
- 原地排序
- 缓存不友好
- 实际运行较慢
冒泡排序 平均: O(n²)
最坏: O(n²)
O(1) 教学、小规模数据 - 实现简单
- 稳定排序
- 效率低下
- 不实用
插入排序 平均: O(n²)
最坏: O(n²)
O(1) 小规模、基本有序数据 - 小数据高效
- 稳定排序
- 自适应
- 大规模数据效率低

1.7 总结

快速排序凭借其优越的平均时间复杂度、原地排序的特性以及对CPU缓存的友好性,成为处理​​大规模随机数据排序任务​​时最常被考虑的算法之一。理解其分治思想和分区过程是关键。通过随机化基准选择或三数取中等策略优化,可以有效规避最坏情况,使其在实践中更加可靠。

posted on 2025-10-30 10:39  weiwei2021  阅读(18)  评论(0)    收藏  举报