加载笔记内容...
加载笔记内容...
快速排序(Quicksort) 作为20世纪最重要的十大算法之一,其核心思想是分治法(Divide and Conquer)与原地分区(In-place Partitioning)的完美结合。让我们从数学角度重新审视这个经典算法:
时间复杂度推导:
数学证明显示,随机化版本的快速排序 在概率意义下能达到 O(N log N) 的时间复杂度,这解释了为什么它在工程实践中如此受欢迎。
常见的两种分区策略对比:
策略 | Lomuto分区 | Hoare分区 |
---|---|---|
实现复杂度 | 简单 | 较复杂 |
交换次数 | 较多 | 较少 |
主元处理 | 显式固定位置 | 动态调整 |
适用场景 | 教学/简单实现 | 高性能实现 |
文中TypeScript实现采用的是Lomuto分区方案,其典型特征是:
1function partition(arr: number[], low: number, high: number) {
2 const pivot = arr[high]; // 固定选择末尾元素
3 let i = low - 1;
4 for (let j = low; j < high; j++) {
5 if (arr[j] <= pivot) {
6 swap(arr, ++i, j);
7 }
8 }
9 swap(arr, i+1, high);
10 return i+1;
11}
工程优化建议:
递归版本可能面临栈溢出风险,迭代版本通过显式栈模拟递归过程:
1const stack = new Array(h - l + 1);
2while (top >= 0) {
3 // 弹出区间边界
4 h = stack[top--];
5 l = stack[top--];
6
7 const p = partition(arr, l, h);
8
9 // 压入子区间
10 if (p-1 > l) { /* 左区间入栈 */ }
11 if (p+1 < h) { /* 右区间入栈 */ }
12}
空间复杂度分析:
1# 伪代码示例
2def parallel_quicksort(arr):
3 if len(arr) < 1000:
4 return sequential_quicksort(arr)
5 pivot = select_pivot(arr)
6 left, right = partition(arr, pivot)
7 spawn parallel_quicksort(left)
8 parallel_quicksort(right)
9 sync
常见问题1:已排序数组性能退化
常见问题2:重复元素处理低效
1// 三向切分伪代码
2void quicksort3Way(int[] arr, int lo, int hi) {
3 if (hi <= lo) return;
4 int lt = lo, gt = hi;
5 int pivot = arr[lo];
6 int i = lo;
7 while (i <= gt) {
8 if (arr[i] < pivot) swap(arr, lt++, i++);
9 else if (arr[i] > pivot) swap(arr, i, gt--);
10 else i++;
11 }
12 quicksort3Way(arr, lo, lt-1);
13 quicksort3Way(arr, gt+1, hi);
14}
争议点:关于是否应该完全避免最坏情况
从V8引擎的数组排序实现可以看到现代算法的演进:
1const MAX_DEPTH = 2 * Math.log2(arr.length);
2while (top >= 0 && depth++ < MAX_DEPTH) {
3 // 迭代逻辑
4}
1function genericQuickSort<T>(arr: T[], compare: (a: T, b: T) => number): T[] {
2 // 实现细节
3}
快速排序的优雅之处在于,它用简单的思想解决了复杂的问题。正如算法发明者Hoare所说:"The key to good algorithm design is to make the computer do the work, not the programmer." 理解其本质,才能在工程实践中做出最合适的选择。