快速排序(Quick Sort)是一种基于分治思想的高效排序算法,由计算机科学家Tony Hoare于1959年提出。它通过递归地将数组分成较小和较大的两个子数组来工作,平均时间复杂度为O(n log n),在实际应用中通常比其他O(n log n)复杂度的排序算法更快。
快速排序的核心是"分而治之"策略,具体包含三个关键步骤:
这种分治策略使得快速排序特别适合处理大规模数据集,因为每次分区都能显著减少需要处理的数据量。
快速排序的性能表现有几个显著特点:
在实际应用中,快速排序通常比归并排序和堆排序更快,因为它的内部循环可以在大多数架构上高效实现。这也是为什么它被广泛用于各种编程语言的标准库中。
分区(partition)是快速排序中最关键的操作。以下是一个典型的分区过程:
这种分区方法被称为Lomuto分区方案,实现简单但效率不如Hoare原始方案高。在实际应用中,Hoare的方案通常更快,因为它减少了交换次数。
快速排序的递归过程遵循以下逻辑:
递归的深度取决于分区后子数组的平衡程度。理想情况下,每次分区都能将数组分成大小相近的两部分,这样递归深度就是O(log n)。
基准值的选择对算法性能有重大影响。常见的策略包括:
随机选择和三数取中法可以有效避免最坏情况的发生,特别是对于已经部分排序的数据。
以下是快速排序在JavaScript中的基础实现:
javascript复制function quickSort(arr) {
if (arr.length <= 1) return arr;
const pivot = arr[arr.length - 1];
const left = [];
const right = [];
for (let i = 0; i < arr.length - 1; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return [...quickSort(left), pivot, ...quickSort(right)];
}
这种实现方式简单易懂,但效率不高,因为它创建了多个新数组,增加了内存使用。
更高效的实现是原地排序版本,减少了内存使用:
javascript复制function quickSort(arr, left = 0, right = arr.length - 1) {
if (left < right) {
const pivotIndex = partition(arr, left, right);
quickSort(arr, left, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, right);
}
return arr;
}
function partition(arr, left, right) {
const pivot = arr[right];
let i = left;
for (let j = left; j < right; j++) {
if (arr[j] < pivot) {
[arr[i], arr[j]] = [arr[j], arr[i]];
i++;
}
}
[arr[i], arr[right]] = [arr[right], arr[i]];
return i;
}
这个版本直接在原数组上进行操作,通过交换元素来实现分区,大大减少了内存使用。
为了避免递归深度过大导致栈溢出,可以使用尾递归优化:
javascript复制function quickSort(arr, left = 0, right = arr.length - 1) {
while (left < right) {
const pivotIndex = partition(arr, left, right);
if (pivotIndex - left < right - pivotIndex) {
quickSort(arr, left, pivotIndex - 1);
left = pivotIndex + 1;
} else {
quickSort(arr, pivotIndex + 1, right);
right = pivotIndex - 1;
}
}
return arr;
}
这种优化确保递归调用发生在较小的子数组上,将最大递归深度限制在O(log n)。
对于包含大量重复元素的数组,传统快速排序效率不高。三路快速排序将数组分为三部分:
javascript复制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 + 1;
while (i <= gt) {
if (arr[i] < pivot) {
[arr[lt], arr[i]] = [arr[i], arr[lt]];
lt++;
i++;
} else if (arr[i] > pivot) {
[arr[gt], arr[i]] = [arr[i], arr[gt]];
gt--;
} else {
i++;
}
}
quickSort3Way(arr, left, lt - 1);
quickSort3Way(arr, gt + 1, right);
}
这种变体在处理重复元素时效率更高,因为它将相等的元素集中在一起,避免了对它们的重复处理。
对于小数组,插入排序通常比快速排序更高效。可以设置一个阈值,当子数组大小小于阈值时切换到插入排序:
javascript复制function hybridQuickSort(arr, left = 0, right = arr.length - 1, threshold = 10) {
if (right - left + 1 <= threshold) {
insertionSort(arr, left, right);
return;
}
const pivotIndex = partition(arr, left, right);
hybridQuickSort(arr, left, pivotIndex - 1, threshold);
hybridQuickSort(arr, pivotIndex + 1, right, threshold);
}
function insertionSort(arr, left, right) {
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;
}
}
这种混合策略在实践中通常能获得更好的性能。
在现代多核CPU上,可以利用Web Workers实现并行快速排序:
javascript复制async function parallelQuickSort(arr) {
if (arr.length <= 1) return arr;
const pivot = arr[0];
const left = [];
const right = [];
for (let i = 1; i < arr.length; i++) {
if (arr[i] < pivot) left.push(arr[i]);
else right.push(arr[i]);
}
const [sortedLeft, sortedRight] = await Promise.all([
sortChunk(left),
sortChunk(right)
]);
return [...sortedLeft, pivot, ...sortedRight];
}
function sortChunk(chunk) {
return new Promise(resolve => {
const worker = new Worker('quicksort-worker.js');
worker.postMessage(chunk);
worker.onmessage = e => resolve(e.data);
});
}
注意这需要单独的worker脚本文件来实现排序逻辑。并行排序对于非常大的数组可以显著提高性能。
快速排序特别适合以下场景:
相比之下,以下场景可能不适合使用快速排序:
| 算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|---|
| 快速排序 | 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) | 稳定 | 小数据量或基本有序数据 |
快速排序在大多数实际应用中表现优异,特别是它的原地排序版本既节省内存又快速。
现代JavaScript引擎(如V8)使用的排序算法实际上是混合策略:
这种混合策略结合了不同算法的优点,在实际应用中提供了最佳性能。这也是为什么JavaScript的Array.prototype.sort()方法在大多数情况下表现良好的原因。
当数组非常大或分区极度不平衡时,递归深度可能导致栈溢出。解决方案包括:
迭代版本示例:
javascript复制function iterativeQuickSort(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;
}
当数组包含大量重复元素时,传统快速排序效率下降。解决方案包括:
在实际项目中,选择排序算法时应考虑:
大多数情况下,JavaScript内置的Array.prototype.sort()已经足够好,只有在有特殊需求或性能瓶颈时才需要实现自定义排序算法。
要准确比较不同排序实现的性能,可以使用performance API:
javascript复制function testSortPerformance(sortFn, arraySize = 10000) {
const arr = Array.from({ length: arraySize }, () => Math.random());
const start = performance.now();
sortFn([...arr]); // 使用副本避免缓存影响
const end = performance.now();
return end - start;
}
// 测试不同实现
console.log('基础快速排序:', testSortPerformance(quickSortBasic));
console.log('原地快速排序:', testSortPerformance(quickSortInPlace));
console.log('内置排序:', testSortPerformance(arr => arr.sort((a, b) => a - b)));
基于实际经验的一些优化建议:
对于内存敏感的环境:
快速选择是一种基于快速排序分区思想的选择算法,用于在未排序数组中找到第k小/大的元素:
javascript复制function quickSelect(arr, k, left = 0, right = arr.length - 1) {
if (left === right) return arr[left];
const pivotIndex = partition(arr, left, right);
if (k === pivotIndex) {
return arr[k];
} else if (k < pivotIndex) {
return quickSelect(arr, k, left, pivotIndex - 1);
} else {
return quickSelect(arr, k, pivotIndex + 1, right);
}
}
快速选择的平均时间复杂度为O(n),比先排序再选择更高效。
这是一种改进的快速排序变体,使用多个pivot进行分区:
javascript复制function dualPivotQuickSort(arr, left = 0, right = arr.length - 1) {
if (left >= right) return;
if (arr[left] > arr[right]) {
[arr[left], arr[right]] = [arr[right], arr[left]];
}
let lt = left + 1;
let gt = right - 1;
let i = left + 1;
while (i <= gt) {
if (arr[i] < arr[left]) {
[arr[i], arr[lt]] = [arr[lt], arr[i]];
lt++;
i++;
} else if (arr[i] > arr[right]) {
[arr[i], arr[gt]] = [arr[gt], arr[i]];
gt--;
} else {
i++;
}
}
[arr[left], arr[lt - 1]] = [arr[lt - 1], arr[left]];
[arr[right], arr[gt + 1]] = [arr[gt + 1], arr[right]];
dualPivotQuickSort(arr, left, lt - 2);
dualPivotQuickSort(arr, lt, gt);
dualPivotQuickSort(arr, gt + 2, right);
}
这种变体在某些情况下比传统快速排序更快,特别是在现代CPU架构上。
在函数式编程风格中,可以这样实现快速排序:
javascript复制const quickSortFP = arr =>
arr.length <= 1 ? arr : [
...quickSortFP(arr.slice(1).filter(x => x < arr[0])),
arr[0],
...quickSortFP(arr.slice(1).filter(x => x >= arr[0]))
];
这种实现简洁但效率不高,因为它创建了多个临时数组。在实际应用中,函数式实现可能需要使用更高效的数据结构。