一、题目描述
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
二、开始手撕
第一次尝试:双重循环,运行时间爆炸
最开始的思路就是遍历所有数字,然后将他们分解因子,但是时间复杂度大:
public class Solution {
public int GetUglyNumber_Solution(int index) {
int n=0;
int m=2;
while(n<index)
{
if(cs(m))
n++;
m++;
}
return n;
}
public boolean cs(int num)
{
while(num%2==0)
num=num/2;
while(num%3==0)
num=num/3;
while(num%5==0)
num=num/5;
if(num==1)
return true;
return false;
}
}
第二次尝试:在第一步的基础上改进,改成递归
但是治标不治本,时间复杂度仍然爆炸
public class Text {
public static void main(String[] args)
{
int n=1;
int sum=0;
int index=7;
int now=1;
while(sum<index)
{
if(cddg(n))
{
sum++;
now=n;
}
n++;
}
System.out.println(now);
}
public static boolean cddg(int cs)
{
if((cs%2==0&&cs/2==1)||(cs%3==0&&cs/3==1)||(cs%5==0&&cs/5==1)||cs==1)
{
return true;
}
int two=cs/2;
int three=cs/3;
int five=cs/5;
if(two*2==cs)
return cddg(two);
else if(three*3==cs)
return cddg(three);
else if(five*5==cs)
return cddg(five);
else
return false;
}
}
第三次尝试:从2的基础上得到灵感,一个丑数的所有因子也必然是丑数
于是我将2进行改进,只需要判断一个数除2(或除3或除5)得到的数是否在集合内则可以判断该数是否是丑数,我用了HashSet的contains方法比较。
最后的结果是,超时。
import java.util.HashSet;
public class Solution {
public int GetUglyNumber_Solution(int index) {
if(index<=0)
return 0;
HashSet set = new HashSet();
set.add(1);
set.add(2);
set.add(3);
set.add(5);
int n=1;
int sum=1;
int now=1;
while(sum<index)
{
if(n%2==0&&set.contains(n/2))
{
set.add(n);
now=n;
sum++;
}
else if(n%3==0&&set.contains(n/3))
{
set.add(n);
now=n;
sum++;
}
else if(n%5==0&&set.contains(n/5))
{
set.add(n);
now=n;
sum++;
}
n++;
}
return now;
}
}
我(此时的)认为原因主要出现在HashSet的contains的方法上,是否是这个方法的实现使我的时间爆表,所以我要另辟道路。
第四次尝试:既然由3知道了每个丑数的所有因子也必然是丑数,那么每个丑数除2(或除3或除5)会是怎样的
由可知上图,每一个丑数由它的前面的丑数与2,3,5相乘得到,而且最后的商仍然按照丑数增长规律。
但明显有部分丑数是重复计算的,所以我们去除重复项后可见。
此处因表格限制未显示出来,事实上数据够长可以发现,当重复项删除后,无论是2或3或5,所产生的数值集都都按照该倍数以某个规律进行被增。
如3:1 3 5 9 15 27 45 是隔2个数进行3的倍增。当然我们这里不对此进行讨论
既然每个数集合是与原排列规律相同,那么我是否可以进行遍历,每找一个数,判断其除2或除3过除5后是否的数字是否在集合内。这个看起来思路好像和3差不多,但其实3的方法在这种具有明显排序递增,且有规律的集合中会显得过于浪费。
因此我使用了队列,利用队列先进先出的特性,当遇到相同时出列,入列。但是,运行时间仍然超时了。
import java.util.Queue;
import java.util.LinkedList;
public class Solution {
public int GetUglyNumber_Solution(int index) {
Queue<Integer> queue2 =new LinkedList();
Queue<Integer> queue3 =new LinkedList();
Queue<Integer> queue5=new LinkedList();
queue2.add(1);
queue3.add(1);
queue5.add(1);
int sum=1;
int now=1;
for(int n=2;sum<index;n++)
{
if(n%2==0&&n/2==queue2.peek())
{
queue2.add(n);
queue2.poll();
sum++;
now=n;
}
else if(n%3==0&&n/3==queue3.peek())
{
queue2.add(n);
queue3.add(n);
queue3.poll();
sum++;
now=n;
}
else if(n%5==0&&n/5==queue5.peek())
{
queue2.add(n);
queue3.add(n);
queue5.add(n);
queue5.poll();
sum++;
now=n;
}
}
return now;
}
}
此时的我十分苦恼,不懂得为什么只用了一个循环还会超时
第五次尝试:我将4的代码由队列存储改为数组,利用双箭头,一个指向数组目前存储位置,一个指向数组当前取值位置。
好吧,时间超时了,意料之内,因为其实并没有什么方法上的创新,只是存储方式改了,甚至损耗空间还更大了,也并不是没有什么收获,起码我知道了不是队列内部存储的方法(起码不是主要原因)导致时间超时,我的逻辑本身是有错误的。
import java.util.*;
import java.util.Queue;
import java.util.LinkedList;
public class Text {
public static void main(String[] args)
{
long start = System.currentTimeMillis();
Scanner sc =new Scanner(System.in);
int[] i2 =new int[10000];
int[] i3 =new int[10000];
int[] i5 =new int[10000];
int n2=0;
int nn2=0;
int n3=0;
int nn3=0;
int n5=0;
int nn5=0;
i2[n2]=1;
i3[n3]=1;
i5[n5]=1;
int index=sc.nextInt();
System.out.print(1+" ");
int sum=1;
int now=1;
int a=0;
for(int n=2;sum<index;n++)
{
if(n%2==0&&n/2==i2[nn2])
{
i2[++n2]=n;
nn2++;
sum++;
now=n;
System.out.print(n+" ");
}
else if(n%3==0&&n/3==i3[nn3])
{
i2[++n2]=n;
i3[++n3]=n;
nn3++;
sum++;
now=n;
System.out.print(n+" ");
}
else if(n%5==0&&n/5==i5[nn5])
{
i2[++n2]=n;
i3[++n3]=n;
i5[++n5]=n;
nn5++;
sum++;
now=n;
System.out.print(n+" ");
}
}
System.out.println();
//此处写要测试的代码
long end = System.currentTimeMillis();
System.out.println("共耗时"+(end-start)+"毫秒");
}
}
第六次尝试(success):我试着输出更多数据,发现丑数的分布数字越大,前后两个丑数相差就会越远,我总算发现一个致命的错误:遍历。当想要知道第1600个丑数后想知道第1601个时,中间有可能需要遍历几十上百万个数,这是时间的巨额浪费。
:我在5的基础上改进,既然不能遍历,那就从第一个开始往后推算,将计算后的结果写入数组,同样是双指针,大致与5相同。
import java.util.*;
import java.util.Queue;
import java.util.LinkedList;
public class Text {
public static void main(String[] args)
{
long start = System.currentTimeMillis();
Scanner sc =new Scanner(System.in);
long[] i2 =new long[100000];
long[] i3 =new long[100000];
long[] i5 =new long[100000];
int n2=0;
int nn2=0;
int n3=0;
int nn3=0;
int n5=0;
int nn5=0;
i2[n2]=1;
i3[n3]=1;
i5[n5]=1;
int index=sc.nextInt();
System.out.print(1+" ");
int sum=1;
long now=1;
while(sum<index)
{
long a=i2[nn2]*2;
long b=i3[nn3]*3;
long c=i5[nn5]*5;
if(Min(a,b,c)==2)
{
i2[++n2]=a;
nn2++;
sum++;
now=a;
System.out.print(a+" ");
}
else if(Min(a,b,c)==3)
{
i2[++n2]=b;
i3[++n3]=b;
nn3++;
sum++;
now=b;
System.out.print(b+" ");
}
else if(Min(a,b,c)==5)
{
i2[++n2]=c;
i3[++n3]=c;
i5[++n5]=c;
nn5++;
sum++;
now=c;
System.out.print(c+" ");
}
}
int innow=(int)now;
System.out.println(innow);
//此处写要测试的代码
long end = System.currentTimeMillis();
System.out.println("共耗时"+(end-start)+"毫秒");
}
public static int Min(long a,long b,long c)
{
if(a<b&&a<c)
return 2;
if(b<c&&b<a)
return 3;
if(c<b&&c<a)
return 5;
return 0;
}
}
终于
虽然排名很低,但确实很开心
看看牛人:既然排名低,又怎能不看看优秀答案呢,牛客牛人还是蛮多的。
public class Solution {
public int GetUglyNumber_Solution(int index) {
if(index <= 0)return 0;
int p2=0,p3=0,p5=0;//初始化三个指向三个潜在成为最小丑数的位置
int[] result = new int[index];
result[0] = 1;//
for(int i=1; i < index; i++){
result[i] = Math.min(result[p2]*2, Math.min(result[p3]*3, result[p5]*5));
if(result[i] == result[p2]*2)p2++;//为了防止重复需要三个if都能够走到
if(result[i] == result[p3]*3)p3++;//为了防止重复需要三个if都能够走到
if(result[i] == result[p5]*5)p5++;//为了防止重复需要三个if都能够走到
}
return result[index-1];
}
}
以三个指针来记录当前乘以2、乘以3、乘以5的最小值,然后当其被选为新的最小值后,要把相应的指针+1;因为这个指针会逐渐遍历整个数组,因此最终数组中的每一个值都会被乘以2、乘以3、乘以5。
总结:乍看之下这位大牛的好像和自己的差不多(吹牛不犯法吧),但其实上他用单指针比我用双指针所耗费的时间自然节省不少。两者有本质上的不同,因为我根本没拐到动态规划那去。
啊啊啊,真的就差一拐了。
学习之路漫漫无期,任何学到的知识都是自己的武器。