https://www.nowcoder.com/questionTerminal/6a296eb82cf844ca8539b57c23e6e9bf
题目描述
输入整数数组 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]
解题思路
方法1:
对于该题最简单的思路就是把输入的n个整数进行排序,排序之后位于最前面的K个数就是最小的K个数。
时间复杂度为:O(n*log(n))——排序的时间复杂度
空间复杂度:O(log(n))
方法2:
可以借鉴快速排序中Partition的思想来解决。——只有当我们可以修改输入的数组时,该方法可用
如果基于数组的第K个数字来进行调整,使得比第K个数字小的所有数字都位于数组的左侧,比第K个数字大的所有数字都位于数组的右边。
经过调整之后,位于数组左边的K个数字就是最小的K 个数字。
index > k-1 :表示k个最小的数字一定在index的左边;只需要对index的左边进行划分即可
index < k-1:index及index左边数字还没能满足k个数字,需要继续对k右边进行划分
时间复杂度:O(n), 最坏情况下是O(n^2)
方法3:大根堆实现
使用一个容量为K的容器来存储最小的K个数字。
每次从输入的n个数中读入一个数:
如果容器中已有的数字少于K个,直接将读入的整数放入容器中
如果容器中已有K个数自,就不能再插入数字。
拿待插入的数字和容器中K个数字中的最大值进行比较,
如果该数字大于容器中的最大值,抛弃该数字
否则,交换该数与容器中的最大值
容器的实现应该用数据结构中的大根堆,其根结点的值永远是最大的结点值。
- 优先级队列(PriorityQueue)中维护的是堆,使用时传入比较器,让数据从大到小排列,堆顶的元素为最大的。
- 用红黑树来实现最大堆容器。TreeSet类实现了红黑树的功能,它的底层是通过TreeMap实现的,TreeSet中的数据会按照插入数据自动升序排序。我们只需要将数据放入TreeSet中,其就会为我们实现排序。
时间复杂度:O(nlogk)
空间复杂度:O(k),因为大根堆里最多 k 个数。
代码实现:
方法1:
public class Solution2 {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> list = new ArrayList<>();
//一定要注意将特殊情况考虑到
if (input == null || input.length == 0 || input.length < k){
return list;
}
Arrays.sort(input);//时间复杂度是O(n*log(n))
for (int i = 0; i < k; i++) {
list.add(input[i]);
}
return list;
}
public static void main(String[] args) {
//输入的数组中没有相同的数字
int[] array = {4,5,1,6,2,7,3,8};
Solution2 solution2 =new Solution2();
//边界值测试
System.out.println(solution2.GetLeastNumbers_Solution(array, 1));//k=1
System.out.println(solution2.GetLeastNumbers_Solution(array, 8));//k=array.length
//特殊值测试:k<1,k>array.length()
System.out.println(solution2.GetLeastNumbers_Solution(array, 0));
System.out.println(solution2.GetLeastNumbers_Solution(array, 10));
//输入的数字中相同的数字
int[] array1 = {1,2,3,4,5,3,2};
System.out.println(solution2.GetLeastNumbers_Solution(array1, 1));//k=1
System.out.println(solution2.GetLeastNumbers_Solution(array1, array1.length));//k=array.length
//特殊值测试:
System.out.println(solution2.GetLeastNumbers_Solution(array1, 0));//k<1
System.out.println(solution2.GetLeastNumbers_Solution(array1, 10));//k>array.length()
}
}
方法2:
/**
* 基于Partition函数的解法
*/
public class Solution {
public static ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
//时间复杂度:O(n)
ArrayList<Integer> list = new ArrayList<>();
if (input == null || input.length ==0 || input.length < k){
return list;
}
int n = input.length;
int start =0;
int end = input.length;
int index = Partition(input,n,start,end);
while (index != k-1){
if (index > k-1){
end = index-1;
index = Partition(input,n,start,end);
}else {
start =index+1;
index = Partition(input,n,start,end);
}
}
for (int i=0;i < k;i++){
list.add(input[i]);
}
return list;
}
private static int Partition(int[] input, int n, int start, int end) {
//挖坑法
int leftIndex= start;
int rightIndex = end-1;
int key = input[leftIndex];
while (leftIndex < rightIndex){
while (leftIndex < rightIndex && input[rightIndex] >= key){
rightIndex--;
}
input[leftIndex] = input[rightIndex];
while (leftIndex < rightIndex && input[leftIndex] <= key){
leftIndex++;
}
input[rightIndex] = input[leftIndex];
}
input[leftIndex] = key;
return leftIndex;
}
public static void main(String[] args) {
int[] array = {1,2,3,4,3,5,6};
System.out.println(GetLeastNumbers_Solution(array,4));
}
}
方法3:
/**
* 基于堆或者红黑树的解法
*/
public class Solution3_Best {
public static ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> list = new ArrayList<>();
if (input == null || input.length == 0 || input.length<k|| k==0){
return list;
}
/**
* PriorityQueue实现
*/
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;//由大到小进行排序
}
});
for (int i = 0; i < input.length; i++) {
if (priorityQueue.size()< k){
priorityQueue.offer(input[i]);
}else if(input[i] < priorityQueue.peek()){//和堆顶的元素进行比较
priorityQueue.poll();
priorityQueue.offer(input[i]);
}
}
for (int i =0;i<k;i++){
list.add(priorityQueue.poll());
}
/**
* TreeSet实现
*/
TreeSet<Integer> kSet = new TreeSet<>();
for (int i=0;i<input.length;i++){
if (kSet.size() < k){
kSet.add(input[i]);
}else if(input[i] < kSet.last()){//和kSet中的最后一个元素进行比较
kSet.remove(kSet.last());
kSet.add(input[i]);
}
}
for (Integer e :kSet){
list.add(e);
}
return list;
}
}
比较
由于第一种排序的方法比较简单,一般面试还是会让写出更优的算法,所以需要着重关注后面两种解决方案。
后面两种方法的特点比较:
- 基于Partiton的解法的平均时间复杂度是O(n),但是它有明显的限制,会修改输入的数组。
- 基于堆或红黑树的解法,有两个明显的优点:
- 没有修改输入的数据
- 该算法适合海量数据的输入。
假设要求从海量的数据中找出最小的k个数,由于内存的大小是有限的,不可能把这些数据一次性全部载入内存。此时可以从辅存中每次读入一个数字,判断能不能放入容器中。
这种方式最适合的情形就是n很大并且k较小的问题。
基于Partition的解法 | 基于堆或红黑树的解法 | |
---|---|---|
时间复杂度 | O(n) | O(nlogk) |
是否需要修改数组 | 是 | 否 |
是否适用于海量的数据 | 否 | 是 |