🔥题目
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
输入:20
输出:36
解释:前20个丑数为 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 16, 18, 20, 24, 25, 27, 30, 32, 36。
☘️解析
解法1:最小堆
使用一个最小堆(优先队列),先加入一个1进行初始化。
每次取堆顶元素x(x是当前堆中最小的丑数),然后把丑数 2x、3x、5x 加入堆中。那么,当你第n次poll的时候,取出的就是第n个丑数啦!
但注意,上述操作会向堆中加入重复的元素。我们还需要使用一个哈希集合避免重复。
解法2:三指针
定义三个指针,它们彼此独立的指向已有的丑数数组。
要求第n个丑数(此时前n-1个丑数的数组已存在),求出三个候选数,最小值就是第n个丑数。这三个候选数分别根据三个指针求出:nums[p2] * 2
、nums[p3] * 3
、nums[p5] * 5
。
如果一个候选数称为了第i个丑数,那么其对应的指针+1。接着进行下一次候选比拼,求第n+1个丑数…
思路大概就是这样,其实这是一个动态规划,称为三指针也是比较生动贴切的。
🧊代码
解法1:最小堆
class Solution {
public int nthUglyNumber(int n) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
Set<Integer> set = new HashSet<>();
heap.offer(1);
set.add(1);
int[] items = {2, 3, 5};
for (int i = 0; i < n - 1; i++) {
int ugly = heap.poll();
for (int item : items) {
int next = ugly * item;
// 下面这个写法有点儿妙
if (set.add(next)) {
heap.offer(next);
}
}
}
return heap.poll();
}
}
解法2:三指针
class Solution {
public int nthUglyNumber(int n) {
int[] nums = new int[n];
nums[0] = 1;
int p2 = 0;
int p3 = 0;
int p5 = 0;
for (int i = 1; i < n; i++) {
int num2 = nums[p2] * 2;
int num3 = nums[p3] * 3;
int num5 = nums[p5] * 5;
nums[i] = Math.min(num2, Math.min(num3, num5));
if (nums[i] == num2) {
p2++;
}
if (nums[i] == num3) {
p3++;
}
if (nums[i] == num5) {
p5++;
}
}
return nums[n - 1];
}
}
🌸补充
最小堆法时间复杂度:O(nlogn) 。找第n个丑数需要从堆里取n次,向堆中放3n次(如果不考虑细节),取n次是 O(n) 的复杂度,放的时候需要在堆中自动排序,是 O(3logn) —— 合起来看时间复杂度是 O(n * 3logn) = O(nlogn) 。
三指针法时间复杂度:O(n) 。需要计算数组中的第n个元素,每个元素的计算都可以在 O(1) 的时间内完成。为什么比最小堆法复杂度更低呢?
最小堆的方法是先存再排,三指针的方法则是先排再存。
这两种思路在排序时有重大区别。最小堆在排序时,是一堆数字在排序,因此是 O(logn) 的排序时间;三指针法在排序时,是3个数字在排序,因此是 O(1) 的排序时间。最终两种方法的总时间复杂度是 O(nlong) 和 O(n) 。