面试常问:如何在一个长度为n的无序数据快速获取前k个数值
1. 场景引入
在面试中,我们经常会遇到类似的问题:
“给定一个长度为
n
的无序数组,如何快速找出其中前k
个最大值?”
初看之下,这似乎只需排序即可解决。但当面对亿级甚至更大规模的数据时,全量排序(O(n log n))的成本极高,不现实。在这种大数据背景下,如何在时间复杂度和空间复杂度之间权衡,高效找出前 k
个数值,成为考察我们算法功力的关键点。
2. 传统排序的局限与优化思路
很多人在遇到这类题目时,第一反应是“全量排序取前 k
大值”,比如直接用Arrays.sort()
。但如果数组长度达到了 10910^9109,排序的时间和空间消耗你恐怕很难接受。
这类问题在算法面试中广泛存在,既考察你对数据结构的理解,也检验你在大规模数据下的优化能力。本文会带你从最直观的排序方案,逐步过渡到小顶堆、快速选择等更高效的做法,帮你理解如何真正搞定“如何高效获取无序数组的前 k
个值”这个核心问题。
3. 场景拓展与问题分析
其实在实际业务中,找“Top K”非常常见:
- 电商推荐:找出销量排名前100的商品
- 日志分析:统计访问量最高的IP
- 搜索引擎:返回相关性得分最高的前10条记录
你不仅在面试中会被问到,在实际开发也常常遇到:
“在长度为
n
的无序数组中,如何快速找到前k
个数?”
4. 堆排序:解决“Top K”更高效
全量排序问题的本质在于,我们只关心前k
个值,没必要排序所有元素。这里,小顶堆(或者大顶堆,针对最小k值)是高效解法之一。核心思想是:
- 构建一个大小为
k
的优先队列(最小堆或最大堆)。 - 依次遍历每个元素:
- 若堆未满,直接加入;
- 若堆已满,且当前元素比堆顶大(对前 K 大值场景),则弹出堆顶,加入新元素;
- 最终堆内存放的就是前
k
个最大值。
Java 实现代码:
1、堆排序(获取前k个最大值或最小值)
获取数组中前k个最大值
// 获取数组中前k个最大值
public int[] searchTopK(int[] nums, int k) {// 创建一个容量为k的小根堆(默认是小根堆)PriorityQueue<Integer> queue = new PriorityQueue<>(k);// 遍历数组中的每个元素for (int num : nums) {if (queue.size() < k) {// 如果堆还没有装满,就直接加入元素queue.offer(num);} else if (num > queue.peek()) {// 如果堆已满,且当前元素比堆顶(最小的元素)大// 移除堆顶(最小值),然后加入当前元素queue.poll();queue.offer(num);}}// 将堆中的元素转移到结果数组中(逆序取出)int[] result = new int[k];for (int i = k - 1; i >= 0; i--) {result[i] = queue.poll();}return result;
}
获取数组中前k个最小值
// 获取数组中前k个最小值
public static int[] searchTopK2(int[] nums, int k) {// 特殊情况处理:数组为空或k为0if (nums.length == 0 || k == 0) {return new int[]{};}// 创建容量为k的大根堆(自定义比较器,堆顶为最大值)PriorityQueue<Integer> queue = new PriorityQueue<>(k, (a, b) -> b - a);// 遍历数组for (int i = 0; i < nums.length; i++) {if (queue.size() < k) {// 堆未满,直接加入元素queue.offer(nums[i]);} else if(queue.peek() > nums[i]){// 如果当前元素比堆顶(最大值)小,则替换 queue.poll(); // 移除最大值queue.offer(nums[i]); // 插入较小的元素}}}// 将堆中的元素转为数组(逆序)int[] res = new int[k];for (int i = k - 1; i >= 0; i--) {res[i] = queue.poll();}return res;
}
时间复杂度: 建堆和维护堆:O(n log k)
空间复杂度: O(k)
2、获取第k个最大值
//获取第k大的最大值public static int findKthLargest(int[] nums, int k) {//创建小根堆PriorityQueue<Integer> minHeap = new PriorityQueue<>(k);for (int num : nums) {if (minHeap.size() < k) {minHeap.offer(num); // 堆未满,直接放} else if (num > minHeap.peek()) {minHeap.poll(); // 弹出最小值minHeap.offer(num); // 放入新值}}return minHeap.peek(); // 堆顶就是第k大}
//获取第k个最小值public static int findKthSmallest(int[] nums, int k) {//创建大根堆PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k,(a,b)->b-a);for (int num : nums) {if (maxHeap .size() < k) {maxHeap.offer(num); // 堆未满,直接放} else if (num < maxHeap.peek()) {maxHeap.poll(); // 弹出最小值maxHeap.offer(num); // 放入新值}}return maxHeap.peek(); // 堆顶就是第k小}
3、查找top3最大值
public static void searchTop3(int[]nums){// 初始化前三大值为最小整数Integer a = null, b = null, c = null;for (int num : nums) {// 跳过已经存在的值,确保去重if (num == a || num == b || num == c) {continue;}if (a == null || num > a) {c = b;b = a;a = num;} else if (b == null || num > b) {c = b;b = num;} else if (c == null || num > c) {c = num;}}System.out.println("a="+a+",b="+b+",c="+c);}
时间复杂度:O(n)
空间复杂度:O(1)
4、PriorityQueue介绍
概述
PriorityQueue
是Java中的一个基于堆(Heap)实现的优先队列。- 元素会根据**优先级(自然顺序或自定义比较器)**自动排序。
- 常用场景:需要不断获取当前最小或最大元素的队列。
底层实现
- 基于堆(Heap):通常是完全二叉树,用数组存储。
- 堆性质:
- 最小堆(Min-Heap):堆顶(根节点)是最小元素。
- 最大堆(Max-Heap):堆顶是最大元素(通过自定义比较器实现)。
- 操作:
offer()
和poll()
操作时间复杂度为O(log n)
。
主要方法
方法 | 描述 | 复杂度 |
---|---|---|
offer(e) | 插入元素 e ,确保堆性质保持 | O(log n) |
poll() | 移除并返回堆顶元素(最小或最大) | O(log n) |
peek() | 查看堆顶元素(不移除) | O(1) |
size() | 获取元素个数 | O(1) |
isEmpty() | 判断队列是否为空 | O(1) |
默认排序
- 默认(无参数构造):
- 是一个最小堆,元素根据元素的自然顺序(比如整数的从小到大)排序。
peek()
返回最小元素。
- 示例:
PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.offer(5); pq.offer(2); pq.offer(8); System.out.println(pq.peek()); // 输出 2
自定义排序
- 通过传入比较器(
Comparator
),可以实现最大堆或其他排序策略。
1. 最大堆(大根堆)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a,b)->b-a);
maxHeap.offer(5);
maxHeap.offer(2);
maxHeap.offer(8);
System.out.println(maxHeap.peek()); // 输出 8
2. 自定义比较器
PriorityQueue<Person> customQueue = new PriorityQueue<>((Person p1, Person p2) -> p2.getAge() - p1.getAge());