一、什么是丑数?
丑数(Ugly Number)指的是 只包含质因子 2、3、5 的正整数。
也就是说,一个数如果可以写成:
2^a × 3^b × 5^c
其中 a、b、c ≥ 0,那它就是丑数。
前几个丑数如下:
1, 2, 3, 4, 5, 6, 8, 9, 10, 12, ...
注意:1 也是丑数(这是题目规定的起点)。
题目要求的是:
给定一个整数 n,返回第 n 个丑数。
二、思路一:最直观的暴力解法
基本想法
- 从 1 开始,一个个判断是不是丑数
- 每遇到一个丑数就计数
- 直到找到第 n 个
判断一个数是否为丑数
不断除以 2 / 3 / 5,直到不能再除:
while (x % 2 == 0) x /= 2;
while (x % 3 == 0) x /= 3;
while (x % 5 == 0) x /= 5;
return x == 1;
问题
- n 最大可以到 1690
- 中间会跳过大量非丑数
- 时间复杂度极高
结论:会超时,不适合面试和实战
三、思路二:小顶堆 + 去重
核心思想
- 每个丑数都可以由之前的丑数 × 2 / 3 / 5 得到
- 用一个 最小堆(PriorityQueue) 每次取当前最小的丑数
- 用 HashSet 去重,避免重复加入
代码示例
class Solution {
public int nthUglyNumber(int n) {
HashSet<Long> set = new HashSet<>();
PriorityQueue<Long> pq = new PriorityQueue<>();
set.add(1L);
pq.add(1L);
int res = 1;
long[] factors = {2, 3, 5};
for (int i = 0; i < n; i++) {
long ugly = pq.poll();
res = (int) ugly;
for (long f : factors) {
long next = ugly * f;
if (!set.contains(next)) {
set.add(next);
pq.add(next);
}
}
}
return res;
}
}
优缺点分析
优点:
- 思路直观
- 容易理解
- 不会漏解
缺点:
- 需要堆 + HashSet
- 空间复杂度高
- 时间复杂度 O(n log n)
结论:能过,但不是最优解
四、最优解:动态规划 + 三指针
这是面试和刷题中最标准、最优雅的解法。
1. 核心观察
所有丑数的生成方式只有三种:
某个较小的丑数 × 2
某个较小的丑数 × 3
某个较小的丑数 × 5
并且,丑数序列是递增的。
2. 定义状态
用数组 dp[i] 表示:
第 i 个丑数
初始化:
dp[1] = 1
3. 三个指针的含义
我们维护三个指针:
p2:下一个要乘 2 的位置
p3:下一个要乘 3 的位置
p5:下一个要乘 5 的位置
初始状态:
p2 = p3 = p5 = 1
4. 每一步如何生成下一个丑数?
对第 i 个丑数:
候选值:
dp[p2] * 2
dp[p3] * 3
dp[p5] * 5
dp[i] = 三者中的最小值
谁产生了这个最小值,对应的指针就往后移动。
注意:可能同时有多个指针都要移动(避免重复)
5. 动态规划代码
class Solution {
public int nthUglyNumber(int n) {
int[] dp = new int[n + 1];
dp[1] = 1;
int p2 = 1, p3 = 1, p5 = 1;
for (int i = 2; i <= n; i++) {
int t2 = dp[p2] * 2;
int t3 = dp[p3] * 3;
int t5 = dp[p5] * 5;
dp[i] = Math.min(t2, Math.min(t3, t5));
if (dp[i] == t2) p2++;
if (dp[i] == t3) p3++;
if (dp[i] == t5) p5++;
}
return dp[n];
}
}
6. 举个具体例子
已知:
dp = [1]
p2 = p3 = p5 = 1
生成过程:
i = 2:
候选:2, 3, 5
dp[2] = 2
p2++
i = 3:
候选:4, 3, 5
dp[3] = 3
p3++
i = 4:
候选:4, 6, 5
dp[4] = 4
p2++
i = 5:
候选:6, 6, 5
dp[5] = 5
p5++
这样保证了:
- 丑数有序
- 不重复
- 不遗漏
五、三种方法对比总结
| 方法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 暴力判断 | 极高 | 低 | 会超时 |
| 堆 + 去重 | O(n log n) | 高 | 思路直观 |
| 动态规划 + 三指针 | O(n) | O(n) | 最优解,强烈推荐 |
六、这道题真正考察什么?
- 能否发现「丑数是由已有丑数生成的」
- 是否具备将问题转化为动态规划的能力
- 对指针移动和去重逻辑的理解是否扎实
这道题是一个非常经典的「由生成规则反推 DP 状态」的例子,理解后对很多题目都有帮助。
七、总结
- 丑数不是判断题,而是生成题
- 最优解不是堆,而是三指针 DP
- 指针的本质是:控制下一个合法生成位置
如果你能完整推导出这个解法,说明你对动态规划的理解已经非常扎实了。