题目描述:
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,
输入描述:
int[] input:待搜索的数组
int k:需要找出最小的数量
输出描述:
当k大于input长度时,返回空(非null)
当k小于input长度时,返回input中最小的k个数字
解法一:
利用冒泡排序的思路,每一次排序都将剩余区间最小的数放到未排序区间的前面,进行K轮排序
代码展示:
package com;
import java.util.ArrayList;
/**
* package:com
* Description:冒泡排序思路
* @date:2019/8/14
* @Author:weiwei
**/
public class GetLeastNumbers1 {
/**
* 利用冒泡排序思想,进行 k 轮排序,就将 k 个 最小的数字排到最前面
* 然后返回
* @param nums
* @param k
* @return
*/
public ArrayList<Integer> getLeastNumbers(int[] nums,int k){
ArrayList<Integer> list = new ArrayList<Integer>();
int lens = nums.length;
if(lens == 0 || nums == null || k <= 0 || k > lens){
return list;
}
int temp;
for(int i = 0;i<k;i++){
for(int j = i+1;j<lens;j++){
if(nums[i] > nums[j]){
temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
list.add(nums[i]);
}
return list;
}
public static void main(String[] args) {
int [] nums = {7,3,8,2,9,1,6,4};
GetLeastNumbers1 getLeastNumbers1 = new GetLeastNumbers1();
ArrayList<Integer> list = getLeastNumbers1.getLeastNumbers(nums,4);
for(int i = 0;i<list.size();i++){
System.out.println(list.get(i));
}
}
}
优点:思路简单,清晰易懂
缺点:改变了原来输入数组,时间复杂度大,为O(kn)
解法二:
O(n)的算法,只有当我们可以修改输入的数组是可用
- 利用快速排序划分的思想,每一次划分就会有一个数字位于以数组从小到达排列的的最终位置index;
- 位于index左边的数字都小于index对应的值,右边都大于index指向的值;
- 所以,当index > k-1时,表示k个最小数字一定在index的左边,此时,只需要对index的左边进行划分即可;
- 当index < k - 1时,说明index及index左边数字还没能满足k个数字,需要继续对k右边进行划分;
代码展示:
package com;
import java.util.ArrayList;
/**
* package:com
* Description:利用快排和二分查找的思想 最小的 k 个数
* @date:2019/8/14
* @Author:weiwei
**/
public class GetLeastNumbers {
/**利用快排的思想,先将数组以index为基准分为一大一小两个子区间
* 位于左边的值都比 index 小,右边的数都比 index 大
*当 index > k-1,说明k个最小的数一定都在index的左边,此时只需要对左边区间进行划分
*当 index < k-1,说明 index 及 index 左边的数字还没能满足k个数字,需要继续对k右边进行划分
* @param nums 数组
* @param k 最小的k个数
* @return 返回 list
*/
public ArrayList<Integer> getLeastNumbers(int[] nums, int k){
ArrayList<Integer> list = new ArrayList<Integer>();
int lens = nums.length;
if( lens == 0 || nums == null || k <= 0 || k > lens){
return list;
}
int start = 0;
int end = lens - 1;
int index = partiton(nums,start,end);
while( index != k-1){
if(index > k-1){ //这一部分类似二分查找,如果 index >k-1
end = index - 1; //就让end = index-1,淘汰后半部分数组,只处理左边的数组
index = partiton(nums,start,end); //对左边的小区间再次进行partition
}else{
start = index + 1; //否则去处理右边的区间
index = partiton(nums,start,end); //对右边的小区间进行partition
}
}
for(int i = 0;i<k;i++){
list.add(nums[i]);
}
return list;
}
/**
*
* @param nums
* @param start partition的开头,也就是数组的第一个元素位置
* @param end partition 的结尾,也就是数组的最后一个元素
* @return 返回 start
*/
private int partiton(int[] nums, int start, int end) {
int pivot = nums[start];
while( start < end){
while(start < end && pivot <= nums[end]){
--end;
}
swap(nums,start,end);
while(start > end && pivot >= nums[start]){
--start;
}
swap(nums,start,end);
}
return start;
}
/**
*进行交换
* @param nums
* @param start
* @param end
*/
private void swap(int[] nums, int start, int end) {
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
}
public static void main(String[] args) {
int[] nums = {7,3,8,2,9,1,6,4};
GetLeastNumbers getLeastNumbers = new GetLeastNumbers();
ArrayList<Integer> list = getLeastNumbers.getLeastNumbers(nums,4);
for(int i = 0;i<list.size();i++){
System.out.println(list.get(i));
}
}
}
优点:时间复杂度较小:为O(n)
缺点:修改了原来输入数组
解法三:
O(nlogk)的算法,特别适合处理海量数据
我们可以创建一个容量为k的数据容器来存储最小的k个数字,接下来我们每次从输入的n个整数中读入一个数。如果容器中已有的数字少于k个,则直接把这次读入的整数放入容器之中;如果容器中已有k个数字了(即容器满了),我们就不能再插入数字了,只能去替换容器中已有的数字。替换的规则是,我们拿待插入的数字和容器中k个数字中的最大值进行比较,如果大于容器中的最大值,则抛弃这个整数,否则用这个整数去替换这个数字。
故容器满了之后,我们需要做3件事:
- 一是在k个整数中找到最大数;
- 二是有可能在这个容器中删除这个最大数;
- 三是有可能会在这个容器中插入一个新数字。
用二叉树实现这个容器,我们能在O(logk)时间内实现这三步操作。因此对于n个数字而言,总的时间效率就是O(nlogk)。
容器的实现用数据结构中的最大堆,因为其根结点的值永远是最大的结点值。我们用红黑树来实现我们的最大堆容器。而TreeSet类实现了红黑树的功能,它的底层是通过TreeMap实现的,TreeSet中的数据会按照插入数据自动升序排序。我们只需要将数据放入TreeSet中,其就会为我们实现排序。
代码展示:
package com;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.TreeSet;
/**
* package:com
* Description:建大堆 最小的 K 个数
* @date:2019/8/14
* @Author:weiwei
**/
public class GetLeatstNumbers2 {
/**
* 创建一个容量为 k 的容器来存放这个 k 个数,接下来从 n 个数中读取一个数,去和容器里面的数做比较
* 如果容器里未满 k 个数,就将读取的这个数放进去,如果容器满了(已经有 k 个数)
* 就替换容器中的数,替换的规则是:当前读取到的数如果比容器里面最大的数小,就将容器中最大的数移除,再将这个数
* 插进去,如果这个数比容器里面最大的数还要大,就抛弃这个数
* 所以当容器满了,我们需要做三件事:
* 1.在 k 个数中找到最大的数
* 2.将最大的数移除
* 3.插入一个更小的新的数
* 用二叉树实现这个容器,能在 O(nlogn)的时间复杂度在完成
* 容器在数据结构中用最大堆,其根结点永远是最大的数,用红黑树来实现最大堆容器,TreeSet实现了红黑树的功能
* 底层用TreeMap实现,Treeset 中的数会自动按升序排序,只要将数据放入TreeSet就会帮我们完成排序
* @param nums
* @param k
* @return
*/
public ArrayList<Integer> getLeatstNumbers2(int[] nums,int k){
ArrayList<Integer> list = new ArrayList<Integer>();
int lens = nums.length;
if(lens == 0 || nums == null || k <= 0 || k > lens){
return list;
}
TreeSet<Integer> kSet = new TreeSet<Integer>();
for(int i = 0; i< lens;i++){
if(kSet.size() < k){
kSet.add(nums[i]);
}else if( nums[i] < kSet.last()){
kSet.remove(kSet.last());
kSet.add(nums[i]);
}
}
Iterator<Integer> iterator = kSet.iterator();
while(iterator.hasNext()){
list.add(iterator.next());
}
return list;
}
public static void main(String[] args) {
int[] nums = {7,3,8,2,9,1,6,4};
GetLeatstNumbers2 getLeatstNumbers2 = new GetLeatstNumbers2();
ArrayList<Integer> list = getLeatstNumbers2.getLeatstNumbers2(nums,4);
for(int i = 0;i<list.size();i++){
System.out.println(list.get(i));
}
}
}
优点:(1)没有修改输入的数据;
(2)这个算法适合海量数据的输入。
算法的题目可以改成从海量数据中找出k个最小的数。第一个利用快速排序的思想的时间复杂度为O(n),后一个借助于一个容器的思想的时间复杂度为O(nlogk)。