Skip to content

实战:丑数 II

找出第 n 个丑数。丑数是只包含质因子 2、3、5 的正整数。


问题描述

LeetCode 264. Ugly Number II

丑数是只包含质因子 2、3 或 5 的正整数。给你一个整数 n,返回第 n 个丑数。1 被视为第一个丑数。

示例 1

输入:n = 10
输出:12
解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是前 10 个丑数

示例 2

输入:n = 1
输出:1

约束条件

  • 1 <= n <= 1690

问题分析

丑数的性质

丑数只有质因子 2、3、5,这意味着:

  • 1 是丑数(特殊情况)
  • 任何丑数 × 2、× 3、× 5 还是丑数
  • 所有丑数都可以表示为 2^a × 3^b × 5^c(a, b, c ≥ 0)
丑数序列:1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 16, ...
          ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓   ↓   ↓   ↓   ↓
       2^0 2^1 3  2^2 5  2×3 2^3 3^2 2×5 2^2×3 3×5 2^4

核心思路

每个丑数都是由更小的丑数乘以 2、3 或 5 得到的。问题是如何按顺序生成丑数?


解法一:最小堆

用最小堆来保证每次取出的都是最小的丑数:

  1. 从 1 开始
  2. 每次取出最小的丑数 x
  3. 把 x×2、x×3、x×5 加入堆
  4. 用 Set 去重(因为同一个丑数可能被多次生成)
javascript
function nthUglyNumber(n) {
  const heap = new MinHeap();
  const seen = new Set();
  
  heap.insert(1);
  seen.add(1);
  
  let ugly = 1;
  
  for (let i = 0; i < n; i++) {
    ugly = heap.extract();
    
    // 生成新的丑数
    for (const factor of [2, 3, 5]) {
      const next = ugly * factor;
      if (!seen.has(next)) {
        seen.add(next);
        heap.insert(next);
      }
    }
  }
  
  return ugly;
}

执行过程

n = 5

i=0: 取出 1, 加入 2,3,5
     堆: [2,3,5], ugly=1

i=1: 取出 2, 加入 4,6,10
     堆: [3,4,5,6,10], ugly=2

i=2: 取出 3, 加入 6(重复),9,15
     堆: [4,5,6,9,10,15], ugly=3

i=3: 取出 4, 加入 8,12,20
     堆: [5,6,8,9,10,12,15,20], ugly=4

i=4: 取出 5, ugly=5

返回 5

复杂度分析

  • 时间:O(n log n),每次操作堆 O(log n)
  • 空间:O(n),堆和 Set 各存储约 3n 个元素

解法二:三指针(动态规划)

更高效的方法是用三个指针,分别表示下一个要乘 2、3、5 的丑数位置:

核心思想

  • 维护一个丑数数组 dp
  • 用 p2、p3、p5 分别指向"乘以 2/3/5 后可能是下一个丑数"的位置
  • 每次取三个候选值的最小值
javascript
function nthUglyNumber(n) {
  const dp = [1];
  let p2 = 0, p3 = 0, p5 = 0;
  
  for (let i = 1; i < n; i++) {
    // 三个候选值
    const next2 = dp[p2] * 2;
    const next3 = dp[p3] * 3;
    const next5 = dp[p5] * 5;
    
    // 取最小值作为下一个丑数
    const next = Math.min(next2, next3, next5);
    dp.push(next);
    
    // 更新指针(注意:可能有多个指针同时产生相同的值)
    if (next === next2) p2++;
    if (next === next3) p3++;
    if (next === next5) p5++;
  }
  
  return dp[n - 1];
}

三指针执行过程详解

初始:dp = [1], p2=0, p3=0, p5=0

i=1:
  next2 = dp[0]*2 = 2
  next3 = dp[0]*3 = 3
  next5 = dp[0]*5 = 5
  min = 2, dp = [1, 2], p2=1

i=2:
  next2 = dp[1]*2 = 4
  next3 = dp[0]*3 = 3
  next5 = dp[0]*5 = 5
  min = 3, dp = [1, 2, 3], p3=1

i=3:
  next2 = dp[1]*2 = 4
  next3 = dp[1]*3 = 6
  next5 = dp[0]*5 = 5
  min = 4, dp = [1, 2, 3, 4], p2=2

i=4:
  next2 = dp[2]*2 = 6
  next3 = dp[1]*3 = 6
  next5 = dp[0]*5 = 5
  min = 5, dp = [1, 2, 3, 4, 5], p5=1

i=5:
  next2 = dp[2]*2 = 6
  next3 = dp[1]*3 = 6
  next5 = dp[1]*5 = 10
  min = 6, dp = [1, 2, 3, 4, 5, 6], p2=3, p3=2  ← 注意:6 由两种方式生成

...

关键点:当 next2 == next3 时,两个指针都要移动,避免重复添加同一个丑数。

复杂度分析

  • 时间:O(n)
  • 空间:O(n)

为什么三指针有效?

问题:为什么只需要三个指针就能保证不遗漏任何丑数?

回答

  1. 每个丑数都是由更小的丑数乘以 2、3 或 5 得到
  2. 三个指针确保了每个丑数都会被乘以 2、3、5
  3. 指针只前进不后退,保证每个丑数只被考虑一次

可视化

dp:    1    2    3    4    5    6    8    9    10   12
       ↑    ↑              ↑
      p5   p3             p2

下一轮候选:
  dp[p2] × 2 = 4 × 2 = 8
  dp[p3] × 3 = 2 × 3 = 6  ← 最小
  dp[p5] × 5 = 1 × 5 = 5

两种方法对比

方法时间空间特点
最小堆O(n log n)O(n)直观易懂,但有去重开销
三指针O(n)O(n)更高效,无重复计算

边界情况

javascript
// 测试用例
nthUglyNumber(1);   // → 1(第一个丑数)
nthUglyNumber(2);   // → 2
nthUglyNumber(10);  // → 12
nthUglyNumber(1690);// → 2123366400(最大测试用例)

常见错误

1. 三指针不同时更新

javascript
// ❌ 错误:用 else if
if (next === next2) {
  p2++;
} else if (next === next3) {
  p3++;
} else {
  p5++;
}
// 当 next2 == next3 时,只更新了 p2,遗漏了 p3

// ✅ 正确:用独立的 if
if (next === next2) p2++;
if (next === next3) p3++;
if (next === next5) p5++;

2. 堆方法忘记去重

javascript
// ❌ 错误:不去重
for (const factor of [2, 3, 5]) {
  heap.insert(ugly * factor);  // 可能重复添加
}

// ✅ 正确:用 Set 去重
if (!seen.has(next)) {
  seen.add(next);
  heap.insert(next);
}

3. 数值溢出

javascript
// 在某些语言中需要注意
// JavaScript 的 Number 足够大,不会溢出
// 但如果用其他语言,需要用 long 类型

相关题目

题目难度关联点
263. 丑数简单判断是否为丑数
313. 超级丑数中等质因子由数组给定
1201. 丑数 III中等二分 + 容斥原理

小结

本题展示了两种生成有序序列的方法:

  1. 最小堆:通用方法,适合任意生成规则
  2. 多指针:利用序列的特殊结构,更高效

关键技巧

  • 理解丑数的递推性质:新丑数 = 旧丑数 × 质因子
  • 三指针确保不遗漏、不重复
  • 同一个值可能由多个指针生成,需要同时更新

这种多指针技巧还可以用于合并多个有序数组、生成其他有规律的序列等问题。

实战:丑数 II has loaded