数据结构与算法--丑数

找出排在第n位大的丑数
  • 丑数:我们将只包含质因子 2,3,5的数称为丑数(ugly Number)。求按从小到大的熟悉怒排列的低1500 个丑数。例如6,8 都是丑数,但是14 不是丑数,因为他包含质因子7。1 是基础丑数

  • 解法一:

    • 最直观的解法,从1 开始逐个判断每个整数是否丑数,
    • 所谓的一个数m是另一个数n的因子,那么n就能被m整除。也就是m%n == 0.
    • 根据丑数定义,丑数只能被2,3,5 整除。也就是说如果一个数能被2 整除,我们可以将它连续除以2。3 和5 也是类似,最后我们得到的是1 ,那么这个数就是丑数。
  • 解法一的实现如下:

public class FindUglyNumber {

    public static void main(String[] args) {
        Long time = System.currentTimeMillis();
        System.out.println(getNumberOfUglyNumber(900));
        Long time_1 = System.currentTimeMillis();
        System.out.println(time_1 - time);
    }
    
    /**
     * 方法一:逐个判断从1 开始的数字是否丑数
     * */
    public static Integer getNumberOfUglyNumber(Integer position){
        if(position <= 0){
            return -1;
        }
        Integer count = 0;
        for (int i =0; true;i++){
            if(isUglyNumber(i)){
                count++;
                if(count.equals(position)){
                    return i;
                }
            }
        }
    }

    /**
     * 判断是否丑数
     * */
    public static boolean isUglyNumber(Integer number) {
        if(number <= 0){
            return false;
        }
        while (number % 2 == 0) {
            number /= 2;
        }
        while (number % 3 == 0) {
            number /= 3;
        }
        while (number % 5 == 0){
            number /= 5;
        }
        return number == 1;
    }
}
  • 以上经过测试在求第1500 个丑数的耗时大概需要11秒以上,这个在线上是不可用额,那么我们应该有更好的优化方案

  • 方法二:

    • 既然要逐个判断,如上实现中的isUglyNumber,其中有一个循环计算的过程,那么我们能否减少计算的步骤,直接判断出是否丑数
    • 我们将已找出的丑数放在一个数组中,利用这个数组判断
    • 当找出一个丑数s 时候,那么在这个丑数基础上s * 2,s * 3,s * 5,必然也是丑数
    • 同理,如果有一个数 k, k/2,k/3,k/5 包含在已有的丑数中那么这个数也必然也是丑数
    • 我们利用如上数学原来,改造丑数的判断方法,在每一次计算后判断是否在已有丑数中,就可以减少计算量
    • 如下实现
/**
 * @author liaojiamin
 * @Date:Created in 10:57 2021/6/9
 */
public class FindUglyNumber {

    public static void main(String[] args) {
        Long time_1 = System.currentTimeMillis();
        System.out.println(getNumberOfUglyNumber_1(900));
        Long time_2 = System.currentTimeMillis();
        System.out.println(time_2 - time_1);
    }

    /**
     * 方法二:保存之前判断过的丑数,丑数* (2/3/5) 必然还是丑数
     * 更慢
     * */
    public static Integer getNumberOfUglyNumber_1(Integer position){
        if(position <=0 ){
            return -1;
        }
        List<Integer> uglyNumberList = new ArrayList<>();
        Integer count =0;
        for (int i=0;true;i++){
            if(isUglyNumber(i, uglyNumberList)){
                count ++;
                if(count.equals(position)){
                    return i;
                }
                uglyNumberList.add(i);
            }
        }
    }

    /**
     * 判断是否丑数
     * */
    public static boolean isUglyNumber(Integer number, List<Integer> uglyNumberList) {
        if(number <= 0){
            return false;
        }
        while (number % 2 == 0) {
            number /= 2;
            if(uglyNumberList.contains(number)){
                return true;
            }
        }
        while (number % 3 == 0) {
            number /= 3;
            if(uglyNumberList.contains(number)){
                return true;
            }
        }
        while (number % 5 == 0){
            number /= 5;
            if(uglyNumberList.contains(number)){
                return true;
            }
        }
        return number == 1;
    }
}
  • 如上方案运行后结果并没有如我们预期得到优化,时间反而更长了,基本要n分钟才能出结果

  • 问题在循环运算的过程中我们每次运算都会判断结果是否包含在已有丑数中

  • 但是 List的content方法的耗时比实际运算的时间多多了,因此它的耗时反而增加了

  • 本想空间换时间,但是算法应该没写好,是一次失败的优化,接着找更优的方案

  • 方案三:

    • 以上两种方案都是对数字逐个判断,方案二中的思路是可以借鉴的,是否还有更优的解,无需逐个判断我们通过算法求出还存在的丑数
    • 在方案二中我们提出了这个数学规律:当找出一个丑数s 时候,那么在这个丑数基础上s * 2,s * 3,s * 5,必然也是丑数
    • 有如上规则的话,我们就有办法通过这个规则找出还没有界别出的丑数,但是难点在于我们需要按顺序排列
    • 按顺序找的话,我们只能找出比当前找出的最大丑数 大的最小丑数,比较拗口,举例如下
    • 当前丑数有{1,2,3,4} 那么我们能通过2,3,5 的乘法找出2,3,4,6,10 ,但是符合当前要求的只有5
    • 因此我们用如下规则查询:
      • 将2,3,5 与现有丑数数组中所有的数字依次做乘法,并且将大于当前最大丑数max的值记录下来记为k
      • 找出记录k中最小的值,就是我们需要找的下一个丑数
    • 经过如上分析有如下代码:
/**
 * @author liaojiamin
 * @Date:Created in 10:57 2021/6/9
 */
public class FindUglyNumber {

    public static void main(String[] args) {
        Long time_2 = System.currentTimeMillis();
        System.out.println(getNumberOfUglyNumber_2(1500));
        System.out.println(System.currentTimeMillis() - time_2);

    }

    /**
     * 方法三:不逐个判断,我们先将1 是基础丑数放入已经查找数组中,此时最大丑数是1 记为max,
     * 因为 uglyNum * (2/3/5) 还是uglyNum,我们直接找正好大于 当前max的最小丑数,
     * 此时将1 分别* (2/3/5) 得到 (2/3/5),其中最小值2,此时 max = 2
     * 以此类推
     * */
    public static Integer getNumberOfUglyNumber_2(Integer position){
        if(position <= 0){
            return -1;
        }
        Integer[] uglyArray = new Integer[position];
        uglyArray[0] = 1;
        Integer nowPosition = 1;
        Integer max = uglyArray[0];
        while (nowPosition < position){
            Integer newMax = findMinUgly(max, uglyArray, nowPosition);
            max = newMax;
            uglyArray[nowPosition] = newMax;
            nowPosition++;
        }

        return uglyArray[position - 1];

    }

    public static Integer findMinUgly(Integer max, Integer[] uglyArray, Integer nowPosition){
        Integer min = Integer.MAX_VALUE;
        for (int i = 0; i< nowPosition ; i++) {
            Integer temp_2 = uglyArray[i] * 2;
            if(temp_2 > max && temp_2 < min){
                min = temp_2;
            }
            Integer temp_3 = uglyArray[i] * 3;
            if(temp_3 > max && temp_3 < min){
                min = temp_3;
            }
            Integer temp_5 = uglyArray[i] * 5;
            if(temp_5 > max && temp_5 < min){
                min = temp_5;
            }
        }
        return min;
    }
}

  • 经过如上优化后,第1500 位丑数的求解控制在46毫秒以内这基本上可以算合格的解法了
  • 但是其实还是有优化空间的,也就是我们在findMinUgly中循环做乘法运算求最小丑数的时候,有一部分乘法是完全没必要的
  • 例如当我们当前的最大丑数是k的时候,在2,3,5与第 n个丑数做乘法的结果中 5*n < k
  • 其中5*n是n 以及n之前 所有丑数能计算出的最大丑数还是 小于 k,就说明n 之前的所有乘法都是不必要的
  • 通过对这个点的优化可以节省很多计算时间。
  • 小优化如下:
/**
 * @author liaojiamin
 * @Date:Created in 10:57 2021/6/9
 */
public class FindUglyNumber {

    public static void main(String[] args) {
        Long time_2 = System.currentTimeMillis();
        System.out.println(getNumberOfUglyNumber_2(1500));
        System.out.println(System.currentTimeMillis() - time_2);

    }

    /**
     * 方法三:不逐个判断,我们先将1 是基础丑数放入已经查找数组中,此时最大丑数是1 记为max,
     * 因为 uglyNum * (2/3/5) 还是uglyNum,我们直接找正好大于 当前max的最小丑数,
     * 此时将1 分别* (2/3/5) 得到 (2/3/5),其中最小值2,此时 max = 2
     * 以此类推
     * */
    public static Integer getNumberOfUglyNumber_2(Integer position){
        if(position <= 0){
            return -1;
        }
        Integer[] uglyArray = new Integer[position];
        uglyArray[0] = 1;
        Integer nowPosition = 1;
        Integer max = uglyArray[0];
        while (nowPosition < position){
            Integer newMax = findMinUgly(max, uglyArray, nowPosition);
            max = newMax;
            uglyArray[nowPosition] = newMax;
            nowPosition++;
        }

        return uglyArray[position - 1];

    }

    public static Integer findMinUgly(Integer max, Integer[] uglyArray, Integer nowPosition){
        Integer min = Integer.MAX_VALUE;
        for (int i = 0; i< nowPosition ; i++) {
            Integer temp_5 = uglyArray[i] * 5;
            if(temp_5 < max){
                continue;
            }
            if(temp_5 > max && temp_5 < min){
                min = temp_5;
            }
            Integer temp_2 = uglyArray[i] * 2;
            if(temp_2 > max && temp_2 < min){
                min = temp_2;
            }
            Integer temp_3 = uglyArray[i] * 3;
            if(temp_3 > max && temp_3 < min){
                min = temp_3;
            }

        }
        return min;
    }
}

  • 经过如上一个小的优化点,可以将1500 位的查找控制在30 毫秒以内

上一篇:数据结构与算法–将数组排成最小的数
下一篇:数据结构与算法–第一个只出现一次的字符

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值