LeetCode 264. Ugly Number II
给你一个整数 n ,请你找出并返回第 n 个 丑数 。
丑数 就是只包含质因数 2、3 和/或 5 的正整数。
示例 1:
输入: n = 10
输出: 12
解释: [1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。
示例 2:
输入: n = 1
输出: 1
解释: 1 通常被视为丑数。
提示:
1 <= n <= 1690
算法1
(最小堆)O(nlogn)
考虑到第n个丑数一定是由前面的丑数乘上2或3或5得到的,因此可以通过对每个丑数依次乘上2、3、5得到后面的丑数,在这里采用最小堆实现。维护一个最小堆,使得最小堆中的数一定是丑数。 首先初始化堆,将1放入堆中,之后循环n次,对于第i次,利用最小堆弹出最小的数,即第i大的丑数,然后对该数乘上2、3、5得到之后的丑数放入堆中,以此类推。
B时间复杂度分析
每轮需要从堆中弹出堆顶并插入新的数,因此每一轮复杂度为O(logn),需要循环n轮,所以复杂度为O(nlogn)。
C++ 代码
class Solution {
public:
long long nthUglyNumber(int n) {
if(n <= 0)
return 0;
priority_queue<long long>pq;
pq.push(-1);
//由于priority queue会把最大的值放在顶端,为了实现最小堆,我把数字取负数放进去,最后取结果时再变为正数即可。
long long lastnum = 0;//标记前一个数字
for(int i = 1;i<=n;i++){
long long top = pq.top();//取最小堆堆顶
pq.pop();
if(top == lastnum){
//注意到不能重复,如果第i个丑数和第i-1个丑数重复了,那么跳过该丑数
i-=1;
continue;
}
lastnum =top;
pq.push(top*2);//将堆顶乘上2、3、5并放入堆中
pq.push(top*3);
pq.push(top*5);
}
return lastnum*-1;//注意将结果取相反数
}
};
算法2
(动态规划 指针)O(n)
由于丑数的因子也必定是丑数,它一定是某个丑数乘2、3、5得到的,因此我们可以采用动态规划的思想,利用前面已经得到的丑数序列来得到之后的丑数,而问题的关键在于如何确定状态转移方程。
由于小的丑数乘5不一定比大的丑数乘2要小,我们没法直接使用目前最大的丑数来乘2、3、5顺序得到更大的丑数。不过,我们可以确定的是,小的丑陋数乘2,肯定小于大的丑陋数乘2。所以我们使用三个指针,分别记录乘2、3、5得出的目前最大丑陋数,而新的丑数就是这三个目前最大丑数中最小的那个,那么就需要更新被选中的丑数的指针,获得新的三个目前最大丑数,依次类推,从而得到最终结果。
时间复杂度分析
需要维护3个指针,从1到n遍历,复杂度为O(n)。
C++ 代码
class Solution {
public:
int nthUglyNumber(int n) {
vector<int> uglyNumbers;
uglyNumbers.push_back(1);
int index2 = 0;//2 3 5三个指针
int index3 = 0;
int index5 = 0;
for(int i = 1;i<n;i++){
int curMaxNum2 = uglyNumbers[index2]*2;//找出2 3 5指针指向的当前最大丑数
int curMaxNum3 = uglyNumbers[index3]*3;
int curMaxNum5 = uglyNumbers[index5]*5;
int uglynum = min(min(curMaxNum2,curMaxNum3),curMaxNum5);//从当前最大丑数中选一个最小的
if(uglynum==curMaxNum2)//更新指针
index2++;
if(uglynum==curMaxNum3)
index3++;
if(uglynum==curMaxNum5)
index5++;
uglyNumbers.push_back(uglynum);
}
return uglyNumbers[n-1];
}
};