top K 问题
top K 问题
/**
* 输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
*
*
*
* 示例 1:
*
* 输入:arr = [3,2,1], k = 2
* 输出:[1,2] 或者 [2,1]
* 示例 2:
*
* 输入:arr = [0,1,2,1], k = 1
* 输出:[0]
*
*
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
1. 方法一,排序,时间复杂度 O(nlgk)
2. 利用最大堆
-
遍历数组,前k个数字依次入队,构建一个最大堆,假设此时堆内就是前k个数字,且堆首是最大元素
-
后面的数字和堆首元素比较,如果比堆首元素小,接着入队,否则看下一个数字
-
遍历结束,堆内就是前k个数字
import java.util.Arrays; import java.util.Collections; import java.util.PriorityQueue; public class Leeoff40 { public int[] getLeastNumbers(int[] arr, int k) { if(invalidePar(arr, k)) throw new IllegalArgumentException("参数错误"); k = Math.min(arr.length, k); // 防止k超过数组长度 // 最大堆,传入逆序迭代器 PriorityQueue<Integer> priQue = new PriorityQueue<>(Collections.reverseOrder()); // 前k个元素入堆 for(int i = 0; i < k; ++i){ priQue.add(arr[i]); } // k个元素以后直接选最大的元素入堆 for(int j = k; j < arr.length; ++j){ Integer maxValue = priQue.remove(); priQue.add(Math.min(maxValue, arr[j])); } // 返回结果 Integer[] res = priQue.toArray(new Integer[0]); return Arrays.stream(res).mapToInt(Integer::valueOf).toArray(); // 转成int数组 } private boolean invalidePar(int[] arr, int k){ return (arr == null) || (k <= 0) || arr.length == 0; } public static void main(String[] arg){ Leeoff40 lee = new Leeoff40(); System.out.println(Arrays.toString(lee.getLeastNumbers(new int[]{1,5,2}, 2))); System.out.println(Arrays.toString(lee.getLeastNumbers(new int[]{0,1,2,1}, 1))); System.out.println(Arrays.toString(lee.getLeastNumbers(new int[]{1,5,2}, 0))); System.out.println(Arrays.toString(lee.getLeastNumbers(new int[]{1,5,2}, 4))); System.out.println(Arrays.toString(lee.getLeastNumbers(null, 4))); } }
3. 利用快速排序中分治的思路O(n)
-
利用快排找到中枢的下标m,分成前后两段
-
m < k ,则在后半段寻找k
-
m > k ,则在前半段寻找k
-
m == k , k已经找到
import java.util.Arrays; import java.util.Random; public class LeeOff40 { private static Random random = new Random(); public int[] getLeastNumbers(int[] arr, int k) { int[] res = null; int piv; if(invalidePar(arr, k)) throw new IllegalArgumentException("参数错误"); k = Math.min(arr.length, k); // 防止k超过数组长度 piv = getPiv(arr, 0, arr.length-1); while (piv != k - 1){ if(piv > k - 1)piv = getPiv(arr, 0, piv - 1); else if(piv < k - 1) piv = getPiv(arr, piv + 1, arr.length - 1); } res = new int[k]; System.arraycopy(arr, 0, res, 0, k); return res; } private boolean invalidePar(int[] arr, int k){ return (arr == null) || (k <= 0) || arr.length == 0; } /** * 选定一个枢纽,一个数组分成两部分, 左边的比这个数小,右边的数比这个数大 * @param arr * @param start * @param end * @return 这个枢纽的下标 */ private int getPiv(int[] arr, int start , int end){ int index = start + random.nextInt(end-start+1); // 随机选择中枢 swap(arr, start, index); // index位置的元素和start位置的元素交换顺序 int origin = arr[start], leftP = start, rightP = end; while (leftP < rightP){ // 从后往前扫描,找到一个小于origin的值 while (leftP < rightP && arr[rightP] >= origin)--rightP; swap(arr, rightP, leftP); // 从前往后扫,找到一个大于origin的值 while (leftP < rightP && arr[leftP] <= origin)++leftP; swap(arr, leftP, rightP); } return leftP; } private void swap(int[] arr, int start, int index){ // arr[start] 和 arr[index] 交换, 适用与不同的两个数字 /*arr[start] = arr[start] ^ arr[index]; arr[index] = arr[start] ^ arr[index]; arr[start] = arr[start] ^ arr[index];*/ /*arr[start] = arr[start] + arr[index]; arr[index] = arr[start] - arr[index]; arr[index] = arr[start] - arr[index];*/ int temp = arr[start]; arr[start] = arr[index]; arr[index] = temp; } public static void main(String[] arg){ LeeOff40 lee = new LeeOff40(); System.out.println(Arrays.toString(lee.getLeastNumbers(new int[]{3,2,1}, 2))); System.out.println(Arrays.toString(lee.getLeastNumbers(new int[]{0,1,2,1}, 1))); // System.out.println(Arrays.toString(lee.getLeastNumbers(new int[]{1,5,2}, 0))); // System.out.println(Arrays.toString(lee.getLeastNumbers(new int[]{1,5,2}, 4))); // System.out.println(Arrays.toString(lee.getLeastNumbers(null, 4))); // System.out.println(Arrays.toString(lee.getLeastNumbers(new int[0], 4))); } }
注意的地方:
- 第一种和第三种方法,都需要把数组全部加载到内存中,而第二种方法适合处理海量数据,依次从磁盘读取,且不会改变数组的顺序
- 注意参数检查