LeetCode刷题笔记01-最小的K个数
1、题目分析
- 来自于剑指 Offer 40. 最小的k个数。
- 如果熟悉优先队列的话,本题首先想到的可能是用Java中的优先队列这一数据结构来做。
- 另外,如果熟悉快排,那么可以探索出另外一种思路,由于题目仅要求找到最小的K个数,并未要求这K个数的顺序,因此,如果我们通过快排的
partition
函数找到第K小的数的位置,那么它连同他前面的K-1个数就是最小的K个数。 - 此外,面试中也可能会让我们手动写一个
大根堆
来解这道题。 - 下面分别给出以上三种解法。
2、优先队列PriorityQueue
-
在这里需要注意优先队列的实现,尤其是比较器的写法,由于是求最小的K个,因此我们将优先队列实现为大根堆,(注意如果求的是最大的K个数则需要小根堆)而优先队列默认是小根堆的:
标准的写法,重写比较器的
compare
方法:Queue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2-o1; } });
利用lambda表达式(Java8新特性):
Queue<Integer> pq = new PriorityQueue<>((o1, o2) -> o2-o1);
-
代码思路即为若队列大小小于K,就往里加数
pq.offer(num)
,若等于K,则判断接下来要加入的数num
和队头的元素(队列中最大的元素)pq.peek()
的大小关系,若num<pq.peek()
,则将队头元素出队,num
入队。class Solution { public int[] getLeastNumbers(int[] arr, int k) { int n = arr.length; if (k == 0 || n == 0) { return new int[0]; } Queue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>(){ @Override public int compare(Integer o1, Integer o2){ return o2 - o1; } }); for(int num:arr){ if(pq.size()<k){ pq.offer(num); }else if (pq.peek()>num){ pq.poll(); pq.offer(num); } } int[] res = new int[k]; for(int i=0;i<k;i++){ res[i] = pq.poll(); } return res; } }
-
时间复杂度分析
要遍历N个数,每次需要维护容量为K的大根堆,因此时间复杂度为
O(NlogK)
。
3、快排partition()
-
由于题目只让求最小的K个数而不规定顺序如何,因此,基于上一种优先队列的思路其实是获得了本不用获得的信息,因此必不可能是最优解法,而快排的思路在于将比某个数小的元素放在其左边,将比它大的放在右边,如果能直接通过快排切分排好第 K 小的数(下标为 K-1),那么它左边的数就是比它小的另外 K-1 个数。
-
在代码实现方面,我们每次的pivot元素的选择采取随机的形式以避免极端的测试用例。
class Solution { public int[] getLeastNumbers(int[] arr, int k) { randomizedSelected(arr, 0, arr.length - 1, k); int[] vec = new int[k]; for (int i = 0; i < k; ++i) { vec[i] = arr[i]; } return vec; } private void randomizedSelected(int[] arr, int l, int r, int k) { if (l >= r) { return; } int pos = randomizedPartition(arr, l, r); //表示找到的是[l,r]区间中第num小的元素 int num = pos - l + 1; if (k == num) { return; } else if (k < num) { //找大了,再去pos前面找 randomizedSelected(arr, l, pos - 1, k); } else { //找小了,再去pos后面找第(k-num)小的元素,比如[2,1,3,4,5],要找第4小的,但第一次找了个第2小的,即数组变为[1,2,3,4,5],则需要去[3,4,5]子数组再去找第(4-2)小的元素。 randomizedSelected(arr, pos + 1, r, k - num); } } private int randomizedPartition(int[] nums, int l, int r) {、 //首先随机处理,new Random().nextInt(r - l + 1) + l 即为在[l,r]闭区间随机选择一个数与边界r位置元素做交换。 int i = new Random().nextInt(r - l + 1) + l; swap(nums, r, i); return partition(nums, l, r); } //返回nums[r]元素所该在的位置,经典的快排partition方法实现 private int partition(int[] nums, int l, int r) { int pivot = nums[r]; int i = l - 1; for (int j = l; j <= r - 1; ++j) { if (nums[j] <= pivot) { i++; swap(nums, i, j); } } //循环结束后,其他元素中小于等于nums[r]的元素都被移动到了[l,i]的位置,因此i++就是nums[r]该在的位置下标 i++; swap(nums, i, r); return i; } private void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } }
-
时间复杂度分析
第一次调用partition方法的时候需要遍历整个数组 (0 ~ n) 找到了下标是pos
的元素,而如果pos - l + 1
并非是k
,那么要么去前面找,要么去后面找,不妨设每次近似都是对半分,那么时间复杂度可以表示为 O(N+N/2+N/4+...+N/N)=O(N)
,因此利用快排的时间复杂度是 O(N)
。
4、自建大顶堆
-
使用大顶堆的代码思路跟使用优先队列是相同的,因为它们就是一个东西,重要的是如何建堆并维护堆,堆其实就是利用完全二叉树的结构来维护的一维数组,因此我们不必建树,直接用一个数组表示堆即可。
-
建堆的过程即为从最后一个非叶子结点开始,判断其与左右孩子结点值的大小关系,然后做交换,在交换过后,还要保证做交换的孩子所在的子树满不满足大顶堆,因此是一个
从下往上的遍历处理,每次处理则是一个从上往下的递归过程。
-
代码如下:
class Solution { public int[] getLeastNumbers(int[] arr, int k) { if(arr == null || arr.length == 0 || k == 0) return new int[0]; int[] result = new int[k]; int i = 0; while(i < k){ result[i] = arr[i]; i++; } buildHeap(result); //建堆 while(i < arr.length){ if(arr[i] < result[0]){ result[0] = arr[i]; //如果arr[i] < result[0],则说明堆顶元素,即result[0]肯定不是前K小的,将原来的值覆盖掉,相当于移除了堆顶元素,然后从堆顶即下标0开始调整大顶堆。 adjust(result, 0); } i++; } return result; } //从最后一个非叶子结点的位置(result.length/2-1)开始 public void buildHeap(int[] result){ for(int i = result.length / 2 - 1; i >= 0; i--){ adjust(result, i); } } public void adjust(int[] result, int i){ int maxIndex = i; int len = result.length; //判断其左右孩子结点的值是否比根结点要大,若大,则交换,比如[2,1,3],换为[3,1,2],并继续判断'2'和它的子树满不满足大顶堆。 if(2*i+1 < len && result[2*i+1] > result[maxIndex]) maxIndex = 2*i+1; if(2*i+2 < len && result[2*i+2] > result[maxIndex]) maxIndex = 2*i+2; if(maxIndex != i){ swap(result, maxIndex, i); adjust(result, maxIndex); } } public void swap(int[] result, int i, int j){ int temp = result[i]; result[i] = result[j]; result[j] = temp; } }