27 最小的k个数: 输入n个整数,找出其中最小的k个数。

本文介绍了解决寻找数组中最小k个数的问题,提供了三种算法:基于Partition的O(n)时间复杂度算法、使用最大堆实现的O(nlogk)算法以及利用TreeSet的算法。详细解析了每种算法的实现过程和优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、题目
输入n个整数,找出其中最小的k个数。
例子说明:
例如输入4 、5 、1、6、2、7、3 、8 这8 个数字,则最小的4 个数字是1 、2、3 、4

解法一:O(n)时间算法,只有可以修改输入数组时可用。
可以基于Partition函数来解决这个问题。如果基于数组的第k个数字来调整,使得比第k个数字小的所有数字都位于数组的左边,比第k个数字大的所有数字都位于数组的右边。这样调整之后,位于数组中左边的k个数字就是最小的k 个数字(这k 个数字不一定是排序的〉。

/**
     * 题目: 输入n个整数,找出其中最小的k个数。
     * 【第一种解法】
     * @param input  输入数组
     * @param output 输出数组
     */
    public static void getLeastNumbers(int[] input,int k) {
    		int[] output=new int[k];

        while(input==null || k<=0 || k>input.length) {
            return output;
        }

        int start = 0;
        int end = input.length - 1;
        int index = partition(input, start, end);
        while (index != k-1) {
            if (index < k-1) {
                start = index + 1;    //前面筛选的不够,后面的接着筛选
            } else {
                end = index - 1;
            }
            index = partition(input, start, end);
        }

        System.arraycopy(input, 0, output, 0, output.length);
    }

    /**
     * 分区算法
     *
     * @param input 输入数组
     * @param start 开始下标
     * @param end   结束下标
     * @return 分区位置
     */
    private static int partition(int[] input, int start, int end) {
        int tmp = input[start];

        while (start < end) {
            while (start < end && input[end] >= tmp) {
                end--;
            }
            input[start] = input[end];

            while (start < end && input[start] <= tmp) {
                start++;
            }
            input[end] = input[start];
        }

        input[start] = tmp;
        return start;
    }

解法二: O(nlogk)的算法,精剧适合处理海量数据。
思路三
可以先创建一个大小为k的数据容器来存储最小的k个数字,从输入的n个整数中一个一个读入放入该容器中,如果容器中的数字少于k个,按题目要求直接返回空;
如果容器中已有k个数字,而数组中还有值未加入,此时就不能直接插入了,而需要替换容器中的值。按以下步骤进行插入:
先找到容器中的最大值;
将待查入值和最大值比较,如果待查入值大于容器中的最大值,则直接舍弃这个待查入值即可;如果待查入值小于容器中的最小值,则用这个待查入值替换掉容器中的最大值;
重复上述步骤,容器中最后就是整个数组的最小k个数字。
对于这个容器的实现,我们可以使用最大堆的数据结构,最大堆中,根节点的值大于它的子树中的任意节点值,
我们可以使用一个大顶堆在O(logk)时间内实现这三步操作

当涉及到频繁查找和替换最大最小值时,二叉树是非常合适的数据结构,要能想到堆和二叉树。

方式一:PriorityQueue
PriorityQueue是个基于优先级堆的极大优先级队列。
此队列按照在构造时所指定的顺序对元素排序,既可以根据元素的自然顺序来指定排序(参阅 Comparable),
也可以根据 Comparator 来指定,这取决于使用哪种构造方法。优先级队列不允许 null 元素。

原文链接: https://www.cnblogs.com/zzmher/p/7730700.html

import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.Comparator;
public class Solution {
        public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
            ArrayList<Integer> result = new ArrayList<Integer>();
            int length = input.length;
            if (k > length || k == 0) {
                return result;
            }
            PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(k, new Comparator<Integer>() {

                @Override
                public int compare(Integer o1, Integer o2) {
                    return o2.compareTo(o1);
                }
            });
            for (int i = 0; i < length; i++) {
                if (maxHeap.size() != k) {
                    maxHeap.offer(input[i]);
                } else if (maxHeap.peek() > input[i]) {
                    Integer temp = maxHeap.poll();
                    temp = null;
                    maxHeap.offer(input[i]);
                }
            }
            for (Integer integer : maxHeap) {
                result.add(integer);
            }
            return result;
        }
    }

方式二:

Java中的TreeSet类实现了红黑树的功能,它底层是通过TreeMap实现的,TreeSet中的数据会按照插入数据自动升序排列(按自然顺序)。因此我们直接将数据依次放入到TreeSet中,数组就会自动排序。

缺点就是:
TreeSet不允许重复数据,因为TreeSet的底层是TreeMap实现,是将TreeSet添加的内容作为TreeMap的key值来存储,也就不能存在重复数据。由于这种限制,这就对我们的输入数组有要求,但我们可以通过自己实现最大堆或优化TreeSet来实现兼容存在重复数字的情况。

原文链接:https://blog.csdn.net/shakespeare001/article/details/51280814

public static ArrayList<Integer> GetLeastNumbers_Solution3(int [] input, int k) {
        if(input == null)
            return null;
        ArrayList<Integer> list = new ArrayList<Integer>(k);
        if(k > input.length)
            return list;
        TreeSet<Integer> tree = new TreeSet<Integer>();
        for(int i = 0 ; i < input.length; i++){
            tree.add(input[i]);
        }
        int i = 0;
        for(Integer elem : tree){
            if(i >= k)
                break;
            list.add(elem);
            i++;
        }
        return list;
    }

方式三:
原文链接:
https://www.cnblogs.com/yongh/p/9944155.html

  /**
     * 最大堆
     *
     * @param input
     * @param k
     * @return
     */
    public static ArrayList<Integer> GetLeastNumbers_Solution2(int[] input, int k) {
        ArrayList<Integer> leastNumbers = new ArrayList<Integer>();
        while (input == null || k <= 0 || k > input.length)
            return leastNumbers;
        int[] numbers = new int[k];  //用于放最小的k个数
        for (int i = 0; i < k; i++)
            numbers[i] = input[i];//先放入前k个数
        for (int i = k / 2 - 1; i >= 0; i--) {
            adjustHeap(numbers, i, k - 1);//将数组构造成最大堆形式
        }
        for (int i = k; i < input.length; i++) {
            if (input[i] < numbers[0]) { //存在更小的数字时,将第一个替换,然后再排序
                numbers[0] = input[i];
                adjustHeap(numbers, 0, k - 1);//
            }
        }
        for (int n : numbers)
            leastNumbers.add(n);
        return leastNumbers;
    }

    private static void adjustHeap(int[] arr, int start, int end) {//最大堆
        int temp = arr[start];//记录根节点
        int child = start * 2 + 1;  //从0号排序,左孩子就是2*i+1
        while (child <= end) {
            if (child + 1 <= end && arr[child + 1] > arr[child])//右孩子>左孩子,选择右孩子
                child++;
            if (arr[child] < temp)//如果左子孩子<根节点 结束,本身就是最大堆
                break;
            arr[start] = arr[child];//
            start = child;//根节点
            child = child * 2 + 1;//左孩子
        }
        arr[start] = temp;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值