1. 题目来源
链接:丑数
来源:LeetCode——《剑指-Offer》专项
2. 题目说明
我们把只包含因子 2、3 和 5 的数称作丑数(Ugly Number
)。求按从小到大的顺序的第 n
个丑数。
示例:
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
说明:
- 1 是丑数。
n
不超过1690。
3. 题目解析
方法一:TLE+常规解法
根据丑数的定义,它只能被 2、3、5 整除,那么这个数能被 2、3、5 其中一个整除时,就连续让它除以 2 / 3 / 5,若最后得到的结果是 1,说明它是丑数,否则说明它不是丑数。然后顺序计算得到 n
个丑数并输出即可。
虽然代码很简洁、直观,但是频繁的求余、除法运算是非常耗时的,最终程序不能 AC
,TLE
了,当 n = 1352
时就无法通过了。
参见代码如下:
// TLE
// 500 / 596 个通过测试用例
// 最后执行的输入:1352
class Solution {
public:
int nthUglyNumber(int n) {
long long cnt = 0, tmp = 0;
while (cnt < n) {
++tmp;
if (judge(tmp)) {
++cnt;
}
}
return tmp;
}
bool judge(int n) {
while (n % 2 == 0) n /= 2;
while (n % 3 == 0) n /= 3;
while (n % 5 == 0) n /= 5;
if (n == 1) return true;
return false;
}
};
方法二:思维+巧妙解法
根据《剑指-Offer》讲解内容,丑数序列可以拆分为下面3个子列表:
- 1x2, 2x2, 2x2, 3x2, 3x2, 4x2, 5x2…
- 1x3, 1x3 ,2x3, 2x3, 2x3, 3x3, 3x3…
- 1x5, 1x5, 1x5, 1x5, 2x5, 2x5, 2x5…
仔细观察上述三个列表,可以发现每个子列表都是一个丑数分别乘以 2,3,5,而要求的丑数就是从已经生成的序列中取出来的,每次都从三个列表中取出当前最小的那个加入序列即可。
参见代码如下:
// 执行用时 :16 ms, 在所有 C++ 提交中击败了33.51%的用户
// 内存消耗 :12 MB, 在所有 C++ 提交中击败了100.00%的用户
class Solution {
public:
int nthUglyNumber(int n) {
vector<int> res(1, 1);
int i2 = 0, i3 = 0, i5 = 0;
while (res.size() < n) {
int m2 = res[i2] * 2, m3 = res[i3] * 3, m5 = res[i5] * 5;
int mn = min(m2, min(m3, m5));
if (mn == m2) ++i2;
if (mn == m3) ++i3;
if (mn == m5) ++i5;
res.push_back(mn);
}
return res.back();
}
};
方法三:思维+堆+巧妙解法
思想同方法二,采用空间换时间的做法,可以使用最小堆来做,具体思路如下:
- 首先放进去一个 1
- 然后从 1 遍历到
n
,每次取出堆顶元素,为了确保没有重复数字,进行一次while
循环,将此时和堆顶元素相同的都取出来,然后分别将这个取出的数字乘以 2,3,5,并分别加入最小堆。这样最终for
循环退出后,堆顶元素就是所求的第n
个丑数。
参见代码如下:
// 执行用时 :236 ms, 在所有 C++ 提交中击败了5.11%的用户
// 内存消耗 :13.1 MB, 在所有 C++ 提交中击败了100.00%的用户
class Solution {
public:
int nthUglyNumber(int n) {
priority_queue<long, vector<long>, greater<long>> pq;
pq.push(1);
for (long i = 1; i < n; ++i) {
long t = pq.top(); pq.pop();
while (!pq.empty() && pq.top() == t) {
t = pq.top(); pq.pop();
}
pq.push(t * 2);
pq.push(t * 3);
pq.push(t * 5);
}
return pq.top();
}
};