1.题目
2.思路
这道题二刷还是忘记了最佳方法是动态规划,最小堆也没想起来,只会暴力搜索(两种思路的暴力)。值得反思!!!
1.超时--O(2 ^32)
第一个想法是,遍历所有的数字,然后判断这个数是不是丑数。大概范围是从【0, 2^31 - 1]查找。这样的时间复杂度是O(2 ^32).大于 4 * 10^9.肯定超时!超时超时超时!!!
2.勉强--O(n * n)
第二个想法是,先找第一个丑数1,再找第二个丑数2,再找第三个丑数3,依次挨个找下去。每一个丑数肯定来源于前面已经出现的丑数。所以挨个遍历已经出现的丑数可以得到答案,时间复杂度为O(n * n) == 1690 * 1690.勉强不超时。800ms....然后肯定能优化的,想不起来了。。。
3.小顶堆--O(nlogn)
第三个思路是,用小顶堆来解决。当x是丑数时,那么2 * x, 3 * x, 5 * x也必然是丑数,加入优先队列即可。注意可能有重复的值,所以加一个哈希表或者set去重,时间复杂度为O(n * (logn + log3n) ).堆弹出堆顶元素,logn,每次弹出一个元素, 都有可能加入3个元素,所以最终堆里面可能有3 * n个元素,所以是log3n.而不是3 *logn.所以最终时间复杂度O(nlogn)
class Solution {
public int nthUglyNumber(int n) {
int ans = 0;
if(n == 1)return n;
//最小堆。
// x 是丑数,2x, 3x, 5x也是丑数
Queue<Long>q = new PriorityQueue();
Map<Long, Integer>map = new HashMap();
q.add((long)1);
int cnt = 0 ;
int[]arrs = new int[]{2, 3, 5};
while(!q.isEmpty()){
long cur = q.poll();
cnt ++;
if(cnt == n) return (int)cur;
for(int arr : arrs){
if(map.getOrDefault(arr * cur, -1) != -1)
continue;
else{
q.add(arr * cur);
map.put(arr * cur, 1);
}
}
}
return -1;
}
}
4.动态规划--O(n)
第四个思路,更加巧妙!!一维的动态规划!dp[i] 定义为第i个丑数为dp[i].那么状态转移函数应该怎么写呢?巧妙的是定义了三个指针p2, p3, p5,分别指向上一个丑数的位置。一开始都指向1.
dp[i] = Math.min(dp[p2] * 2, dp[p3] * 3, dp[p5] * 5).p2, p3, p5,代表每次使用2, 3, 5最后的丑数位置。如果dp[i] == dp[p2] * 2,那么说明下一个丑数是用了2,所以p2++.同理p3, p5.
这个的基础还是来源于下一个丑数必然来源于上一个丑数 * {2, 3, 5}中的一个。用三个指针p2, p3, p5减少了重复数据的计算,保证了每次得到的丑数都是最小且递增的。所以跟小顶堆相比减少了从堆里查找最小的丑数的时间,所以时间从O(nlogn) 到O(n).
class Solution {
public int nthUglyNumber(int n) {
int ans = 0;
if(n == 1)return n;
// 动态规划
int p2 = 1, p3 = 1, p5 = 1;
int[]dp = new int[n + 1]; // dp[i]表示第i个丑数
dp[1] = 1;
for(int i = 2 ; i < n + 1 ; i++){
// 核心
dp[i] = Math.min(Math.min(dp[p2] * 2, dp[p3] * 3), dp[p5] * 5);
if(dp[i] == dp[p2] * 2)
p2++;
if(dp[i] == dp[p3] * 3)
p3++;
if(dp[i] == dp[p5] * 5)
p5++;
}
return dp[n];
}
}
3.结果