题意
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
丑数即只能被 2、3、5 整除。判断一个数是不是丑数的代码如下:
bool is_uglyNumber(int num)
{
while(num%2==0)
num=num/2;
while(num%3==0)
num=num/3;
while(num%5==0)
num=num/5;
return num==1;
}
法1—最小堆
要得到从小到大的第 n 个丑数,可以使用最小堆实现。初始时堆为空。首先将最小的丑数 1 加入堆。每次取出堆顶元素 x,则 x 是堆中最小的丑数,由于 2x, 3x, 5x 也是丑数,因此将 2x, 3x, 5x加入堆。
上述做法会导致堆中出现重复元素的情况。为了避免重复元素,可以使用哈希集合(unordered_set)去重,避免相同元素多次加入堆。
在排除重复元素的情况下,第 n 次从最小堆中取出的元素即为第 n 个丑数。
C++实现
class Solution
{
public:
int nthUglyNumber(int n)
{
if(n<=1)
return n;
//用来与当前取出的丑数相乘,得到之后的丑数
vector<int> factors{2,3,5};
//最小堆,用来存放丑数,由于丑数范围可能超出int,所以用long
priority_queue<long,vector<long>,greater<long>> min_heap;
//使用集合去重
unordered_set<long> set;
//第n个丑数
int ugly_number;
//首先将最小的丑数加入最小堆和集合中
set.insert(1L); //1L表示long类型的1
min_heap.push(1L);
for(int i=0;i<n;++i)
{
long temp = min_heap.top();
min_heap.pop();
ugly_number=static_cast<int>(temp);
for(auto& num:factors)
{
long next_guly_number=num*temp;
//如果哈希集合里面没有重复的丑数
if(set.find(next_guly_number)==set.end())
{
min_heap.push(next_guly_number);
set.insert(next_guly_number);
}
}
}
return ugly_number;
}
};
复杂度分析 :
- 时间复杂度:O(nlogn)。得到第 n 个丑数需要进行 n 次循环,每次循环都要从最小堆中取出 1 个元素以及向最小堆中加入最多 3 个元素,因此每次循环的时间复杂度是 O(logn+log3n)=O(logn),总时间复杂度是O(nlogn)。
- 空间复杂度:O(n)。空间复杂度主要取决于最小堆和哈希集合的大小,最小堆和哈希集合的大小都不会超过 3n
法2—动态规划
方法一使用最小堆,会预先存储较多的丑数,导致空间复杂度较高,维护最小堆的过程也导致时间复杂度较高。可以使用动态规划的方法进行优化。
定义数组 dp,其中 dp[i] 表示第 i 个丑数,第 n 个丑数即为 dp[n]。
由于最小的丑数是 1,因此dp[1]=1。如何得到其余的丑数呢?我们知道,丑数只包含因子 2, 3, 5,因此有 “丑数 == 某较小丑数 × 某因子”(例如:10=5*2,较大丑数10等于较小丑数5*2这个因子)。
定义三个指针 p2,p3,p5,表示下一个丑数是当前指针(p2、p3、p5)指向的丑数乘以对应的质因数 中 最小的一个。
初始时,三个指针的值都是 1。当 2 ≤ i ≤n 时,令:dp[i] = min( dp[p2]*2,dp[p3]*3,dp[p5]*5 ),然后分别比较 dp[i]和 dp[p2]*2、dp[p3]*3、dp[p5]*5相不相等,如果相等则将对应的指针加 1。
三指针解释:
例如 n = 10, primes = [2, 3, 5]。 打印出丑数列表:1, 2, 3, 4, 5, 6, 8, 9, 10, 12
首先一定要知道,后面的丑数一定由前面的丑数乘以2,或者乘以3,或者乘以5得来。例如,8,9,10,12一定是1, 2, 3, 4, 5, 6乘以2,3,5三个质数中的某一个得到。
这样的话我们的解题思路就是:从第一个丑数开始,一个个数丑数,并确保数出来的丑数是递增的,直到数到第n个丑数,得到答案。那么问题就是如何递增地数丑数?
观察上面的例子,假如我们用1, 2, 3, 4, 5, 6去形成后面的丑数,我们可以将1, 2, 3, 4, 5, 6分别乘以2, 3, 5,这样得到一共6*3=18个新丑数。也就是说1, 2, 3, 4, 5, 6中的每一个丑数都有一次机会与2相乘,一次机会与3相乘,一次机会与5相乘(一共有18次机会形成18个新丑数),来得到更大的一个丑数。
这样就可以用三个指针:
p2, 指向1, 2, 3, 4, 5, 6中,还没使用 乘2机会 的丑数的位置。该指针的前一位已经使用完了乘以2的机会。
p3, 指向1, 2, 3, 4, 5, 6中,还没使用 乘3机会 的丑数的位置。该指针的前一位已经使用完了乘以3的机会。
p5, 指向1, 2, 3, 4, 5, 6中,还没使用 乘5机会 的丑数的位置。该指针的前一位已经使用完了乘以5的机会。
下一次寻找丑数时,则对这三个位置分别尝试使用一次乘2机会,乘3机会,乘5机会,看看哪个最小,最小的那个就是下一个丑数。最后,那个得到下一个丑数的指针位置加一,因为它对应的那次乘法机会使用完了。
这里需要注意下去重的问题,如果某次寻找丑数,找到了下一个丑数10,则p2和p5都需要加一,因为5乘2等于10, 2乘5也等于10,这样可以确保10只被数一次(所以不能使用 if ... else if;要用三个if判断)。
C++实现
class Solution
{
public:
inline int _min(int n1,int n2,int n3)
{
return n1>n2?(n2>n3?n3:n2):(n1>n3?n3:n1);
}
public:
int nthUglyNumber(int n)
{
if(n<=1)
return n;
vector<int> dp(n+1);
dp[1]=1;
int p2,p3,p5;
p2=p3=p5=1; //初始化三个指针,都指向第一个元素,因为第一个元素还没有使用分别乘2、乘3、乘5的机会
for(int i=2;i<=n;++i)
{
int num2=dp[p2]*2;
int num3=dp[p3]*3;
int num5=dp[p5]*5;
dp[i]=_min(num2,num3,num5);
if(dp[i]==num2)
p2++; //p2所指向的元素已经使用完 乘2 这个机会了
if(dp[i]==num3)
p3++; //p3所指向的元素已经使用完 乘3 这个机会了
if(dp[i]==num5)
p5++; //p5所指向的元素已经使用完 乘5 这个机会了
}
return dp[n];
}
};