1.数组中第k大/前k大的元素(215-中)
示例:
输入: [3,2,1,5,6,4] 和 k = 2 输出: 5 输入: [3,2,1,5,6,4] 和 k = 2 输出: [5, 6]
思路:
法1:快排变形:类似传统快排,区别在于,我们每次进行划分得到的索引index,我们只需要比较该索引值与目标索引的大小,继续递归满足条件的区间,而不需要对整个数组进行排序;
法2:优先队列:直接利用java
现成的PriorityQueue
优先队列实现(默认小根堆),维持有K
个元素的小根堆,最终返回堆顶或者将堆中所有元素输出。
代码1:时间复杂度O(n)
// 数组中第k大元素 public int findKthLargest(int[] nums, int k) { if (nums == null || nums.length == 0 || k == 0) return 0; int target = nums.length - k; return quickSort(nums, 0, nums.length - 1, target); } private int quickSort(int[] nums, int l, int r, int target) { int index = partition(nums, l, r); if (index == target) return nums[index]; return index > target ? quickSort(nums, l, index - 1, target) : quickSort(nums, index + 1, r, target); } // 划分过程:一定要随机选取划分值! private int partition(int[] nums, int l, int r) { swap(nums, l + (int)(Math.random() * (r - l + 1)), r); int less = l - 1, more = r; while (l < more) { if (nums[l] < nums[r]) { swap(nums, ++less, l++); } else if (nums[l] > nums[r]) { swap(nums, --more, l); } else { l++; } } swap(nums, more, r); return more; } public void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; }
代码2:时间复杂度O(nlogk)
// 数组第k大元素的队列实现 public int findKthLargest(int[] nums, int k) { PriorityQueue<Integer> pq = new PriorityQueue<>(); for (int num : nums) { pq.add(num); // 维持小根堆大小为K if (pq.size() > k) { pq.poll(); } } return pq.peek(); } // 数组前k大元素的队列实现 public int[] largestK(int[] arr, int k) { if (arr == null || arr.length == 0 || k == 0) return new int[0]; Queue<Integer> pq = new PriorityQueue<>(); for (int num : arr) { pq.add(num); if (pq.size() > k) { pq.poll(); } } int[] ans = new int[pq.size()]; int index = 0; for (int i : pq) { ans[index++] = i; } return ans; }
2.数组中第k小/前k小个的元素(面试题)
示例:
输入: [3,2,1,5,6,4] 和 k = 2 输出: 5 输入: arr = [1,3,5,7,2,4,6,8], k = 4 输出: [1,2,3,4]
思路:
法1:快排变形:与上题思路相同,唯一不同的就是目标索引,数组中第k小/前k小个的元素为k - 1
;
法2:优先队列:重写比较方法,维持有K
个元素的小根堆,最终返回堆顶或者将堆中所有元素输出。
代码1:时间复杂度O(n)
// 数组中前k小的元素 public int[] smallestK(int[] arr, int k) { if (k == 0 || arr == null || arr.length == 0) return new int[0]; // 注意:第k小的元素下标为k - 1 return quickSort(arr, 0, arr.length - 1, k - 1); } private int[] quickSort(int[] arr, int l, int r, int k) { int index = partition(arr, l, r); if (index == k) { // 复制数组arr的前k个数,也就是0 - k-1 return Arrays.copyOf(arr, index + 1); } return index > k ? quickSort(arr, l, index - 1, k) : quickSort(arr, index + 1, r, k); } private int partition(int[] arr, int l, int r) { // 随机选取划分值 swap(arr, l + (int)(Math.random() * (r - l + 1)), r); int less = l - 1, pivot = arr[r]; for (int i = l; i < r; ++i) { if (arr[i] <= pivot) { swap(arr, ++less, i); } } swap(arr, less + 1, r); return less + 1; } public void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
代码2:时间复杂度O(nlogk)
// 数组第k小的队列实现 public int findKthSmallest(int[] nums, int k) { if (nums == null || nums.length == 0 || k == 0) return new int[0]; Queue<Integer> pq = new PriorityQueue<>((v1, v2) -> v2 - v1); for (int num : nums) { pq.add(num); if (pq.size() > k) { pq.poll(); } } return pq.peek(); } // 前k小的元素 public int[] smallestK(int[] arr, int k) { if (arr == null || arr.length == 0 || k == 0) return new int[0]; Queue<Integer> pq = new PriorityQueue<>((v1, v2) -> v2 - v1); for (int num : arr) { pq.add(num); if (pq.size() > k) { pq.poll(); } } int[] ans = new int[pq.size()]; int index = 0; for (int i : pq) { ans[index++] = i; } return ans; }
队列的add()方法和offer()方法的区别:
两者都是往队列尾部插入元素,不同的时候,当超出队列界限的时候,
- add()方法是抛出异常让你处理,
- offer()方法是直接返回false
总结
topk问题还有像线性查找算法(bfprt)等高效的算法,这里只介绍使用快排思路实现和优先级队列实现两种思路。
- 优先队列:代码简单,即遍历数组,维护一个大小为k的堆(面试可以先写出),但是时间复杂度较高O(NlogK)。
- 快排变形:关键是我们无需对不需要的区间进行排序,只需要找到目标值或者目标区间即可,可在O(N)时间复杂度解决问题。
对于快排变形:
- 数组中第k大/前k大的元素:目标索引为
nums.length - k
; - 数组中第k小/前k小个的元素:目标索引为
k - 1
。
对于优先队列:
- 数组中第k大/前k大的元素:使用小根堆(堆顶元素最小,PriorityQueue默认实现);
- 数组中第k小/前k小个的元素:使用大根堆(堆顶元素)。
最终的返回区间结果:
- 快排变形:使用
Arrays.copyOf(arr, k)
:返回arr数组的前k个数; - 优先队列:直接使用增强for循环取出队列元素存入数组。
参考鸣谢: