[数学 堆 动态规划] 263. 丑数 264. 丑数 II(暴力解 → 小顶堆 → 动态规划+三指针)
263. 丑数(判断是否为丑数)
题目链接:https://leetcode-cn.com/problems/ugly-number/
分类:
- 数学(丑数的特点:只包含质因数2,3,5)
- 递归实现、迭代实现(拿2,3,5循环除num)
思路:数学
丑数的特点是只包含质因数2,3,5,且1是丑数。
对于给定的输入num,要判断num是不是丑数,就不断拿2,3 ,5去除num,如果能被其中某个质因数整除,就拿整除结果继续除以2,3,5:
- 如果num最终 == 1,说明num本身为1或能够被2,3,5整除,属于丑数,返回true;
- 如果num最总 == 0,说明num不能被2,3,5整除,不属于丑数,返回false。
根据上面的思路,可以得出递归实现和迭代实现:
//递归实现
class Solution {
public boolean isUgly(int num) {
if(num == 1) return true;
if(num <= 0 ) return false;
//递归判断一个数分别能否被2,3,5整除,且整除之后的结果仍能被2,3,5整除,,,直到商=1
if(num % 2 == 0){
return isUgly(num/2);
}
else if(num % 3 == 0){
return isUgly(num / 3);
}
else if(num % 5 == 0){
return isUgly(num / 5);
}
//2,3,5都不能整除它,所以返回false
return false;
}
}
/*
迭代实现:
按照2,3,5 的顺序依次循环除num,当除到不是当前因数的倍数时,
就进行下一个因数的整除,这样,最后剩下的数为1则为丑数
*/
class Solution {
public boolean isUgly(int num) {
if(num == 0) return false;
//将2,3,5存入数组便于循环使用
while(num % 2 == 0) num /= 2;
while(num % 3 == 0) num /= 3;
while(num % 5 == 0) num /= 5;
return num == 1;
}
}
264. 丑数 II(寻找第n个丑数)
题目链接:https://leetcode-cn.com/problems/ugly-number-ii/
分类:
- 数学(丑数:只包含质因子2,3,5的正整数;暴力解:每个数字都判断是否为丑数,直到找到第n个丑数);
- 堆(PriorityQueue作为小顶堆存放丑数;顶部 = 当前的最小丑数;HashSet去重);
- 动态规划(dp数组按升序存放丑数、三指针法构造dp数组)。
题目分析
这题和263相比,需要找出从小到大排序的第n个丑数,因此涉及到丑数的排序和存储:
- 元素的存储和排序可以使用堆这一数据结构同时解决(思路2);
- 也可以使用动态规划,但是动态规划的dp数组在构造时还需要考虑排序问题,本题使用三指针法解决dp元素的排序问题(思路3)。
思路1:暴力解
设置一个计数器,初始值 = 0,然后每个数字都判断是否只包含2,3,5质因数,如果当前数字是丑数,计数器 + 1,直到计数器 == n,当前数字就是第n个丑数。
这里不再赘述。
思路2:小顶堆(推荐)
创建一个小根堆用于存放丑数,每次获取顶部就是获取当前最小的丑数。堆最先存放1,然后依次存放2,3,5以及这三个数的2,3,5倍。
算法流程:
初始时计数器count = 0,每次从小根堆中取出一个当前最小的元素时count + 1,直到count == n,即此时弹出的小根堆顶部就是第n个丑数。
同时每从堆中取一个数时,就把当前数的2,3,5倍都入堆,前提是要做去重,可以用set来实现。
为什么当count == n时,小顶堆顶部就是第n个丑数?
当第 i 个数从堆中取出来时,又会同时向堆中加入当前弹出元素的2,3,5倍数字,当前弹出的元素是堆里最小的元素,它的2倍必然是set集合元素之外最小的丑数了;且每个数弹出时,又会加入3个丑数进去,所以当count == n时我们从一开始到现在找到的丑数总数(可能在堆内,也可能已出堆)一定是 > n的,所以当计数器count == n时必然能从中取到第n个丑数。
实现遇到的问题
1、数值溢出问题
丑数的计算过程中可能出现数值 > int的上界,所以我们可以将小顶堆、HashSet的泛型设置为Long,计算每个丑数时也用 long型的变量来存放。
2、丑数去重问题
我们按算法流程生成丑数时,可能会得到重复的丑数,例如:丑数2的3倍=6也是丑数,加入堆中;丑数3的2倍=6,已经出现过,所以不能重复加入堆中。
我们可以使用一个Set来存放已经出现过的丑数来达到去重的目的。
也可以将PriorityQueue + Set替换成TreeSet,可以同时达到排序和去重的效果。
实现代码:
class Solution {
int[] array = {2,3,5};
public int nthUglyNumber(int n) {
//特殊用例
if(n <= 0) return -1;
//小顶堆存放丑数,配合计数器找到第n个出堆的元素就是第n个丑数
Queue<Long> queue = new PriorityQueue<>();//优先级队列默认是小顶堆
//set用于去重,已经生成过的丑数就不再加入堆,以免影响n的判断
Set<Long> set = new HashSet<>();
//将1加入队列
queue.offer((long)1);
set.add((long)1);
int count = 1;//计数器
long num = 0;//用于存放堆弹出的顶部,便于最后返回
while(count <= n){
//将堆顶部弹出,此时顶部是当前最小丑数(除已出堆以外的所有丑数中最小)
num = queue.poll();
//弹出顶部时,将它的2,3,5倍丑数都加入到堆和set中,加入前先去重
for(int i = 0; i < 3; i++){
long temp = num * array[i];
if(!set.contains(temp)){
queue.offer(temp);
set.add(temp);
}
}
//弹出一个元素,计数器才+1
count++;
}
return (int)num;
}
}
思路3:动态规划-三指针法(推荐)
丑数数量是有限的,且可以通过一定规则生成:前面的丑数就作为后面数生成时的因子,关键在于大小的排列。
使用三个指针 i_2, i_3和 i_5,标记所指向丑数要乘以的因子。
算法很简单:设置一个dp数组按升序存放丑数,在 2nums[i_2],3nums[i_3]和2*nums[i_5]中选出最小的丑数并添加到数组中。并将该丑数对应的因子指针往前走一步。重复该步骤直到计算出第n个丑数。
参考:https://leetcode-cn.com/problems/ugly-number-ii/solution/chou-shu-ii-by-leetcode/
class Solution {
public int nthUglyNumber(int n) {
//特殊用例
if(n <= 0) return -1;
//数组存放计算过的丑数,以便后续直接使用
int[] ugly = new int[1690];
ugly[0] = 1;
//三指针各自从0开始移动
int i_2 = 0, i_3 = 0, i_5 = 0;
for(int i = 1; i < n; i++){
int factor2 = ugly[i_2] * 2;
int factor3 = ugly[i_3] * 3;
int factor5 = ugly[i_5] * 5;
int min = Math.min(Math.min(factor2,factor3) , factor5);
ugly[i] = min;
if(min == factor2) i_2++;
if(min == factor3) i_3++;
if(min == factor5) i_5++;
}
return ugly[n - 1];
}
}