剑指 Offer 40. 最小的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]
方法一:最大堆:
需要借助java的PriorityQueue来完成,我们先来简单介绍一下PriorityQueue。
PriorityQueue
底层的数据结构是一个最小堆,而最小堆是一个二叉堆,其结构符合完全二叉树的要求,并且:任意一个节点的值小于其左右子节点的值。
而对应的还有最大堆,顾名思义,即任意一个节点的值大于其左右节点的值。
所以可知,最小堆的堆顶就是整个堆的最小元素。
限制:
- PriorityQueue不允许插入空值。
- PriorityQueue不允许插入不可比较的对象。PriorityQueue使用Comparable和Comparator接口来给对象排序。
- PriorityQueue是无界队列,即大小无限制,会自动扩容。
- PriorityQueue是非线程安全的,如果需要线程安全可以使用PriorityBlockingQueue。
PriorityQueue的使用方法:
创建:
创建一个PriorityQueue需要实现一个Comparator接口,如果不实现这个接口,则底层数据结构默认为最小堆。
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>(){
//接口的具体实现
public int compare(Integer num1,Integer num2){
return num1 - num2;
}
});
接口的实现函数compare(Integer num1,Integer num2)
,如果返回值为num1-num2
则创建一个最小堆,如果返回值为num2-num1
则创建一个最大堆,如果不实现这个接口,则默认创建一个最小堆。
//不实现接口。默认创建最小堆
PriorityQueue<Integer> queue = new PriorityQueue<Integer>();
添加、删除、查看堆顶元素
和普通的队列没有任何区别
PriorityQueue<Integer> queue = new PriorityQueue<Integer>();
//添加元素
queue.offer(e)
//堆顶元素出堆
e = queue.pop()
//查看堆顶元素
e = queue.peek();
好了,预习完了PriorityQueue的基础知识,让我们来看看这个算法应该怎么实现吧
简单来说:
创建一个最大堆,那么堆顶的元素是堆中最大值,然后先将给定的数组中的k个数字放入堆中,然后数组中剩下的数字,依次和堆顶的元素比较,如果比堆顶元素小,则将堆顶元素出堆,将当前元素放入堆中。经过调整后,堆顶的元素又成了堆中最大的 元素,然后用数组中的下一个元素继续和堆顶元素比较,重复以上操作直至遍历完整个数组。
举个例子:
当前数组,找出3个最小的数:
首先将数组中前三个数插入最大堆
然后从第k+1(第4个元素开始向后,依次和堆顶元素比较)
3<14,将堆顶元素弹出,将3插入堆底并按照规则调整堆的结构,使最中的最大值保持在堆顶。
然后接着比较元素中下个元素和堆顶元素。
小于堆顶元素,堆顶元素弹出,数组中当前元素插入堆底并调整堆结构。
继续比较,发现数组中当前元素仍需小于堆顶元素,重复上述操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y2LUqVvz-1633490687139)(/Users/liushanlin/Library/Application%20Support/typora-user-images/image-20211004112110038.png)]
当遍历完整个数组时,我们就得到了数组中最小的3个值:2,3,4
代码如下:
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
int[] res = new int[k];
if(k == 0){return res;}
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>(){
public int compare(Integer num1,Integer num2){
return num1 - num2;
}
});
for (int i=0;i<k;i++){
queue.offer(arr[i]);
}
for(int i=k ;i<arr.length;i++){
if(arr[i] < queue.peek()){
queue.poll();
queue.offer(arr[i]);
}
}
for(int i=0; i<k; i++){
res[i] = queue.poll();
}
return res;
}
}
- 方法二:快速排序
之前发过一篇讲快速排序的方法:912. 排序数组(快速排序)
我们根据快速排序的性质可知,只要我们找到这个数组中第k
大值x
,经过快速排序后,则x
前面的元素均大于x
,x
后面的元素均小于x
。那么只要我们取数组中的前k
个值,即为数组中最小的k
个值。
当然我们不需要像快速排序那样每次对分割后的两个子数组都进行递归。我们只需要:
- 如果当前放到最终位置的元素的位置
index>k
,那么我们对[left,index-1]
进行递归快速排序 - 如果当前放到最终位置的元素的位置
index<k
,那么我们对[index+1,right]
进行递归快速排序 - 如果当前放到最终位置的元素的位置
index=k
,那么递归结束
最终数组中的前k个数就是我们要的结果,代码如下:
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
if(arr.length == 0 || k <= 0){return new int[0];}
quickSort(arr,0,arr.length-1,k-1);
int[] res = new int[k];
for(int i=0;i<k;i++){
res[i] = arr[i];
}
return res;
}
public void quickSort(int[] nums,int left,int right,int k){
if(left < right){
int index = partition(nums,left,right);
if(index == k){return ;}
else if(index < k){quickSort(nums,index+1,right,k);}
else if(index > k ){quickSort(nums,left,index-1,k);}
}
}
public int partition(int[] nums, int left, int right){
Random random = new Random();
int randomIndex = random.nextInt(right-left+1)+left;
swap(nums,left,randomIndex);
int privot = nums[left];
int lt = left;
for(int i=left+1;i<=right;i++){
if(lt<=right && nums[i]<privot){
lt++;
swap(nums,i,lt);
}
}
swap(nums,lt,left);
return lt;
}
public void swap(int[] nums, int i ,int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
下面是leetCode解答中,最大堆方法的运行数据:
下面是leetCode解答中,快速排序方法的运行数据:
可以看到,在空间占用大小几乎相等的情况下,快速排序的方法比最大堆的方法快了数倍,效率很高。