一.题目大意
给你一个数n,要求返回第n个丑数。其中,丑数的定义如下:
丑数是指只包含因子2、3和5的数。(数字1也是丑数,不过是个特例)引用《剑指offer》上的话来说,对于一个数M,如果M能被2整除,就连续除以2;若果能被3整除,就连续除以3;如果能被5整除,就连续除以5。如果最终的结果是1的话,那么M就是丑数,否则M不是丑数。以下是判断一个数是否为丑数的代码:
bool IsUglyNumber(int num)
{
while(num % 2 == 0)
num /= 2;
while(num % 3 == 0)
num /= 3;
while(num % 5 == 0)
num /= 5;
return (num == 1)? true : false; }
二.思路分析
方法1.
最直观的解法,就是从1开始进行遍历,对于每个数分别判断是否为丑数,直到找到n个丑数为止。
此方法,对于n比较大的情况,往往都会超时的。所以,这种方法并不最优选择。
方法2:
实质是对方法1的优化,分析方法1,会发现浪费的操作主要在于非丑数的数。所以想方设法对非丑数的数进行排除,即只对丑数进行操作。《剑指offer》上给了这样一种思路:
(1)首先,根据丑数的定义,任何一个丑数都应该是另一个丑数乘以2或者3或者5的结果(1除外)。因此,根据这种特性,我们可以从最小的丑数开始逐渐生成新的丑数,并且要求新生成的丑数是排好序的(升序),如果用一个数组来存储它们的话,那么数组的第n个元素就是我们想要的结果。
(2)这种方法的重点在于如何确保新生成的丑数是排好序的:
假设这个有序的丑数数组为A[0]、A[1]、A[2]、... A[M],那么,A[M + 1]一定是A[0]或A[1]或 ... A[M]中的某个元素(假设这个元素为A[i])乘以2或者乘以3或者乘以5得到的,至于A[i]具体是哪个值,我们现在是不知道的。不过我们可以有以下的分析:
1.我们首先考虑把A[0]、A[1]、A[2]、...A[M]这M+1个数都乘以2,那么我们就能得到M + 1个新的丑数,但是这M+1个丑数中,一定有一部分与A[0]、A[1]、A[2]、... A[M]中的某些数是重复的(这M+1个丑数中小于A[M]的数一定就是重复的,因为数组A就是有序的丑数序列),这些重复的丑数就不用考虑了,也一定有一部分数是大于A[M]的,而我们要找的就是从这些大于A[M]的丑数中,选出最小的一个来,并把这个最小的丑数命名为N2。
2.我们再把A[0]、A[1]、A[2]、...A[M]这M+1个数都乘以3,那么我们也能得到M + 1个新的丑数,这M+1个丑数中,同理,一定有一部分与A[0]、A[1]、A[2]、... A[M]中的某些数是重复的(这M+1个丑数中小于A[M]的数一定就是重复的),这些重复的丑数就不用考虑了,也一定有一部分数是大于A[M]的,而我们要找的就是从这些大于A[M]的丑数中,选出最小的一个来,并把这个最小的丑数命名为N3。
3.我们最后再把A[0]、A[1]、A[2]、...A[M]这M+1个数都乘以5,那么我们同样能得到M + 1个新的丑数,这M+1个丑数中,同理,一定也有一部分与A[0]、A[1]、A[2]、... A[M]中的某些数是重复的(这M+1个丑数中小于A[M]的数一定就是重复的),这些重复的丑数就不用考虑了,也一定有一部分数是大于A[M]的,而我们要找的就是从这些大于A[M]的丑数中,选出最小的一个来,并把这个最小的丑数命名为N5。
4.经过前3步,我们得到了三个最小的数N2、N3、N5,那么,A[M+1]一定是这三个数中最小的那个数。
5.我们就可以重复第1步~第4步,直到数组A中的元素达到n个时,A[n - 1]就是我们想要的结果。
注:我们在以上的理论分析中每次都需要把A[0]、A[1]、A[2]、... A[M](M在每次迭代后都会加1)分别乘以2、3和5,这样太麻烦了啊,能不能进一步优化呢?我们下面来详细分析下:
1.首先,我们要了解上面的第1~3步,主要是为了找出N2、N3、N5这三个数来,而实际上,只需找到原数组A(即已知的丑数序列)中对应的元素,令该元素乘以2或者乘以3或者乘以5就可以得到N2、N3或者N5了。以下进一步分析。
2.对于有序的丑数数组A[0]、A[1]、A[2]、... A[M],如果把数组中每个元素都乘以2的话,就会得到M+1个新的丑数,对于这M+1个新的丑数,我们之前已经给分析了,有一部分是与数组中原来的元素是重复的,另一部分则是大于A[M]的。所那么在原数组A(即已有的丑数)中,这M+1个旧的丑数中,一定存在这么一个丑数T2,排在它前面的丑数乘以2都小于等于A[M],排在它后面的丑数,都大于A[M]。那么,根据之前的1中的叙述,就可以得到T2 * 2 = N2。也就是说确定了T2,就能确定N2了。我们在进一步分析,如果N2、N3、N5这三个数中最小的是N2的话,那么A[M+1] = N2(令M1 =M +1),那么此时的T2就不满足之前的要求了(即排在它前面的丑数乘以2都小于等于A[M1],排在它后面的丑数,都大于A[M1],此时M1=M+1)所以此时,需要更新T2,即T2变成了原数组A中T2之后的那个元素(相当于下标索引加1)。每次选出相应的N值时,必须要更新相应的T值。
3.同理的话,我们根据1,可知同样在原数组A中可以得到一个丑数T5,使得排在它前面的丑数乘以5都小于等于A[M],排在它后面的丑数,都大于A[M]。同理可得,T5 * 5 = N5。同样的,如果N5是N2、N3、N5中最小的丑数的话,那么A[M+1] = N5,此时T5就更新成原数组A中T5之后的那个元素了。
4.同理,T3也是同样的更新准则。
5.综合1、2、3我们只需找到并更新T2、T3、T5的值即可,并不用把所有的已知丑数都乘一遍。(当然了,如果N2、N3、N5中存在两个相同的值(假设N2 == N3),并且都是最小的话,那么对应的两个数(T2和T3)都需要更新)。
6.由第5步可知,我们需要的是获得T2、T3、T5的初值,并对他们进行更新,就能获取排好序的数组之后的元素了(A[M+1]、A[M+2]、A[M+3]、... A[n-1])。那么如何获得T2、T3、T5的初始值?由于刚开始数组中的元素只有1个,即A[0] =1,此时的T2 = T3 = T5 =1的,所以按照之前的规则进行更新就可以了。
================================================================================================================================================
综合以上分析,可得代码如下:
int GetUglyNumber_Solution(int index) {
if(index < 7)
return index; //由于数字1~6都是丑数,而且是排好序的,所以可以直接返回
vector<int> rs(index);
rs[0] = 1;//初始化数组,1是个特例
int t2 =0, t3 = 0, t5 = 0;//rs[t2]对应T2,rs[t3]对应T3,rs[t5]对应T5
for(int i = 1; i < index; i++)
{
rs[i] = min(rs[t2] * 2,min(rs[t3] * 3,rs[t5] * 5));//选出T2、T3、T5中的最小值
if(rs[i] == rs[t2] * 2)
t2++;//更新T2,T2变成数组中T2之后的那个元素了
if(rs[i] == rs[t3] * 3)
t3++;
if(rs[i] == rs[t5] * 5)
t5++;
}
return rs[index - 1]; }
该方法的时间复杂度为O(n),空间复杂度O(n)。相比思路一时间效率得到了极大的提高。
观察改代码可知,实际上通过t2、t3、t5这三个索引下标就实现了对原数组中所有的元素乘以2、乘以3、乘以5的操作,只不过避免重复操作,才引入了T2、T3、T5这三个丑数。(一旦选定了N中的最小值,假设是N2,一定要去更新对应的T值,即N2对应的是T2)
可以具体分析下这段代码:
(1)首先,在第一轮循环中,数组rs中只有元素1,即rs[0] = 1,且t2 = t3 = t5 = 0,T2 = T3 =T5 = rs[0] = 1,由于N2是三者中最小值,故数组中新添加的元素rs[1] = N2 = 2。此时T2需要更新,即t2 = t2 +1(T2变成了T2之后的那个元素)
(2)在第二轮循环中,数组rs有两个元素了,且t2 =1,t3 = t5 = 0,T2 =rs[1] = 2,T3 = T5 = rs[0] = 1,由于N3是三者中的最小值,故数组中新添加的元素rs[2] = N3 = 3。此时T3需要更新,即t3 = t3 +1(T3变成了T3之后的那个元素)
(3)在第三轮循环中,数组rs有三个元素了,且t2 = t3 = 1,t5 = 0,T2 =T3 =rs[1] =2,T5 = rs[0] = 1,由于N2= T2 *2 =4,N3 = T3 * 3 = 6,T5= N5 * 5 = 5,N2是三者之中最小值,故数组中新添加的元素rs[3] = N2 =4,同时,T2需要更新,即t2 = t2 + 1
(4)在四轮循环汇总,数组rs有四个元素了,且t2 = 2,t3 = 1,t5 = 0,T2 = rs[2] = 3,T3 = rs[1] = 2,T5 = rs[0] = 1,由于N2 = 6,N3 = 6,N5 =5,即N5是三者之中的最小值,故数组中新添加的元素rs[4] = N5 = 5,同时,T5需要更新,即t5 = t5 +1
.......
按照此规律依次进行下去。