排序---快速排序(Quick Sort)
一、算法核心概念
快速排序是一种分治法(Divide and Conquer) 思想的排序算法,由计算机科学家Tony Hoare于1960年提出。其核心思想是:通过选择一个“基准元素”(Pivot),将数组划分为两个子数组,其中左子数组的元素均小于等于基准,右子数组的元素均大于等于基准;然后递归地对两个子数组执行相同操作,最终使整个数组有序。
快速排序的高效性源于分区操作(Partition)——每次分区能将一个元素(基准)放到最终位置,同时将问题规模缩小一半(理想情况),从而实现接近O(nlogn)的时间复杂度。
二、基本工作原理
快速排序的工作流程可概括为三步:选择基准→分区→递归排序,具体如下:
-
选择基准(Pivot Selection)
从数组中选择一个元素作为基准(如第一个、最后一个、中间元素或随机元素)。基准的选择直接影响算法效率(后续优化部分详细说明)。 -
分区(Partition)
重新排列数组,使所有小于基准的元素移到基准左侧,所有大于基准的元素移到基准右侧(等于基准的元素可放任意侧)。分区后,基准元素的位置即为其在最终有序数组中的位置。 -
递归排序
对基准左侧的子数组和右侧的子数组分别重复上述步骤(选择基准→分区),直至子数组长度为0或1(天然有序)。
三、分区策略(核心步骤详解)
分区是快速排序的核心操作,常用的分区方法有双边循环法和单边循环法,以下以双边循环法为例详细说明:
双边循环法步骤(升序排序):
- 初始状态:设数组为
arr
,左边界left
,右边界right
,选择arr[left]
为基准pivot
。 - 左指针
i
从left
开始向右移动,寻找第一个大于pivot
的元素; - 右指针
j
从right
开始向左移动,寻找第一个小于pivot
的元素; - 若
i < j
,交换arr[i]
和arr[j]
,重复上述步骤; - 当
i >= j
时,交换arr[left]
(基准)和arr[j]
,此时j
为基准的最终位置,分区完成。
四、实例演示(完整排序过程)
以数组[4, 7, 6, 5, 3, 2, 8, 1]
为例,演示快速排序过程:
-
第一次分区:
- 选择基准
pivot = 4
(第一个元素),left=0
,right=7
; - 左指针
i
右移,找到第一个大于4的元素7
(索引1); - 右指针
j
左移,找到第一个小于4的元素1
(索引7); - 交换
7
和1
→数组变为[4, 1, 6, 5, 3, 2, 8, 7]
; - 继续移动指针:
i
找到6
(索引2),j
找到2
(索引5),交换→[4, 1, 2, 5, 3, 6, 8, 7]
; - 继续移动:
i
找到5
(索引3),j
找到3
(索引4),交换→[4, 1, 2, 3, 5, 6, 8, 7]
; - 此时
i=4
,j=4
(i >= j
),交换基准4
和arr[j]
→[3, 1, 2, 4, 5, 6, 8, 7]
; - 分区完成,基准
4
的位置为索引3,左子数组[3,1,2]
,右子数组[5,6,8,7]
。
- 选择基准
-
递归处理左子数组
[3,1,2]
:- 选择基准
3
,分区后得到[2,1,3]
,基准3
在索引2,左子数组[2,1]
。 - 递归处理
[2,1]
,分区后得到[1,2]
。
- 选择基准
-
递归处理右子数组
[5,6,8,7]
:- 选择基准
5
,分区后5
在索引0,右子数组[6,8,7]
。 - 处理
[6,8,7]
,选择基准6
,分区后6
在索引0,右子数组[8,7]
。 - 处理
[8,7]
,分区后得到[7,8]
。
- 选择基准
-
最终有序数组:
[1, 2, 3, 4, 5, 6, 7, 8]
。
五、时间复杂度与空间复杂度
-
时间复杂度:
- 理想情况(基准每次将数组分为等长两部分):每轮分区处理n个元素,递归深度为logn,总操作次数为O(nlogn);
- 最坏情况(基准为数组的最值,如已排序数组选第一个元素):每次分区仅减少1个元素,递归深度为n,总操作次数为O(n²);
- 平均情况:通过合理选择基准(如随机选择),可避免最坏情况,平均时间复杂度为O(nlogn)。
-
空间复杂度:
快速排序为原地排序(仅需常数空间存储临时变量),但递归调用会产生栈空间开销:- 理想情况:递归深度为logn,空间复杂度O(logn);
- 最坏情况:递归深度为n,空间复杂度O(n)。
六、稳定性分析
快速排序是不稳定的排序算法。
- 原因:分区过程中,相等元素的相对顺序可能被破坏。例如,数组
[2, 2, 1]
中,若选择第一个2
为基准,分区时会将1
与第一个2
交换,导致两个2
的相对顺序颠倒(变为[1, 2, 2]
,虽然结果有序,但原数组中第一个2
在第二个2
之前,排序后位置交换)。
七、优化策略(避免最坏情况)
快速排序的性能极大依赖基准选择,以下是常见优化手段:
-
随机选择基准
每次从当前区间随机选择一个元素作为基准,避免在有序数组中总是选择最值作为基准(概率上降低最坏情况发生的可能性)。 -
三数取中法
从区间的首、尾、中间三个位置选择中间值作为基准(如median(arr[left], arr[mid], arr[right])
),适用于已知数据可能接近有序的场景。 -
小规模子数组改用插入排序
当子数组长度小于阈值(如10)时,改用插入排序(插入排序在小规模数据上效率高于快速排序,减少递归开销)。 -
尾递归优化
对较大的子数组采用递归,较小的子数组采用循环,减少递归栈深度(避免栈溢出)。
八、C++实现代码(含优化)
以下实现采用随机选择基准和双边循环分区,并添加基本边界处理:
#include <iostream>
#include <vector>
#include <cstdlib> // 用于rand()和srand()
#include <ctime> // 用于time()初始化随机数种子
using namespace std;// 交换两个元素
void swap(int& a, int& b) {int temp = a;a = b;b = temp;
}// 随机选择基准并分区(双边循环法)
int partition(vector<int>& arr, int left, int right) {// 随机选择基准索引(避免最坏情况)int pivotIndex = left + rand() % (right - left + 1);// 将基准交换到区间左侧(方便分区逻辑)swap(arr[left], arr[pivotIndex]);int pivot = arr[left]; // 基准值int i = left; // 左指针(从left开始)int j = right; // 右指针(从right开始)while (i < j) {// 右指针左移:找到第一个小于pivot的元素while (i < j && arr[j] >= pivot) {j--;}// 左指针右移:找到第一个大于pivot的元素while (i < j && arr[i] <= pivot) {i++;}// 交换左右指针指向的元素if (i < j) {swap(arr[i], arr[j]);}}// 将基准放到最终位置(i=j处)swap(arr[left], arr[i]);return i; // 返回基准索引
}// 快速排序主函数
void quickSort(vector<int>& arr, int left, int right) {// 递归终止条件:区间长度 <= 1if (left >= right) {return;}// 分区:得到基准的最终位置int pivotPos = partition(arr, left, right);// 递归排序左子数组quickSort(arr, left, pivotPos - 1);// 递归排序右子数组quickSort(arr, pivotPos + 1, right);
}int main() {// 初始化随机数种子(确保每次运行随机基准不同)srand(time(nullptr));vector<int> arr = {4, 7, 6, 5, 3, 2, 8, 1};cout << "排序前:";for (int num : arr) {cout << num << " ";}quickSort(arr, 0, arr.size() - 1);cout << "\n排序后:";for (int num : arr) {cout << num << " ";}// 输出:排序前:4 7 6 5 3 2 8 1 // 排序后:1 2 3 4 5 6 7 8 return 0;
}
九、适用场景与优缺点
适用场景:
- 大规模数据排序:平均时间复杂度O(nlogn),实际性能优于归并排序和堆排序(缓存友好,局部性好);
- 内存受限场景:原地排序(除递归栈外无需额外空间),空间效率高;
- 通用排序需求:C++标准库的
std::sort
、Java的Arrays.sort
(针对基本类型)等均采用快速排序的变种(如 introsort)。
优点:
- 平均性能优异,实际应用中排序速度快;
- 原地排序,空间复杂度低(O(logn));
- 对缓存友好(局部访问数据,符合CPU缓存机制)。
缺点:
- 不稳定,不适合要求保持相等元素相对顺序的场景;
- 最坏情况时间复杂度为O(n²)(需通过基准选择优化避免);
- 递归实现可能导致栈溢出(可通过尾递归优化解决)。
快速排序是分治法的经典应用,通过“选择基准→分区→递归”三步实现高效排序。其核心优势在于平均O(nlogn)的时间复杂度和原地排序特性,使其成为实际应用中最常用的排序算法之一。尽管存在不稳定性和最坏情况风险,但通过随机基准、三数取中等优化策略,可有效规避缺陷。理解快速排序的分区逻辑和优化思想,不仅能掌握一种高效排序方法,更能深化对分治法的理解,为解决复杂算法问题提供思路。