描述
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第 n个丑数。
数据范围: 0 ≤ n ≤ 2000
要求:空间复杂度 O(n) , 时间复杂度 O(n)
方法一:最小堆(推荐使用)
知识点1:优先队列
优先队列即PriorityQueue,是一种内置的机遇堆排序的容器,分为大顶堆与小顶堆,大顶堆的堆顶为最大元素,其余更小的元素在堆下方,小顶堆与其刚好相反。且因为容器内部的次序基于堆排序,因此每次插入元素时间复杂度都是O(log2n),而每次取出堆顶元素都是直接取出。
知识点2:哈希表
哈希表是一种根据关键码(key)直接访问值(value)的一种数据结构。而这种直接访问意味着只要知道 key 就能在 O(1) 时间内得到 value,因此哈希表常用来统计频率、快速检验某个元素是否出现过等。
思路:
我们都知道如果 x 是丑数,则2x 、3x 、5x 都是丑数,丑数也是从1开始由每个丑数这样构建而来的,我们要做的就是找到这样的前 n 个数,即最小的 n 个。
整体排序不现实,但是我们可以利用小顶堆,即优先队列,每次取出堆顶元素一定是最小的,一共取n 次就可以了,每次取出来的元素我们分别乘2、乘3、乘5后入堆,即作为之后要访问的数字,当然为了防止重复比如2∗3=6 、3∗2=6 ,我们还要用哈希表去重。
这里面有的数字会超过int的表示范围,因此哈希表和小顶堆都用long。
具体做法:
- step 1:使用小顶堆记录即将从小到大访问的丑数,哈希表去重,数组记录2、3、5乘数因子。
- step 2:数字1作为第一个丑数,首先入堆,后面的丑数都是其不断乘上2、3、5的结果。
- step 3:每次依次从小顶堆中弹出最小的元素,一共弹出n次。
- step 4:对于每个弹出的元素,可以用起构造后面的丑数,即分别乘上2、3、5,若是不重复则加入堆中排队等到访问。
代码:
class Solution {
public:
int GetUglyNumber_Solution(int index) {
//排除0
if(index == 0)
return 0;
//要乘的因数
vector<int> factors = {2, 3, 5};
//去重
unordered_map<long, int> mp;
//小顶堆
priority_queue<long, vector<long>, greater<long>> pq;
//1先进去
mp[1LL] = 1;
pq.push(1LL);
long res = 0;
for(int i = 0; i < index; i++){
//每次取最小的
res = pq.top();
pq.pop();
for(int j = 0; j < 3; j++){
//乘上因数
long next = res * factors[j];
//只取未出现过的
if(mp.find(next) == mp.end()){
mp[next] = 1;
pq.push(next);
}
}
}
return (int)res;
}
};
运行时间:3ms
超过35.51% 用C++提交的代码
占用内存:652KB
超过44.66%用C++提交的代码
复杂度分析:
时间复杂度:O(nlog2n),一共循环 n 次,取 n 次最小值,每次循环中最多有3次 入堆操作,每次入堆都是O(log2n) ,哈希表的操作是 O(1)
空间复杂度: O(n),哈希表和小顶堆最大空间为 3∗n 的长度
方法二:动态规划(扩展思路)
知识点:动态规划
动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。
思路:
我们知道丑数是由1开始的每个丑数依次乘上2、3、5得到,而我们每次只需要在其中找到最小的一个,一共找nnn次即可。我们可以用iii、jjj、kkk三个下标表示在已经找到的丑数中那个数分别被乘2、乘3、乘5有无被记录过,然后依次找nnn个数字就可以了。
具体做法:
- step 1:第一个丑数1加入数组。
- step 2:使用i、j、k三个索引表示该数字有无被乘2、乘3、乘5.
- step 3:后续继续找n−1n-1n−1个丑数,每次取当前丑数索引乘2、乘3、乘5的最小值加入数组,并计数。
- step 4:若是该丑数为相应索引乘上某个数字,则对应的索引往后一位。
图示:
代码:
class Solution {
public:
//寻找三个数中的最小值
int findMin(int x, int y, int z){
int res = x;
res = y < res ? y : res;
res = z < res ? z : res;
return res;
}
int GetUglyNumber_Solution(int index) {
//排除0
if(index == 0)
return 0;
//按顺序记录丑数
vector<int> num;
num.push_back(1);
//记录这是第几个丑数
int count = 1;
//分别代表要乘上2 3 5的下标
int i = 0, j = 0, k = 0;
while(count < index){
//找到三个数中最小的丑数
num.push_back(findMin(num[i] * 2, num[j] * 3, num[k] * 5));
count++;
//由2与已知丑数相乘得到的丑数,那该下标及之前的在2这里都用不上了
if(num[count - 1] == num[i] * 2)
i++;
//由3与已知丑数相乘得到的丑数,那该下标及之前的在3这里都用不上了
if(num[count - 1] == num[j] * 3)
j++;
//由5与已知丑数相乘得到的丑数,那该下标及之前的在5这里都用不上了
if(num[count - 1] == num[k] * 5)
k++;
}
return num[count - 1];
}
};
运行时间:3ms
超过35.51% 用C++提交的代码
占用内存:524KB
超过66.45%用C++提交的代码
复杂度分析:
时间复杂度: O(n),只需要遍历一次
空间复杂度: O(n),记录丑数的数组最大长度为 n
官方解释~