给定一个长度为 n 的可能有重复值的数组,找出其中不去重的最小的 k 个数。例如数组元素是4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4(任意顺序皆可)。
数据范围:0<=k,100000≤k,n≤10000,数组中每个数的大小 10000≤val≤1000
要求:空间复杂度 O(n) ,时间复杂度 O(nlogn)
示例一
输入:
[4,5,1,6,2,7,3,8],4
复制
返回值:
[1,2,3,4]
复制
说明:
返回最小的4个数即可,返回[1,3,2,4]也可以
示例二
输入:
[1],0
复制
返回值:
[]
示例三
输入:
[0,1,2,1,2],3
复制
返回值:
[0,1,1]
法一:快速排序
算法步骤:将输入数组进行快速排序,若快排一轮结束后,首数字插入位置==k,那么说明前k个数就是最小的k个数,若首数字插入位置<k,那么说明还需要对后面的数字进行快排,若首数字插入位置>k,则需要对前边的数字再进行快排,直到最后数字插入位置为k再返回前k个数字的数组即为结果。
时间复杂度一般情况下O(nlogn),空间复杂度O(1)
易错点:容易忘记快速排序left < right的条件在快排内部也需要出现,也需要进行比较。
import java.util.*;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
ArrayList<Integer> res = new ArrayList<Integer>();
if(k > input.length || k < 0)
return res;
QuickSort(input, res, 0, input.length-1, k);
return res;
}
public void QuickSort(int[] input, ArrayList<Integer> res, int left, int right, int k){
int start = left;
int end = right;
while(left < right){
while(left < right && input[start] <= input[right])
right--;
while(left < right && input[start] >= input[left])
left++;
swap(input, left, right);
}//快速排序
swap(input, start, left);//把首数字移到指针处
if(k == left){//首数字插入位置==k,那么说明前k个数就是最小的k个数
for(int i = 0; i < k; i++){
res.add(input[i]);
}
}
else if(k > left)//若首数字插入位置<k,那么说明还需要对后面的数字进行快排
QuickSort(input, res, left + 1, end, k);
else//若首数字插入位置>k,则需要对前边的数字再进行快排
QuickSort(input, res, start, left - 1, k);
}
private void swap(int[] input, int num1, int num2){
if(num1 == num2)
return;
int temp = input[num1];
input[num1] = input[num2];
input[num2] = temp;
}//交换数组内部元素
}
法二 大根堆
算法步骤:将k个数字先初始建堆成为大根堆,即上层数字大于下层数字,再插入余下数字到大根堆中,如果该数字小于堆顶,则弹出堆顶元素插入该数字到堆中,最后排序完成的,有k个数字的堆输出到数组中就是结果。
(该方法参考了:牛客网用户 刷刷题 的代码)
新知识:
PriorityQueue bigHeap = new PriorityQueue<>((num1, num2) -> num2 - num1)是创建大根堆。
(num1, num2) -> num2 - num1代表堆内父子节点元素按照大到小排序 构成大顶堆。
比较器中,num1是要被添加的元素,num2是堆顶元素。
import java.util.*;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
return bigHeap(input, k);
}
public ArrayList<Integer> bigHeap (int [] input, int k) {
ArrayList<Integer> result = new ArrayList<>(k);
//根据题意要求,如果K>数组的长度,返回一个空的数组
if (k > input.length || k == 0) {
return result;
}
/**
* 创建最大堆 看PriorityQueue的offer源码可知:
* public boolean offer(E e) {
* if (e == null)
* throw new NullPointerException();
* modCount++;
* int i = size;
* if (i >= queue.length)
* grow(i + 1);
* size = i + 1;
* if (i == 0)
* queue[0] = e;
* else
* siftUp(i, e);
* return true;
* }
* siftUp(i, e)这个方法,当插入的元素不是顶部位置,会进行内容排序调整,siftUp(i, e)方法就是起到这个作用
* 默认的插入规则中,新加入的元素可能会破坏小顶堆或大顶堆的性质,因此需要进行调整。
* 调整的过程为:从尾部下标的位置开始,将加入的元素逐层与当前点的父节点的内容进行比较并交换,直到满足父节点内容都小于或大于子节点的内容为止。
* private void siftUp(int k, E x) {
* while (k > 0) {
* int parent = (k - 1) >>> 1;//parentNo = (nodeNo-1)/2
* Object e = queue[parent];
* if (comparator.compare(x, (E) e) >= 0)//调用比较器的比较方法
* break;
* queue[k] = e;
* k = parent;
* }
* queue[k] = x;
* }
*
* 这里覆盖的比较器,num1是要被添加的元素,num2是堆顶元素,第一次放入第一个元素,直接队列头元素赋值为该第一个元素,后续
* 放入第N个元素的时候,会执行siftUp 紧随着会执行比较器 根据比较器构建最大堆还是最小堆
* siftUp(i, e)这个方法:默认的插入规则中,新加入的元素可能会破坏小顶堆或大顶堆的性质,因此需要进行调整。
* 调整的过程为:从尾部下标的位置开始,将加入的元素逐层与当前点的父节点的内容进行比较并交换,直到满足父节点内容都小于或大于子节点的内容为止。
*
* 我们重写的比较器是:comparator:表示比较器对象,如果为空,使用自然排序
* (num1, num2) -> num2 - num1
* 所以代表 堆内父子节点元素按照大到小排序 构成大顶堆
*/
PriorityQueue<Integer> bigHeap = new PriorityQueue<>(new Comparator<Integer>() {
//结合自定义比较器 构建大顶堆, 如果不重写比较器,那么默认为小顶堆
@Override
public int compare (Integer num1, Integer num2) {
return num2 - num1;
}
});
for (int i = 0; i < k; i++) {
bigHeap.offer(input[i]);
}
//因为是最大堆,也就是堆顶的元素是堆中最大的,遍历数组后面元素的时候,
//如果当前元素比堆顶元素小,就把堆顶元素给移除,然后再把当前元素放到堆中
for (int j = k; j < input.length; j++) {
if (bigHeap.peek() > input[j]) {
bigHeap.poll();
bigHeap.offer(input[j]);
}
}
for (int h = k - 1; h >= 0; h--) {
result.add(bigHeap.poll());
}
return result;
}
}