|
| 1 | +# 题目描述(中等难度) |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +输出第 `n` 个丑数。 |
| 6 | + |
| 7 | +# 解法一 暴力 |
| 8 | + |
| 9 | +判断每个数字是否是丑数,然后数到第 `n` 个。 |
| 10 | + |
| 11 | +```java |
| 12 | +public int nthUglyNumber(int n) { |
| 13 | + int count = 0; |
| 14 | + int result = 1; |
| 15 | + while (count < n) { |
| 16 | + if (isUgly(result)) { |
| 17 | + count++; |
| 18 | + } |
| 19 | + result++; |
| 20 | + } |
| 21 | + //result 多加了 1 |
| 22 | + return result - 1; |
| 23 | +} |
| 24 | + |
| 25 | +public boolean isUgly(int num) { |
| 26 | + if (num <= 0) { |
| 27 | + return false; |
| 28 | + } |
| 29 | + while (num % 2 == 0) { |
| 30 | + num /= 2; |
| 31 | + } |
| 32 | + while (num % 3 == 0) { |
| 33 | + num /= 3; |
| 34 | + } |
| 35 | + while (num % 5 == 0) { |
| 36 | + num /= 5; |
| 37 | + } |
| 38 | + return num == 1; |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +不过题目没有那么简单,这样的话会超时。 |
| 43 | + |
| 44 | +受到 [204 题](https://leetcode.wang/leetcode-204-Count-Primes.html) 求小于 `n` 的素数个数的启发,我们这里考虑一下筛选法。先把当时的思路粘贴过来。 |
| 45 | + |
| 46 | +> 用一个数组表示当前数是否是素数。 |
| 47 | +> |
| 48 | +> 然后从 `2` 开始,将 `2` 的倍数,`4`、`6`、`8`、`10` ...依次标记为非素数。 |
| 49 | +> |
| 50 | +> 下个素数 `3`,将 `3` 的倍数,`6`、`9`、`12`、`15` ...依次标记为非素数。 |
| 51 | +> |
| 52 | +> 下个素数 `7`,将 `7` 的倍数,`14`、`21`、`28`、`35` ...依次标记为非素数。 |
| 53 | +> |
| 54 | +> 在代码中,因为数组默认值是 `false` ,所以用 `false` 代表当前数是素数,用 `true` 代表当前数是非素数。 |
| 55 | +
|
| 56 | +下边是当时的代码。 |
| 57 | + |
| 58 | +```java |
| 59 | +public int countPrimes(int n) { |
| 60 | + boolean[] notPrime = new boolean[n]; |
| 61 | + int count = 0; |
| 62 | + for (int i = 2; i < n; i++) { |
| 63 | + if (!notPrime[i]) { |
| 64 | + count++; |
| 65 | + //将当前素数的倍数依次标记为非素数 |
| 66 | + for (int j = 2; j * i < n; j++) { |
| 67 | + notPrime[j * i] = true; |
| 68 | + } |
| 69 | + } |
| 70 | + } |
| 71 | + return count; |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +这里的话,所有丑数都是之前的丑数乘以 `2, 3, 5` 生成的,所以我们也可以提前把后边的丑数标记出来。这样的话,就不用调用 `isUgly` 函数判断当前是否是丑数了。 |
| 76 | + |
| 77 | +```java |
| 78 | +public int nthUglyNumber(int n) { |
| 79 | + HashSet<Integer> set = new HashSet<>(); |
| 80 | + int count = 0; |
| 81 | + set.add(1); |
| 82 | + int result = 1; |
| 83 | + while (count < n) { |
| 84 | + if (set.contains(result)) { |
| 85 | + count++; |
| 86 | + set.add(result * 2); |
| 87 | + set.add(result * 3); |
| 88 | + set.add(result * 5); |
| 89 | + } |
| 90 | + result++; |
| 91 | + } |
| 92 | + return result - 1; |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +但尴尬的是,依旧是超时,悲伤。然后就去看题解了,分享一下别人的解法。 |
| 97 | + |
| 98 | +# 解法二 |
| 99 | + |
| 100 | +参考 [这里](https://leetcode.com/problems/ugly-number-ii/discuss/69372/Java-solution-using-PriorityQueue)。 |
| 101 | + |
| 102 | +看一下解法一中 `set` 的方法,我们递增 `result`,然后看 `set` 中是否含有。如果含有的话,就把当前数乘以 `2, 3, 5` 继续加到 `set` 中。 |
| 103 | + |
| 104 | +因为 `result` 是递增的,所以我们每次找到的其实是 `set` 中最小的元素。 |
| 105 | + |
| 106 | +所以我们不需要一直递增 `result` ,只需要每次找 `set` 中最小的元素。找最小的元素,就可以想到优先队列了。 |
| 107 | + |
| 108 | +还需要注意一点,当我们从 `set` 中拿到最小的元素后,要把这个元素以及和它相等的元素都删除。 |
| 109 | + |
| 110 | +```java |
| 111 | +public int nthUglyNumber(int n) { |
| 112 | + Queue<Long> queue = new PriorityQueue<Long>(); |
| 113 | + int count = 0; |
| 114 | + long result = 1; |
| 115 | + queue.add(result); |
| 116 | + while (count < n) { |
| 117 | + result = queue.poll(); |
| 118 | + // 删除重复的 |
| 119 | + while (!queue.isEmpty() && result == queue.peek()) { |
| 120 | + queue.poll(); |
| 121 | + } |
| 122 | + count++; |
| 123 | + queue.offer(result * 2); |
| 124 | + queue.offer(result * 3); |
| 125 | + queue.offer(result * 5); |
| 126 | + } |
| 127 | + return (int) result; |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +这里的话要用 `long`,不然的话如果溢出,可能会将一个负数加到队列中,最终结果也就不会准确了。 |
| 132 | + |
| 133 | +我们还可以用是 `TreeSet` ,这样就不用考虑重复元素了。 |
| 134 | + |
| 135 | +```java |
| 136 | +public int nthUglyNumber(int n) { |
| 137 | + TreeSet<Long> set = new TreeSet<Long>(); |
| 138 | + int count = 0; |
| 139 | + long result = 1; |
| 140 | + set.add(result); |
| 141 | + while (count < n) { |
| 142 | + result = set.pollFirst(); |
| 143 | + count++; |
| 144 | + set.add(result * 2); |
| 145 | + set.add(result * 3); |
| 146 | + set.add(result * 5); |
| 147 | + } |
| 148 | + return (int) result; |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +# 解法三 |
| 153 | + |
| 154 | +参考 [这里](https://leetcode.com/problems/ugly-number-ii/discuss/69362/O(n)-Java-solution)。 |
| 155 | + |
| 156 | +我们知道丑数序列是 `1, 2, 3, 4, 5, 6, 8, 9...`。 |
| 157 | + |
| 158 | +我们所有的丑数都是通过之前的丑数乘以 `2, 3, 5` 生成的,所以丑数序列可以看成下边的样子。 |
| 159 | + |
| 160 | + `1, 1×2, 1×3, 2×2, 1×5, 2×3, 2×4, 3×3...`。 |
| 161 | + |
| 162 | +我们可以把丑数分成三组,用丑数序列分别乘 `2, 3, 5` 。 |
| 163 | + |
| 164 | +```java |
| 165 | +乘 2: 1×2, 2×2, 3×2, 4×2, 5×2, 6×2, 8×2,9×2,… |
| 166 | +乘 3: 1×3, 2×3, 3×3, 4×3, 5×3, 6×3, 8×3,9×3,… |
| 167 | +乘 5: 1×5, 2×5, 3×5, 4×5, 5×5, 6×5, 8×5,9×5,… |
| 168 | +``` |
| 169 | + |
| 170 | +我们需要做的就是把上边三组按照顺序合并起来。 |
| 171 | + |
| 172 | +合并有序数组的话,可以通过归并排序的思想,利用三个指针,每次找到三组中最小的元素,然后指针后移。 |
| 173 | + |
| 174 | +当然,最初我们我们并不知道丑数序列,我们可以一边更新丑数序列,一边使用丑数序列。 |
| 175 | + |
| 176 | +```java |
| 177 | +public int nthUglyNumber(int n) { |
| 178 | + int[] ugly = new int[n]; |
| 179 | + ugly[0] = 1; // 丑数序列 |
| 180 | + int index2 = 0, index3 = 0, index5 = 0; //三个指针 |
| 181 | + for (int i = 1; i < n; i++) { |
| 182 | + // 三个中选择较小的 |
| 183 | + int factor2 = 2 * ugly[index2]; |
| 184 | + int factor3 = 3 * ugly[index3]; |
| 185 | + int factor5 = 5 * ugly[index5]; |
| 186 | + int min = Math.min(Math.min(factor2, factor3), factor5); |
| 187 | + ugly[i] = min;//更新丑数序列 |
| 188 | + if (factor2 == min) |
| 189 | + index2++; |
| 190 | + if (factor3 == min) |
| 191 | + index3++; |
| 192 | + if (factor5 == min) |
| 193 | + index5++; |
| 194 | + } |
| 195 | + return ugly[n - 1]; |
| 196 | +} |
| 197 | +``` |
| 198 | + |
| 199 | +这里需要注意的是,归并排序中我们每次从两个数组中选一个较小的,所以用的是 `if...else...`。 |
| 200 | + |
| 201 | +这里的话,用的是并列的 `if` , 这样如果有多组的当前值都是 `min`,指针都需要后移,从而保证 `ugly` 数组中不会加入重复元素。 |
| 202 | + |
| 203 | +# 总 |
| 204 | + |
| 205 | +解法二的话自己其实差一步就可以想到了。 |
| 206 | + |
| 207 | +解法三又是先通过分类,然后有一些动态规划的思想,用之前的解更新当前的解。 |
0 commit comments