快速排序(Quick Sort)作为 C++ 中最常用的高效排序算法之一,凭借其平均 O (n log n) 的时间复杂度和原地排序的特性,在实际开发和算法竞赛中被广泛应用。本文将从基本原理出发,逐步深入快速排序的实现细节、优化策略,并结合 C++ 特性探讨其在实际场景中的最佳实践。
快速排序的核心思想是 “分而治之”,通过以下三个步骤递归实现排序:
形象理解:如同整理书籍时,先选一本作为参考,把比它薄的放左边,比它厚的放右边,再分别整理左右两堆书,最终实现整体有序。
分区是快速排序的核心操作,直接影响算法效率。以下是最常用的 Lomuto 分区法 (单边扫描):
#include
using namespace std;
// 分区函数:返回基准值最终位置的索引
int partition(vector& arr, int low, int high) {
int pivot = arr[high]; // 选最右侧元素作为基准值
int i = low - 1; // i指向小于基准值区域的最后一个元素
// 遍历数组,将小于基准值的元素移到左侧
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++; // 扩大小于基准值的区域
swap(arr[i], arr[j]); // 将当前元素加入该区域
}
}
// 将基准值放到最终位置(小于区域和大于区域的中间)
swap(arr[i + 1], arr[high]);
return i + 1; // 返回基准值的索引
}
基于分区函数,实现快速排序的递归逻辑:
void quickSort(vector& arr, int low, int high) {
if (low < high) {
// 分区并获取基准值位置
int pi = partition(arr, low, high);
// 递归排序基准值左侧和右侧的子数组
quickSort(arr, low, pi - 1); // 左侧子数组:[low, pi-1]
quickSort(arr, pi + 1, high); // 右侧子数组:[pi+1, high]
}
}
示例执行流程:
对数组 [3, 6, 8, 10, 1, 2, 1] 排序时,首次分区以最后一个元素 1 为基准,经过交换后数组变为 [1, 6, 8, 10, 3, 2, 1],基准值位置为 0,随后递归处理右侧子数组。
快速排序在最坏情况(如数组已排序或所有元素相同)下时间复杂度会退化为 O (n²),通过以下优化可显著提升稳定性:
当数组有序时,选择两端元素作为基准会导致分区失衡(一侧子数组为空)。三数取中法通过比较首、中、尾三个元素,选择中间值作为基准:
// 三数取中:选择首、中、尾三个元素的中间值作为基准
int medianOfThree(vector& arr, int low, int high) {
int mid = low + (high - low) / 2; // 避免溢出的中间索引计算
// 排序三个位置的元素,使中间值移到high-1位置
if (arr[low] > arr[mid]) swap(arr[low], arr[mid]);
if (arr[low] > arr[high]) swap(arr[low], arr[high]);
if (arr[mid] > arr[high]) swap(arr[mid], arr[high]);
swap(arr[mid], arr[high - 1]); // 将基准值藏在high-1位置
return arr[high - 1]; // 返回基准值
}
当数组中存在大量重复元素时,传统快排会将元素分为 “小于” 和 “大于” 两部分,导致重复元素集中在一侧。三路快排将元素分为 “小于”“等于”“大于” 三部分,减少递归范围:
void quickSort3Way(vector& arr, int low, int high) {
if (low >= high) return;
int pivot = arr[low]; // 基准值
int lt = low; // [low, lt) 存放小于基准值的元素
int gt = high; // (gt, high] 存放大于基准值的元素
int i = low + 1; // 当前遍历的元素索引
while (i <= gt) {
if (arr[i] < pivot) {
swap(arr[lt++], arr[i++]);
} else if (arr[i] > pivot) {
swap(arr[i], arr[gt--]); // 交换后i不变,需重新检查新元素
} else {
i++; // 等于基准值的元素不交换,直接跳过
}
}
// 递归处理小于和大于基准值的子数组
quickSort3Way(arr, low, lt - 1);
quickSort3Way(arr, gt + 1, high);
}
递归到小规模子数组(如长度≤10)时,插入排序的实际效率高于快排(减少递归调用开销):
// 插入排序:用于小规模子数组
void insertionSort(vector& arr, int low, int high) {
for (int i = low + 1; i <= high; i++) {
int key = arr[i];
int j = i - 1;
// 将大于key的元素后移
while (j >= low && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
// 优化后的快排:小规模数组使用插入排序
void optimizedQuickSort(vector& arr, int low, int high) {
const int THRESHOLD = 10; // 阈值,可根据实际情况调整
if (high - low + 1 <= THRESHOLD) {
insertionSort(arr, low, high); // 小规模数组用插入排序
return;
}
// 大规模数组用快排(结合三数取中)
int pivot = medianOfThree(arr, low, high);
// ... 分区和递归逻辑(省略,参考前文)
}
C++ 标准库的 std::sort 并非纯快排,而是混合排序算法(通常为 introsort,即内省排序):
使用示例:
#include // 包含std::sort
#include
int main() {
vector arr = {3, 1, 4, 1, 5, 9, 2, 6};
// 对整个数组排序(默认升序)
sort(arr.begin(), arr.end());
// 对部分区间排序(从索引1到5,左闭右开)
sort(arr.begin() + 1, arr.begin() + 6);
return 0;
}
优势:std::sort 经过编译器优化和工程实践验证,在绝大多数场景下性能优于手写快排,实际开发中应优先使用。
快速排序是 C++ 算法中的基础且重要的知识点,掌握其原理不仅能应对面试和算法竞赛,更能理解 “分治” 思想在复杂问题中的应用。实际开发中:
通过本文的实现与优化分析,希望能帮助你从 “会用” 快排进阶到 “理解并优化” 快排,在面对不同场景时做出最佳选择。
参考资料: