[剑指Offer]最小的k个数 -------三种不同的解法

题目描述:

输入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)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值