JZ33_丑数

题目陈述

描述:把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

算法一:质因数分解(暴力)

算法实现

  • 一个很朴素的做法
  • 1 ∼ ∞ 1 \sim \infty 1每次+1,一直枚举,直到找到地N个丑数为止
  • 那么还有一个待解决的问题,如何判断当前数字是不是丑数呢?
  • 我们总结一下丑数的性质:只能分解为2,3,5的如干次幂相乘的数,即设第 i i i个丑数为 u n u_n un,则 u n = 2 x × 3 y × 5 z u_n=2^x \times 3^y \times 5^z un=2x×3y×5z
  • 那么我们只需要通过质因数分解,判断他分解2,3,5后,是否为1,如果为1,则说明没有其他的因数,否则则有其他因数,那么他就不是一个丑数

代码实现

class Solution {
public:
    bool check(int x){//判断x是否是仅由2,3,5三个因子组成的数字
        while(x%2==0)x/=2;
        while(x%3==0)x/=3;
        while(x%5==0)x/=5;
        return x==1;//如果x此时不为1,则说明x还含有其他的质因数
    }
    int GetUglyNumber_Solution(int idx) {
        int now=1;
        vector<int> v(1,1);//放入1个1
        while(1){
            if(v.size()==idx){//找到地idx个丑数
                return v[v.size()-1];
            }
            now++;
            if(check(now)){//now为丑数
                v.push_back(now);//now入队
            }
        }
    }
};

复杂度分析

  • 时间复杂度 O ( u n ) O(u_n) O(un),其中 u n u_n un为第n个丑数的大小,因为丑数的可能会达到INT_MAX那么大,所以这个算法最坏,可能是一个1e9(甚至更高)级别的算法
  • 只能通过11/13的数据,所以我们仍需要寻找更优的算法

算法二:集合+优先队列

思路引入

  • 我们试一下能否找到相邻丑数之间的规律,或者丑数生成的规律
  • 比较遗憾的是,我们通过模拟发现,相邻的丑数之间并没有规律,那么这题的另一个切入点,就是生成丑数

思路推进

  • 我们可以发现,对于 u n , ( n > 1 ) u_n,(n>1) un,(n>1),它必然是由 u i ( i ∈ [ 1 , n − 1 ] ) u_i(i\in [1,n-1]) ui(i[1,n1])乘以2或3或5生成的
  • 如果对于 i i i也有 i > 1 i>1 i>1,那么必然也有 u j , ( j ∈ [ 1 , i − 1 ] ) u_j,(j \in [1,i-1]) uj,(j[1,i1])乘以2或3或5生成 u i u_i ui
  • 所以,如果知道前面n-1个丑数,我们可以每个数都乘以2,3,5,然后检查出里面跟前面n-1个丑数不重复的并且是最小的数,得到的便是第n个丑数

考虑复杂度

  • 不借助set,每次检查重复的复杂度为 O ( n ) O(n) O(n),每个丑数生成三个新的,最多有 ( n − 1 ) (n-1) (n1)个丑数,时间复杂度 O ( 3 n 2 ) O(3n^2) O(3n2)
  • 如果借助set去重,每次检查重复的复杂度为 O ( log ⁡ n ) O(\log n) O(logn),时间复杂度 O ( 3 n log ⁡ n ) O(3n \log n) O(3nlogn)
  • 取出最小值,如果借助堆的话,对于维护堆,每次插入一个数,花费 O ( l o g ( 3 n ) ) O(log(3n)) O(log(3n)),最多插入3n次,每次取出最小值,花费 O ( 1 ) O(1) O(1)
  • 当然这个算法是可以AC的,我瞅了一眼,题解区貌似只有我一个人题解写了这个算法
    在这里插入图片描述
  • 事实上,如果直接看其他题解的正解,三指针做法,会觉得自己一下子似懂非懂
  • 实际比赛的时候,除非是巨强的神犇,几乎人没有能一下子就想到最后的三指针算法,不然往往都会有一个思路递进的过程

代码实现

  • 注意,此处,进入小顶堆的元素可能会有重复,比如(23和32),所以我们需要去重,这一点我们可以用STL容器中的set,内嵌红黑树,begin即是最小的元素,插入和删除的代价都是 O ( log ⁡ 2 n ) O(\log 2n) O(log2n)
typedef long long ll;
class Solution {
	public:
		int GetUglyNumber_Solution(int idx) {
			if(idx<1)return 0;
			set<ll> s,s_q;//用set来表示队列,同时起到去重的作用 
			s.insert(1);
			vector<ll> v(1,1);//放入1个1
			int p=0;
			int e[3]= {2,3,5};
			while(v.size()<idx) {
				for(int i=0; i<3; i++) {
					if(s.find(e[i]*v[p])==s.end()) {//该新元素,未在前面出现过 
						s_q.insert(e[i]*v[p]);//此处还需要考虑堆中有重复元素 
					}
				}
				p++; 
				v.push_back(*s_q.begin());//插入新丑数 
				s.insert(*s_q.begin());//插入新丑数 
				s_q.erase(s_q.begin());//弹出堆中最小值 
			}
			return v[idx-1];
		}
};
  • 接下来,我们思路进一步递进,即我们的正解,三指针做法

算法三:三指针做法

算法思路

  • 我们会发现判断是否跟前面重复这个过程,以及维护小顶堆,会花费大量时间,不妨想一想能不能省略去这个过程?
  • 我们可以发现,如果已经知道[1~i]个丑数,假如i足够小( i ∗ 5 < u n i*5<u_n i5<un),那么是不是 u 1 ∼ u i u_1 \sim u_i u1ui每个数都会乘以2,3,5再次放入这个队列中
  • 如果当前数是由 u n = u j ∗ 2 u_n=u_j*2 un=uj2得到的,那么下一个因为乘以2而得到的丑数必然是由 u j + 1 ∗ 2 u_{j+1}*2 uj+12得到的(后面的数乘以2,必然大于这个数),对于3,5同理
  • 所以我们可以利用这个单调性维护三个指针,依次比较三个指针所指向的数所生成的新丑数,即可 O ( n ) O(n) O(n)得出第n个丑数
  • 即维护i,j,k指针,其中i,j,k分别为指向下一个*2,*3,*5可能成为下一个丑数的数的位置的指针,我们就可以在三个指针所对应的数的乘以相应的数的运算结果中,找到下一个丑数

动画演示

在这里插入图片描述

代码实现

  • 注意,下面的if,不能写成if-else,因为可能出现v[i]*2==v[j]*3这样的情况,这种情况我们就需要同时移动i,j
  • 否则,数组v中就可能出现重复的元素,导致错误答案

C++

class Solution {
public:
    int GetUglyNumber_Solution(int idx) {
        int i=0,j=0,k=0,now;//i,j,k分别为指向下一个*2,*3,*5可能成为下一个丑数的数的位置的指针
        vector<int> v(1,1);//放入1个1
        while(v.size()<idx){//v中的数量为为idx时候,停止循环
            now=min(v[i]*2,min(v[j]*3,v[k]*5));//三个指针运算的结果中找,下一个丑数
            v.push_back(now);//将下一个丑数入队
            if(v[i]*2==now)i++;//下一个丑数可以由v[i]*2得到,则i指针后移
            if(v[j]*3==now)j++;//下一个丑数可以由v[j]*3得到,则j指针后移
            if(v[k]*5==now)k++;//下一个丑数可以由v[k]*5得到,则k指针后移
            //此处不能写if -else ,因为可能存在v[i]*2==v[j]*3这种情况
            //那么在下一次循环中,v[j]*3就会被再次选中,这样就会造成v中有重复元素出现
        }
        return v[idx-1];//此处元素不能写now,当idx==1时被hack
    }
};

Python

class Solution:
    def GetUglyNumber_Solution(self, idx):
        if idx<1:#非法输入的情况
            return 0
        lst=[1]
        i=0#i为指向下一个*2可能成为下一个丑数的数的位置的指针
        j=0#j为指向下一个*3可能成为下一个丑数的数的位置的指针
        k=0#k为指向下一个*5可能成为下一个丑数的数的位置的指针
        while len(lst)<idx :#当得到第idx个丑数的时候,循环停止
            now=min(lst[i]*2,lst[j]*3,lst[k]*5)#三个指针运算的结果中找,下一个丑数
            lst.append(now)#将下一个丑数入队
            if now==lst[i]*2:#下一个丑数可以由v[i]*2得到,则i指针后移
                i+=1
            if now==lst[j]*3:#下一个丑数可以由v[j]*3得到,则j指针后移
                j+=1
            if now==lst[k]*5:#下一个丑数可以由v[k]*5得到,则k指针后移
                k+=1
        return lst[idx-1]#返回答案,如果idx==1,now没有定义,依旧会CE,所以此处不能写now
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值