264. 丑数 II

126 阅读3分钟

题目介绍

力扣264题:leetcode-cn.com/problems/ug…

image.png

方法一:最小堆

要得到从小到大的第 nn 个丑数,可以使用最小堆实现。初始时堆为空。首先将最小的丑数 11 加入堆。

每次取出堆顶元素 x,则 x 是堆中最小的丑数,由于 2x, 3x, 5x2x,3x,5x 也是丑数,因此将 2x, 3x, 5x 加入堆。

上述做法会导致堆中出现重复元素的情况。为了避免重复元素,可以使用哈希集合去重,避免相同元素多次加入堆。在排除重复元素的情况下,第 n 次从最小堆中取出的元素即为第 n 个丑数。

代码如下:

class Solution {
    public int nthUglyNumber(int n) {
        int[] factors = {2, 3, 5};
        Set<Long> seen = new HashSet<Long>();
        PriorityQueue<Long> heap = new PriorityQueue<Long>();
        seen.add(1L);
        heap.offer(1L);
        int ugly = 0;
        for (int i = 0; i < n; i++) {
            //弹出当前最小丑数
            long curr = heap.poll();
            ugly = (int) curr;
            for (int factor : factors) {
                long next = curr * factor;
                if (seen.add(next)) {
                    heap.offer(next);
                }
            }
        }
        return ugly;
    }
}

复杂度分析

  • 时间复杂度:O(nlogn)。得到第n 个丑数需要进行 n 次循环,每次循环都要从最小堆中取出 1 个元素以及向最小堆中加入最多 3 个元素,因此每次循环的时间复杂度是 O(log3n+3log3n)=O(logn),总时间复杂度是 O(nlogn)。

  • 空间复杂度:O(n)。空间复杂度主要取决于最小堆和哈希集合的大小,最小堆和哈希集合的大小都不会超过 3n。

方法二:动态规划

该思路与[313. 超级丑数]的解题思路类似, 只需要把代码稍加修改即可,代码如下:

class Solution {
    public int nthUglyNumber(int n) {
        int[] primes = new int[]{2,3,5};
        int pLen = 3;
        int[] indexes = new int[pLen];

        int[] dp = new int[n];
        dp[0] = 1;
        for (int i = 1; i < n; i++) {
            // 因为选最小值,先假设一个最大值
            dp[i] = Integer.MAX_VALUE;
            for (int j = 0; j < pLen; j++) {
                dp[i] = Math.min(dp[i], dp[indexes[j]] * primes[j]);
            }

            // dp[i] 是之前的哪个丑数乘以对应的 primes[j] 选出来的,给它加 1
            for (int j = 0; j < pLen; j++) {
                if (dp[i] == dp[indexes[j]] * primes[j]) {
                    // 注意:这里不止执行一次,例如选出 14 的时候,2 和 7 对应的最小丑数下标都要加 1,大家可以打印 indexes 和 dp 的值加以验证
                    indexes[j]++;
                }
            }
        }
        return dp[n - 1];
    }
}

不过上述代码还是比较复杂,由于质数数组只有2,3,5,思路还可以这样:

定义数组 dp,其中 dp[i] 表示第 i 个丑数,第 n 个丑数即为 dp[n]。

由于最小的丑数是 1,因此 dp[1]=1。

如何得到其余的丑数呢?定义三个指针 p2,p3,p5,表示下一个丑数是当前指针指向的丑数乘以对应的质因数。初始时,三个指针的值都是 1。

当 2 ≤ i ≤n 时,令 dp[i] = min(dp[p2] × 2,dp[p3] × 3,dp[p5] × 5),然后分别比较dp[i]dp[p2]×2,dp[p3]×3,dp[p5]×5 是否相等,如果相等则将对应的指针加 1。

代码如下:

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 num2 = dp[p2] * 2, num3 = dp[p3] * 3, num5 = dp[p5] * 5;
            dp[i] = Math.min(Math.min(num2, num3), num5);
            if (dp[i] == num2) {
                p2++;
            }
            if (dp[i] == num3) {
                p3++;
            }
            if (dp[i] == num5) {
                p5++;
            }
        }
        return dp[n];
    }
}

复杂度分析

  • 时间复杂度:O(n)。需要计算数组 dp 中的 n 个元素,每个元素的计算都可以在 O(1) 的时间内完成。

  • 空间复杂度:O(n)。空间复杂度主要取决于数组 dp 的大小。