JZ40 最小的K个数

JZ40 最小的K个数

描述

给定一个长度为 n 的可能有重复值的数组,找出其中不去重的最小的 k 个数。例如数组元素是4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4(任意顺序皆可)。

数据范围:0 ≤ k,n ≤ 10000,数组中每个数的大小0 ≤ val ≤10000

要求:空间复杂度 O(n),时间复杂度 O(nlogn)

示例1

输入:

[4,5,1,6,2,7,3,8],4 

返回值:

[1,2,3,4]
// 说明:返回最小的4个数即可,返回[1,3,2,4]也可以

示例2

输入:

[1],0

返回值:

[]

示例3

输入:

[0,1,2,1,2],3

返回值:

[0,1,1]

解析

很容易理解的求 TOPK 小的问题,比较容易想到和实现的方式就是对输入的数组递增地进行排序,当数组有序时的前 K 个数就是最小的 K 个数。不过这种方式过于低级,时间复杂度取决于排序的时间复杂度,失去了写这道题的意义。

除了直接排序外,还可以借助快速排序的思想解决这个问题。

先复习一下快速排序,快速排序的一次排序步骤为:

  1. 先在要排序的数组中选取一个数作为基准数(如何选也是个问题),设置临时变量保存该基准数;
  2. 设置 low 和 high 两个标记指针分别在数组开头和末尾,当 low < high 时进行排序;
  3. 若 high 指针指向的数比基准数大,则 high 减小,直到指向的数比基准数小时停止;
  4. 若 low 指针指向的数比基准数小,则 low 增加,直到指向的数比基准数大时停止;
  5. 交换 low 和 high 指向的数,完成一次交换,此时若 low < high,重复 3、4、5 步骤;
  6. 当 low = high 时,循环结束,它们指向的位置就是基准数在数组中的准确位置,一次排序完成;
  7. 交换基准数与当前 low、high 指向的数,完成基准数归位;
  8. 针对基准数左右两边递归应用快速排序进行排序,直至数组有序,递归停止的标记为初始的 low > high。

快速排序的思想就是针对每个基准数,找到它在数组中的准确位置,使其左边都小于它,右边都大于它。

代码清单(快速排序)

public class Solution {
    public static void QuickSort(int[] arr,int start,int end) {
        if(start > end)
            return;
        // 左右的两个标记
        int low = start, high = end;
        // 基准数
        int index = arr[start];
        while(low < high){
            // 从右边开始,寻找比基准数小的数的位置
            while(low < high && arr[high] >= index)
                high--;
            // 再到左边,寻找比基准数大的数的位置
            while(low < high && arr[low] <= index)
                low++;
            // 交换一大一小的两数
            if(low < high){
                int temp = arr[low];
                arr[low] = arr[high];
                arr[high] = temp;
            }
        }
        // 循环结束,即 low == high,此时 arr[low] 位置就是基准数在数组中的准确位置!
        // 将当前位置的数与基准数进行交换,这个位置的数一定比基准数小,或就是基准数本身
        // 因为循环中 high 先移动,必定位于比基准数小的数上,或直接与初始的 low 相遇
        arr[start] = arr[low];
        arr[low] = index;
        QuickSort(arr,start,low-1);
        QuickSort(arr,high+1,end);
    }

}

以上为快速排序的一种实现,基准数选择每次排序的第一个数,循环中让 high 先于 low 移动。这是因为基准数处于数组中的最小处,执行交换的前提是找到一个更小的数(即 high 停止之处),才能保证即使找不到大于基准数的数,本次交换也是合理的。相反,若基准数选择最后一个数,则要让 low 先于 high 移动。

快速排序需要从哪边开始

https://blog.csdn.net/lkp1603645756/article/details/85008715

讲了这么多其实都是在说快速排序,不过搞明白快速排序后,寻找数组中的最小的 K 个数就很简单了:**只要找到位于数组中第 K 个位置的基准数,其左边就是数组中最小的 K 个数了。**只要借助快速排序分区的部分就可以实现了。

代码清单

import java.util.ArrayList;

public class Solution {
    // 借用快速排序分区的方法 Partition
    // 基于第 K 个位置的数 N 进行分区,完成后 N 左边的数都比 N 小,右边的数都比 N 大
    // 此时前 K 个数就是最小的 K 个数了(不一定有序,可以看做快速排序的中间态)
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> result = new ArrayList<Integer>();
        if(input == null || k == 0)
            return result;
        // 直到分区分到第 K 个位置为止
        int index = Partition(input,0,input.length-1);
        // 为什么是 k - 1?
        while(index != k-1){
            // 基准数在位置 K 的右边,对左边再进行分区
            if(index > k-1)
                index = Partition(input,0,index-1);
            // 基准数在位置 K 的左边,对右边再进行分区
            if(index < k-1)
                index = Partition(input,index+1,input.length-1);
        }
        // 找到第 K 个位置的准确数字了,此时它左边就是比他小的 K 个数!
        for(int i = 0;i < k;i++){
            result.add(input[i]);
        }
        return result;
    }
    // 返回当前基准数处于的位置
    public int Partition(int[] input,int low,int high){
        // 取第一个数作为基准数
        int temp = input[low];
        while(low < high){
            // 从右边开始,遇到比基准数小的就移到左边
            while(low < high && input[high] >= temp)
                high--;
            input[low] = input[high];
            // 再到左边,遇到比基准数大的就移到右边
            while(low < high && input[low] <= temp)
                low++;
            input[high] = input[low];
        }
        // 循环结束,即 low == high,此时这个位置就是基准数在数组中的准确位置!
        input[low] = temp;
        return low;
    }
}

其中的 Partition 方法是快速排序的另一种具体实现方法,在排序过程中会修改基准数的位置,之前的那种实现则不会,不过都能实现快速排序就对了。

此外,还有一个问题容易忽视:寻找的基准数只要位于 K-1 上即可而不用位于 K 上。

先说问题:数组下标从0开始,若输入数组包含8个数,是没有下标为8的位置的,此时若 K 为8且程序要寻找位于 8 上的基准数,就会产生越界访问错误。

那么如果是正常情况呢?基准数处于 K-1 的位置,它左边都是比它小的 K-1 个数(包括0),还差一个数呢?这个数就是基准数本身:基准数右边都是比它大的数,即基准数也是最小的 K 个数之一!

总结

这题真要通过,随便排个序就完成了,非常简单,但这样就没什么意思了。从快速排序开始研究,搞明白了快速排序,还顺便引出了快速排序基准数选择的问题、从哪边开始的问题;再回来看这个题,又产生了不一样的思考,虽然过程很折磨,但还好搞明白了吧。本题还有一种使用大根堆的解法,我是懒得再去研究了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值