谈谈快速排序算法,其时间复杂度和空间复杂度是多少?
一、快速排序算法原理
快速排序(QuickSort)是一种基于分治策略的高效排序算法,核心思想是通过选取基准值(pivot)将数组拆分为两个子数组,递归排序后合并。具体步骤如下:
- 基准选择:从数组中选一个元素作为基准(如中间元素、首尾元素或随机元素)。
- 分区操作:重新排列数组,所有比基准小的元素放在左侧,大的放在右侧(等于可放任意一侧)。
- 递归排序:对左右子数组递归执行上述操作,直到子数组长度为1或0。
// 基础快速排序实现(以中间元素为基准)
function quickSort(arr) {
if (arr.length <= 1) return arr; // 终止条件:空数组或单元素
const pivotIndex = Math.floor(arr.length / 2); // 选中间索引为基准
const pivot = arr.splice(pivotIndex, 1)[0]; // 取出基准值(原数组被修改)
const left = [];
const right = [];
// 分区操作:比基准小的放左,大的放右
for (const num of arr) {
num < pivot ? left.push(num) : right.push(num);
}
// 递归排序并合并
return [...quickSort(left), pivot, ...quickSort(right)];
}
二、时间复杂度与空间复杂度
- 时间复杂度:
- 平均情况:
O(n log n)
。每次分区操作将数组分成两半,递归深度为log n
,每层遍历n
次。 - 最坏情况:
O(n²)
。当数组已排序且每次选第一个元素为基准,导致每次分区仅减少一个元素。
- 平均情况:
- 空间复杂度:
- 平均情况:
O(log n)
。递归调用栈的深度。 - 最坏情况:
O(n)
。递归深度退化为线性。
- 平均情况:
三、优化方向与使用建议
1. 基准选择优化
避免最坏情况的关键是合理选择基准:
- 三数取中法:取头、中、尾三个元素的中位数作为基准。
- 随机基准:随机选择索引降低有序数组的影响。
// 优化:三数取中法选择基准
function getPivotIndex(arr, left, right) {
const mid = Math.floor((left + right) / 2);
// 比较三个值,返回中间值的索引
if (arr[left] > arr[mid]) [arr[left], arr[mid]] = [arr[mid], arr[left]];
if (arr[left] > arr[right]) [arr[left], arr[right]] = [arr[right], arr[left]];
if (arr[mid] > arr[right]) [arr[mid], arr[right]] = [arr[right], arr[mid]];
return mid; // 中间值作为基准
}
2. 小数组切换插入排序
当子数组较小时(如长度 < 10),插入排序更高效:
function quickSortOptimized(arr, left = 0, right = arr.length - 1) {
if (right - left < 10) {
// 插入排序优化小数组
for (let i = left + 1; i <= right; i++) {
const key = arr[i];
let j = i - 1;
while (j >= left && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
return;
}
const pivotIndex = partition(arr, left, right);
quickSortOptimized(arr, left, pivotIndex - 1);
quickSortOptimized(arr, pivotIndex + 1, right);
}
3. 避免递归栈溢出
- 尾递归优化:确保递归调用是最后一步操作(部分引擎支持)。
- 迭代实现:用栈模拟递归过程。
// 迭代实现快速排序(避免栈溢出)
function quickSortIterative(arr) {
const stack = [{ left: 0, right: arr.length - 1 }];
while (stack.length) {
const { left, right } = stack.pop();
if (left >= right) continue;
const pivotIndex = partition(arr, left, right);
stack.push({ left, right: pivotIndex - 1 });
stack.push({ left: pivotIndex + 1, right });
}
return arr;
}
四、实际开发注意事项
1. 处理大量重复元素
当数组中重复元素较多时,基础分区可能导致性能下降。使用三向切分优化:
function quickSort3Way(arr, left = 0, right = arr.length - 1) {
if (left >= right) return;
let lt = left; // 小于基准的右边界
let gt = right; // 大于基准的左边界
const pivot = arr[left];
let i = left;
while (i <= gt) {
if (arr[i] < pivot) {
[arr[i], arr[lt]] = [arr[lt], arr[i]];
lt++;
i++;
} else if (arr[i] > pivot) {
[arr[i], arr[gt]] = [arr[gt], arr[i]];
gt--;
} else {
i++;
}
}
quickSort3Way(arr, left, lt - 1);
quickSort3Way(arr, gt + 1, right);
}
2. 稳定性问题
快速排序是不稳定排序(相同元素可能交换位置)。若业务需要稳定性(如多字段排序),应选择归并排序。
3. 性能权衡
- 大数据量:优先选择快速排序(平均性能最优)。
- 内存敏感场景:堆排序(
O(1)
空间复杂度)更合适。
五、总结与建议
- 适用场景:快速排序适合内存充足、数据量大且对稳定性无要求的场景(如前端处理本地大规模数据排序)。
- 避免踩坑:
- 避免直接对已排序数组使用固定基准(如首元素)。
- 处理海量数据时,优先测试递归深度限制,必要时改用迭代版本。
- 工程实践:大多数语言内置排序(如JS的
Array.sort()
)已做优化(混合使用快排、插入排序等),无特殊需求建议直接使用内置方法。