在海量数据处理面试中,有一个经典问题: "如何从 10 亿个整数中找到最大的 100 个?"
如果你回答"先排序再取值",面试官可能会摇摇头。因为全量排序的复杂度是 O(N log N),而 快速选择 (Quick Select) 算法可以将平均复杂度降到 O(N) 。
今天我们就来拆解这个比快排还快的"找数神器"。
1. 为什么排序不是最优解?
假设我们要找第 K 大的元素:
- 排序法 :把整个数组排好序,然后取下标
K。这就像为了找出班里最高的同学,让全班所有人按身高站成一排。 - 堆排序法:维护一个大小为 K 的小顶堆。复杂度 O(N log K)。
- 快速选择法 :利用快排的
partition思想,每次只处理我们需要的那一半。我们不需要知道其他元素的精确顺序,只需要知道它们比目标大还是小。
2. 核心逻辑:分治与剪枝
快速选择的核心在于 partition(分区)操作:
-
随机选一个基准值
pivot。 -
把数组分成两部分:左边都比
pivot小,右边都比pivot大。 -
此时
pivot所在的位置就是它在全局排序后的最终位置。 -
关键一步:
- 如果
pivot的位置正好是K,恭喜,找到了! - 如果
pivot的位置比K小,说明目标在右半部分,递归右边。 - 如果
pivot的位置比K大,说明目标在左半部分,递归左边。
- 如果
注意: 每次递归我们只处理一半的数据,所以速度极快。
3. 代码实现:JavaScript 版
ini
function findKthLargest(nums, k) {
// 转换为寻找下标为 nums.length - k 的元素
const targetIndex = nums.length - k;
return quickSelect(nums, 0, nums.length - 1, targetIndex);
}
function quickSelect(arr, left, right, targetIndex) {
if (left === right) return arr[left];
// 随机选择 pivot,避免最坏情况
const randomIndex = left + Math.floor(Math.random() * (right - left + 1));
[arr[left], arr[randomIndex]] = [arr[randomIndex], arr[left]];
const pivotIndex = partition(arr, left, right);
if (pivotIndex === targetIndex) {
return arr[pivotIndex];
} else if (pivotIndex < targetIndex) {
// 目标在右边
return quickSelect(arr, pivotIndex + 1, right, targetIndex);
} else {
// 目标在左边
return quickSelect(arr, left, pivotIndex - 1, targetIndex);
}
}
function partition(arr, left, right) {
const pivot = arr[left];
let i = left + 1;
let j = right;
while (true) {
while (i <= j && arr[i] < pivot) i++;
while (i <= j && arr[j] > pivot) j--;
if (i > j) break;
[arr[i], arr[j]] = [arr[j], arr[i]];
i++;
j--;
}
[arr[left], arr[j]] = [arr[j], arr[left]];
return j;
}
4. 性能分析:为什么它是 O(N)?
-
平均情况 :每次
partition都能把数据大致对半劈开。 N+N/2+N/4+...=2NN+N/2+N/4+...=2N,即 O(N) 。 -
最坏情况 :每次选的
pivot都是最大或最小值,退化为 O(N²)。- 优化方案 :使用随机化 Pivot(如代码所示)或"三数取中法",可以极大概率避免最坏情况。
5. 工业界应用:数据库与监控系统
-
数据库查询优化:
- 当执行
SELECT * FROM table ORDER BY score DESC LIMIT 10时,数据库引擎内部往往不会全表排序,而是使用类似快速选择或堆的算法来加速。
- 当执行
-
实时监控 Top N:
- 统计系统中占用 CPU 最高的前 10 个进程。
-
电商排行榜:
- 实时计算销量最高的商品列表。
6. 总结与面试考点
| 特性 | 说明 |
|---|---|
| 核心优势 | 平均 O(N) 时间复杂度,无需额外空间 |
| 稳定性 | 不稳定排序(会打乱原始顺序) |
| 适用场景 | 寻找第 K 大/小元素、Top K 问题 |
面试官常问:
- "快速选择和快速排序的区别?" (答:快排递归处理左右两边,快选只递归一边。)
- "如果数据量太大内存放不下怎么办?" (答:使用外部排序或多路归并,或者维护一个大小为 K 的堆。)
下期预告: 找数搞定了,接下来我们聊聊处理字符串和数组的万能钥匙------滑动窗口 (Sliding Window) 。看看它是如何优雅解决"最长无重复子串"问题的。
如果你觉得这篇干货对你有帮助,欢迎点赞收藏!🚀