264. 丑数 II ●●
描述
只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。
通常 1 为第一个丑数,求按从小到大的顺序的第 n 个丑数。
示例
输入:n = 10
输出:12
解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。
题解
1. 小顶堆
要得到从小到大的第 n 个丑数,可以使用最小堆实现。
初始时堆为空。首先将最小的丑数 1 加入堆。
每次取出堆顶元素 x,即堆中最小的丑数,由于 2x, 3x, 5x 也是丑数,因此将 2x, 3x, 5x 加入堆。
上述做法会导致堆中出现重复元素的情况。为了避免重复元素,可以使用哈希集合去重,避免相同元素多次加入堆。
在排除重复元素的情况下,第 n 次从最小堆中取出的元素即为第 n 个丑数。
- 时间复杂度: O ( n log n ) O(n \log n) O(nlogn)。得到第 n 个丑数需要进行 n 次循环,每次循环都要从最小堆中取出 1 个元素以及向最小堆中加入最多 3 个元素,因此每次循环的时间复杂度是 O ( log ( 3 n ) + 3 log ( 3 n ) ) O(\log (3n) + 3 \log (3n)) O(log(3n)+3log(3n)),总时间复杂度是 O ( n log n ) O(n \log n) O(nlogn)。
- 空间复杂度: O ( n ) O(n) O(n)。空间复杂度主要取决于最小堆和哈希集合的大小,最小堆和哈希集合的大小都不会超过 3n。
class Solution {
public:
int nthUglyNumber(int n) {
vector<int> factors = {2, 3, 5}; // 因子
unordered_set<long> hash; // 哈希
priority_queue<long, vector<long>, greater<long>> heap; // 小顶堆
hash.insert(1L);
heap.push(1L);
int uglyn = 1;
for(int i = 1; i <= n; ++i){
long top = heap.top();
heap.pop();
uglyn = (int)top; // 第n个丑数
for(int factor : factors){ // 堆中最小的丑数 * 因子2/3/5
long next = top * factor;
if(hash.count(next) == 0){
hash.insert(next);
heap.push(next);
}
}
}
return uglyn;
}
};
2. 动态规划
方法一使用最小堆,会预先存储较多的丑数,维护最小堆的过程也导致时间复杂度较高。
可以使用动态规划的方法进行优化。
定义数组 uglys,其中 uglys[i-1]
表示第 i 个丑数。
uglys[0] = 1;
如何得到其余的丑数呢?
每个丑数都有一次与 2/3/5 相乘的机会,定义三个指针 p2、p3、p5,表示下一个丑数是三个指针指向的丑数乘以对应的质因数得到的最小值。初始时,三个指针的值都是 0,每轮将对应指针执行 +1 即可。
uglys[i] = min(min(num2, num3), num5);
在进行指针判断移动操作时,对每个指针得到的结果都进行一次对比,去除重复操作,比如 5*2 与 2*5,那么 p2 和 p5 都右移。
- 时间复杂度:O(n)。需要计算数组中的 n 个元素,每个元素的计算都可以在 O(1) 的时间内完成。
- 空间复杂度:O(n)
class Solution {
public:
int nthUglyNumber(int n) {
vector<int> uglys(n, 1);
int p2 = 0, p3 = 0, p5 = 0;
for(int i = 1; i < n; ++i){
int num2 = uglys[p2]*2, num3 = uglys[p3]*3, num5 = uglys[p5]*5;
uglys[i] = min(min(num2, num3), num5);
if(uglys[i] == num2) ++p2;
if(uglys[i] == num3) ++p3;
if(uglys[i] == num5) ++p5; // 不用else,去重,2*5 跟 5*2的情况,p2与p5++
}
return uglys[n-1];
}
};