最小的k个数

题目描述

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,


这道题最简单的思路就是把数组排好序,然后取前面的k个数就好了,但是这种算法的时间复杂度为O(N*logN),所以我们需要利用更快的方法来解决这个问题。

解法一:时间复杂度为O(N)的算法,只有当我们可以修改数组的时候才可以用
基于快排的思想,如果基于数组的第k个数字来调整,则使得比第k个数字小的所有数字都位于数组的左边,比第k个数字大的所有数字都位于数组的右边。这样调整之后,位于数组中左边的k个数字就是最小的k个数字了(不一定是排序的),所以之后我们还需要为其排序一次。
以下为该方法的参考代码:

class Solution1
{
    /// <summary>
    /// 输出一个集合,集合包含传入的数组里最小的k个数
    /// </summary>
    /// <param name="input">传入的数组</param>
    /// <param name="k">要传回的最小数的个数</param>
    /// <returns>最小数排序后的集合</returns>
    public List<int> GetLeastNumbers_Solution(int[] input, int k)
    {
        List<int> list = new List<int>();

        //判断输入是否合法
        if (input == null || input.Length < 1 || k < 0 || k > input.Length)
            return list;

        int start = 0, end = input.Length - 1;
        //计算第一个分组的最后一位的下标
        int index = Partition(ref input, start, end);
        //找到分组N次后左边分组的个数只剩下k个数
        while (index != k - 1)
        {
            //如果当前左边分组的个数多余k,那么就再把左边分一次
            if (index > k - 1)
            {
                end = index - 1;
                index = Partition(ref input, start, end);
            }
            //如果右边分组的个数少于k个,那么就把右边分组的元素分一部分给左边分组
            else
            {
                start = index + 1;
                index = Partition(ref input, start, end);
            }
        }

        //把最小的k个数添加到集合,再排序输出
        for (int i = 0; i < k; i++)
        {
            list.Add(input[i]);
        }

        list.Sort();

        return list;
    }

    /// <summary>
    /// 对数组进行分组
    /// </summary>
    /// <param name="input">原数组</param>
    /// <param name="start">分组的起始位置</param>
    /// <param name="end">分组的结束位置</param>
    /// <returns>返回中位数的下标</returns>
    private int Partition(ref int[] input, int start, int end)
    {
        int index = start + 1;
        for (int i = index; i <= end; i++)
        {
            if (input[i] < input[start])
            {
                Swap(ref input, i, index);
                index++;
            }
        }
        Swap(ref input, start, index - 1);
        return index - 1;
    }

    /// <summary>
    /// 交换数组中两个数的位置
    /// </summary>
    /// <param name="input">原数组</param>
    /// <param name="index1">交换数1的下标</param>
    /// <param name="index2">交换数2的下标</param>
    private void Swap(ref int[] input, int index1, int index2)
    {
        int temp = input[index1];
        input[index1] = input[index2];
        input[index2] = temp;
    }
}

解法二:时间复杂度为O(N*logK)的算法,特别适合海量数据的处理
创建一个容器大小为k的容器,然后每次从输入的n个整数中读入一个数。如果容器中已有的数字少于k个,则直接把这次读入的整数放入容器之中;如果,容器中已经有了k个数字,那么就比较替换,拿每次待插入的数字和容器中的最大值进行比较。如果待插入的值比当前已有的最大值小,那么用这个数替换最大值,如果待插入的值没有当前已有的最大值小,比它要大,那么这个数不可能是最小的k个整数之一,于是我们可以抛弃这个整数。

因此,当容器满了之后,我们需要做三件事:
1. 在k个整数中找到最大数;
2. 在这个容器中删除最大数;
3. 插入一个新的数字。

这样想了之后会发现,如果用一个二叉树来实现这个容器就好了,那么我们能在O(logK)时间内实现这3步操作。因此,对于n个输入数字而言,总的时间效率就是O(n*logk)。
我们可以选择不同的二叉树来实现这个数据容器。由于每次都需要找到k个整数中的 最大值,我们很/容易就能想到最大堆。在最大堆中,根节点的值总是大于它的子树中任意节点的值。于是我们每次可以在O(1)时间内得到已有的k个数字中的最大值,但需要O(logk)时间完成删除及插入操作。
我们自己从头实现一个最大堆需要一定的代码,这在面试期间是很难完成的。我们还可以采用红黑树来实现我们的容器。红黑树通过把节点分为红黑两种颜色并根据一些规则确保树在一定程度上是平衡的,从而保证在红黑树中的查找、删除和插入操作都只需O(logk)时间。在STL中,set和multiset都是基于红黑树实现的,在可以使用STL的情况下,直接使用即可。
以下为参考代码:

using System.Collections.Generic;
using System.Linq;

 class Solution2
 {
     /// <summary>
     /// 输出一个集合,集合包含传入的数组里最小的k个数
     /// </summary>
     /// <param name="input">传入的数组</param>
     /// <param name="k">要传回的最小数的个数</param>
     /// <returns>最小数排序后的集合</returns>
     public List<int> GetLeastNumbers_Solution(int[] input,int k)
     {
         List<int> list = new List<int>();

         if (input == null || input.Length < 1 || k <= 0 || k > input.Length)
             return list;

         for(int i = 0; i < input.Length; i++)
         {
             if (list.Count < k)
                 list.Add(input[i]);
             else
             {
                 int max = list.Max();
                 if ( max> input[i])
                 {
                     list.Remove(max);
                     list.Add(input[i]);
                 }
             }
         }

         list.Sort();
         return list;
     }
 }

基于函数Partition的第一种解法平均时间复杂度是O(N),比第二种方法要快,但是很明显,他有一个缺陷就是会改变输入的数组。
第二种方法虽然慢一点,但他有两个优点:
1、没有修改输入的数据,所有的操作都是在容器中进行的;
2、该算法适合海量数据的输入。假设题目是要求从海量的数据中找出K个最小的数,由于内存的大小是有限的,所以不能把这些海量的数据都一次性全部载入内存。这个时候就可以每次从辅存只读一个数据,根据GetLeastNumbers_Solution的方式判断是不是需要放入容器list即可。他最适合的情形就是n很大并且k较小的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值