题目:
输入整数数组 arr
,找出其中最小的 k
个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
题解:
这道题目难度是简单,第一眼会想到sort排序,然后找出前k个数:
public static int[] getLeastNumbers(int[] arr, int k) {
Arrays.sort(arr);
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = arr[i];
}
return res;
}
时间复杂度:O(nlogn)
看到这里,可能有人会想,这道题目真的这么简单!简直侮辱智商!事实上,并不是这样的解法,如果真的这么做,可能会被无情pass,接下来,我们想一想有没有其他的办法。
我们可以思考一下,有没有其他的排序方法可以使用,仔细一想,有很多的排序方法,比如快排,堆排序,这里先从堆排序入手。堆排序分为大顶堆和小顶堆,如果我们维护一个小顶堆,直接取出前k个数,是不是完全ok的?理论上是可以的,java的优先队列PriorityQueue就是使用小顶堆实现的,我们只需要把数组内的数值放入队列中即可(关于堆排序的具体实现,以后会说。在这里,其实并没有必要手动实现堆排序,重复造轮子是一件得不偿失的事情):
public static int[] getLeastNumbers(int[] arr, int k) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
for (int i = 0; i < arr.length; i++) {
priorityQueue.offer(arr[i]);
}
int[] priority = new int[k];
for (int i = 0; i < k; i++) {
priority[i] = priorityQueue.poll();
}
return priority;
}
其实这种解法和前面提到的sort解法没有什么区别,时间复杂度相同,且完全没必要维护n个结点的小顶堆,只需要维护k个结点的小顶堆即可。可以先将k个结点放入堆中进行维护,形成小顶堆,然后将数组中k之后的数值放入小顶堆中,通过和根节点的值比较大小,确定此数值是否加入小顶堆中。
理论如此,但是实现起来是很麻烦的,因为小顶堆的根节点是k个数值中的最小值,当新来的数值x和根节点root的数值进行比较时,假如x<root,x可以加入小顶堆,取代root,但是因为原本root的值比x大,但比小顶堆内其他值小,那么还要找到另一个出堆的值;再假如,x>root,怎么处理呢,x比root大是一定的,但是x和小顶堆内根节点以外的值谁大谁小呢。
所以,换种思路,维护k个结点的大顶堆。大顶堆的根节点为最大值,其他节点小于根节点,所以只需要和根节点比较即可,若x>root,说明不在最小的k个数值范围之内,直接过滤掉;若x<root,x加入大顶堆,root出堆即可。
PriorityQueue<Integer> priority = new PriorityQueue<>((o1,o2) -> {
return o2 - o1;
}
);
需要重写一下比较器,将小顶堆转化为大顶堆;
for (int i = k; i < arr.length; i++) {
if(arr[i] < priority.peek()){
priority.poll();
priority.offer(arr[i]);
}
}
这是核心代码,即只需要处理当前值小于堆顶元素的情况就行。
完整代码:
public static int[] getLeastNumbers(int[] arr, int k) {
if(k > arr.length || k == 0) return new int[]{};
PriorityQueue<Integer> priority = new PriorityQueue<>((o1,o2) -> {
return o2 - o1;
}
);
for (int i = 0; i < k; i++) {
priority.offer(arr[i]);
}
for (int i = k; i < arr.length; i++) {
if(arr[i] < priority.peek()){
priority.poll();
priority.offer(arr[i]);
}
}
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = priority.poll();
}
return res;
}
这种方法的时间的时间复杂度是O(nlogk);
还有另外一种排序方法:快速排序。快排的思路就不多赘述了,直接贴代码了。
public static int[] getLeastNumbers(int[] arr, int k) {
if(arr == null || k == 0) return new int[0];
quickSort(arr,0,arr.length - 1);
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = arr[i];
}
return res;
}
public static void quickSort(int[] arr,int i,int j){
if(i >= j) return;
int left = i,right = j,tmp = arr[left];//将tmp作为哨兵
while (left < right){
while (left < right && arr[right] >= tmp) right--;//如果right指向的值大于等于哨兵,right左移
arr[left] = arr[right];
while (left < right && arr[left] <= tmp) left++;//如果left指向的值小于等于哨兵,left右移
arr[right] = arr[left];
}
arr[left] = tmp;
quickSort(arr,i,left - 1);
quickSort(arr,left + 1,j);
}
快排需要排序,时间复杂度为O(nlogn)。这里使用快排,是为了引出快速选择算法。快排中,每一轮排序后,都是左边小右边大,比如5,7,3,4,8,第一轮排序下来就是4,3,5,7,8,显然5就是第三小的数值,同时这道题目对于k个数字的顺序是不做要求的,所以,我们只要找到i==k就可以,不需要全部排序。
public static int[] getLeastNumbers(int[] arr, int k) {
if(arr == null || k == 0) return new int[0];
if(k >= arr.length) return arr;
quickSearch(arr, 0, arr.length - 1, k );
return Arrays.copyOf(arr, k);
}
public static void quickSearch(int[] arr,int i,int j,int k){
int left = i,right = j,tmp = arr[left];//将tmp作为哨兵
while (left < right){
while (left < right && arr[right] >= tmp) right--;//如果right指向的值大于等于哨兵,right左移
arr[left] = arr[right];
while (left < right && arr[left] <= tmp) left++;//如果left指向的值小于等于哨兵,left右移
arr[right] = arr[left];
// swap(arr,left,right);
}
arr[left] = tmp;
if(left < k) quickSearch(arr, left + 1, j, k);
if(left > k) quickSearch(arr,i,left - 1,k);
return;
}
和快排的代码基本相同,不同的只是改变了结束条件,快排需要排序到仅剩一个数字,而快速选择只要找到i==k即可,当left<k时,在left的右边排序,当left>k时,在left左边排序,当left==k时,直接return(left和right都指向同一个地方)。
时间复杂度:O(n)
还有一种计数排序的方法,直接记录每个数字出现的次数,然后返回:
public static int[] getLeastNumbers(int[] arr, int k) {
if(arr == null || k == 0) return new int[0];
int[] counter = new int[10001];
int[] res = new int[k];
for (int num:arr) {
counter[num]++;
}
int idx = 0;
for (int i = 0; i < counter.length; i++) {
while (counter[i]-- > 0 && idx < k){
res[idx++] = i;
}
if(idx == k) break;
}
return res;
}
时间复杂度:O(n)
总结:题目简单,但是解法很多,对于排序的掌握也很重要