题目描述
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。


我开始想的是想的是通过除法,然后逐个判断方式。比如判断一个丑数,由题意知,只包含2、3、5。对于14的判断规则就是先拿掉所有的2的因子,再拿掉所有3的因子,最后拿到5的因子,最后为1,则说明是丑数。

思路1:

哈哈,一个时间复杂度比较高的算法,在牛客上没跑成功。我自己验证的时候感觉没问题

public class ac {
    public static void main(String[] args) {
        System.out.print(GetUglyNumber_Solution(1));
    }    
    public static int GetUglyNumber_Solution(int index) {
            if(index<0)
                return 0;
            int num = 0;
            int cur = 0;
            while(cur < index){
                num++;//+1用来下一个数的判断
                if(isUgly(num))
                    cur++;//维持已知的丑数数量

            }
            return num;
        }        
    public static boolean isUgly(int num){
            while(num%2 == 0)//能被2整除就除以一个2,消除所有2的因子
                num = num/2;
            while(num%3 == 0)//能被3整除就除以一个3,消除所有3的因子
                num = num/3;
            while(num%5 == 0)//能被5整除就除以一个5,消除所有5的因子
                num = num/5;//如果是丑数,去掉这些因子,最后只剩1了
            return (num == 1)?true:false;
        }
}

思路2:改进

得换一个大方向去思考了,用空间换时间。在使用不同的工具以及思路时,都会出现不同的写法。比如书上的方法是用一个大数组来存放这些数据,而其他题解以及讨论中,出现了是三个数组或者指针的方法。
  介绍某一种思路:将丑数存起来,然后利用丑数去计算下一个丑数数。
  过程如下:用3个数记录(指针角色),初始都指向的1。也就是第一个丑数。然后三个指针指向的数分别乘上因子2,3,5,然后进行比较。

  • 初始集合为{1}-> 比较1*2,1*3,1*5 最小值2。需要更新第1个指针,此时指向2(指针++)
  • {1,2} -> 比较2*2,1*3,1*5 最小值3。需要更新第2个指针,让他指向2(指针++)
  • {1,2,3} -> 比较2*2,2*3,1*5 最小值4。需要更新第1个指针,让他指向3(指针++)
  • {1,2,3,4} -> 比较3*2,2*3,1*5 最小值5。需要更新第3个指针,指向2(指针++)
  • {1,2,3,4,5} ->比较3*2,2*3,2*5 最小值6。需要同时更新第1和第2指针,均++
    则第1指针指向4,第2指针指向3
  • {1,2,3,4,5,6} 比较4*2,3*3,2*5 最小值8 更新第1指针,指向5
  • {1,2,3,4,5,6,8} 比较5*2,3*3,2*5 最小值9 更新第2指针 指向4

  除了在6的地方出现了相等,处理方式稍微不一样,其余的都相同。谁乘出来是最小的,最小的加入到集合后,然后就把谁的位置往后挪一个。
  这个方法的原理是,已经有了一部分丑数,且当前拥有的丑数最大值为M,需要找到比M大,又最接近M的丑数。下一个抽数一定是之前的某个丑数乘2,或者乘3,或者乘5的结果。以乘2为例,理当应该将所有的丑数都乘2,这样可以获得若干小于M和大于M的丑数,小于M的丑数已经在我们的集合中了,所以我们需要在大于M的这些数中找到最小值,也就是最接近M的丑数,记录其为n2。同理,在所有丑数乘3的时候,也能找到一个最接近M的丑数,记录为n3,同理有一个n5。所以,我们需要比较n2,n3,n5的最小值。
  而使用i2,i3,i5的意义是,当前所指向的丑数,乘上对应的因子,就是最接近当前集合中,比M大又最接近M的丑数。比如上面分析的{1,2,3,4,5,6} i2指向4,i3指向3,i5指向2。i2指向4,表明当前序列中,乘2比M=6还大,又最接近的数就是4了。同理,i3表示,如果你要通过乘上3,获取要比M大的数,还接近M,那就是3了。因此,正确的维护i2、i3、i5可以将刚刚叙述的所有数都乘2、3、5,并判断这一过程省略。(如果看起来还觉得绕,其实就是因为这个本身就是一个优化版,正常来说,应当维持3个队列(乘2乘3乘5),每个队列中都记录比M大或者小的数,而通过一个while循环判断,获取这个队列中的比M大一丢丢的数是可以做到的,然后在将获取的三个数比较)
  那么,如何正确的维护i2/i3/i5呢,试想,如果当前的某个丑数是通过乘2获取的,比如获取了3乘2,为6,所以把6装入到集合中,那么显然i2所指向的3再乘2就等于当前最大丑数,那么i2指向下一个丑数时,不管下一个丑数是多少,乘上2一定会比6大了,并且还是当前所有丑数中,乘2最逼近于6的。

import java.util.ArrayList;
public class Solution {
    public int GetUglyNumber_Solution(int index) {
        if(index<=0)
            return 0;
        ArrayList<Integer> list = new ArrayList<Integer>();
        //add进第一个丑数1
        list.add(1);
        //三个下标用于记录丑数的位置
        int i2=0,i3=0,i5=0;
        while(list.size()<index){
            //三个数都是可能的丑数,取最小的放进丑数数组里面
            int n2=list.get(i2)*2;
            int n3=list.get(i3)*3;
            int n5=list.get(i5)*5;
            int min = Math.min(n2,Math.min(n3,n5));
            list.add(min);
            if(min==n2)
                i2++;
            if(min==n3)
                i3++;
            if(min==n5)
                i5++;
        }
        return list.get(list.size()-1);
    }
}
/* 三个数求最小 上面调用的是API
public int Min(int a,int b,int c) {
        int min=a;
        if(min>b)
            min=b;
        if(min>c)
            min=c;
        return min;
      }*/