剑指Offer49—丑数

剑指Offer49—丑数

题意

我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。

丑数即只能被 2、3、5 整除。判断一个数是不是丑数的代码如下:

bool is_uglyNumber(int num)
{
    while(num%2==0)
        num=num/2;
    while(num%3==0)
        num=num/3;
    while(num%5==0)
        num=num/5;
    return num==1;
}

法1—最小堆

要得到从小到大的第 n 个丑数,可以使用最小堆实现。初始时堆为空。首先将最小的丑数 1 加入堆。每次取出堆顶元素 x,则 x 是堆中最小的丑数,由于 2x, 3x, 5x 也是丑数,因此将 2x, 3x, 5x加入堆。

上述做法会导致堆中出现重复元素的情况。为了避免重复元素,可以使用哈希集合(unordered_set)去重,避免相同元素多次加入堆。

在排除重复元素的情况下,第 n 次从最小堆中取出的元素即为第 n 个丑数。

C++实现

class Solution 
{
public:
    int nthUglyNumber(int n) 
    {
        if(n<=1)
            return n;
        //用来与当前取出的丑数相乘,得到之后的丑数
        vector<int> factors{2,3,5}; 
        //最小堆,用来存放丑数,由于丑数范围可能超出int,所以用long
        priority_queue<long,vector<long>,greater<long>> min_heap;
        //使用集合去重
        unordered_set<long> set;
        //第n个丑数
        int ugly_number;
        
        //首先将最小的丑数加入最小堆和集合中
        set.insert(1L); //1L表示long类型的1
        min_heap.push(1L);

        for(int i=0;i<n;++i)
        {
            long temp = min_heap.top();
            min_heap.pop();
            ugly_number=static_cast<int>(temp);
            for(auto& num:factors)
            {
                long next_guly_number=num*temp;
                //如果哈希集合里面没有重复的丑数
                if(set.find(next_guly_number)==set.end())
                {
                    min_heap.push(next_guly_number);
                    set.insert(next_guly_number);
                }
            }
        }
        return ugly_number;

    }
};

复杂度分析 :

  • 时间复杂度:O(nlogn)。得到第 n 个丑数需要进行 n 次循环,每次循环都要从最小堆中取出 1 个元素以及向最小堆中加入最多 3 个元素,因此每次循环的时间复杂度是 O(logn+log3n)=O(logn),总时间复杂度是O(nlogn)。
  • 空间复杂度:O(n)。空间复杂度主要取决于最小堆和哈希集合的大小,最小堆和哈希集合的大小都不会超过 3n

法2—动态规划

方法一使用最小堆,会预先存储较多的丑数,导致空间复杂度较高,维护最小堆的过程也导致时间复杂度较高。可以使用动态规划的方法进行优化。

定义数组 dp,其中 dp[i] 表示第 i 个丑数,第 n 个丑数即为 dp[n]。

由于最小的丑数是 1,因此dp[1]=1。如何得到其余的丑数呢?我们知道,丑数只包含因子 2, 3, 5,因此有 “丑数 == 某较小丑数 × 某因子”(例如:10=5*2,较大丑数10等于较小丑数5*2这个因子)。

定义三个指针 p2​,p3​,p5​,表示下一个丑数是当前指针(p2、p3、p5)指向的丑数乘以对应的质因数 中 最小的一个。

初始时,三个指针的值都是 1。当 2 ≤ i ≤n 时,令:dp[i] = min( dp[p2]*2,dp[p3]*3,dp[p5]*5 ),然后分别比较 dp[i]和 dp[p2]*2、dp[p3]*3、dp[p5]*5相不相等,如果相等则将对应的指针加 1。


三指针解释:

例如 n = 10, primes = [2, 3, 5]。 打印出丑数列表:1, 2, 3, 4, 5, 6, 8, 9, 10, 12

首先一定要知道,后面的丑数一定由前面的丑数乘以2,或者乘以3,或者乘以5得来。例如,8,9,10,12一定是1, 2, 3, 4, 5, 6乘以2,3,5三个质数中的某一个得到。

这样的话我们的解题思路就是:从第一个丑数开始,一个个数丑数,并确保数出来的丑数是递增的,直到数到第n个丑数,得到答案。那么问题就是如何递增地数丑数?

观察上面的例子,假如我们用1, 2, 3, 4, 5, 6去形成后面的丑数,我们可以将1, 2, 3, 4, 5, 6分别乘以2, 3, 5,这样得到一共6*3=18个新丑数。也就是说1, 2, 3, 4, 5, 6中的每一个丑数都有一次机会与2相乘,一次机会与3相乘,一次机会与5相乘(一共有18次机会形成18个新丑数),来得到更大的一个丑数。

这样就可以用三个指针:

p2, 指向1, 2, 3, 4, 5, 6中,还没使用 乘2机会 的丑数的位置。该指针的前一位已经使用完了乘以2的机会。
p3, 指向1, 2, 3, 4, 5, 6中,还没使用 乘3机会 的丑数的位置。该指针的前一位已经使用完了乘以3的机会。
p5, 指向1, 2, 3, 4, 5, 6中,还没使用 乘5机会 的丑数的位置。该指针的前一位已经使用完了乘以5的机会。

下一次寻找丑数时,则对这三个位置分别尝试使用一次乘2机会,乘3机会,乘5机会,看看哪个最小,最小的那个就是下一个丑数。最后,那个得到下一个丑数的指针位置加一,因为它对应的那次乘法机会使用完了。 

这里需要注意下去重的问题,如果某次寻找丑数,找到了下一个丑数10,则p2和p5都需要加一,因为5乘2等于10, 2乘5也等于10,这样可以确保10只被数一次(所以不能使用 if ... else if;要用三个if判断)。

C++实现

class Solution 
{
public:
    inline int _min(int n1,int n2,int n3)
    {
        return n1>n2?(n2>n3?n3:n2):(n1>n3?n3:n1);
    }

public:
    int nthUglyNumber(int n) 
    {
        if(n<=1)
            return n;
        vector<int> dp(n+1);
        dp[1]=1;
        int p2,p3,p5;
        p2=p3=p5=1; //初始化三个指针,都指向第一个元素,因为第一个元素还没有使用分别乘2、乘3、乘5的机会
        for(int i=2;i<=n;++i)
        {
            int num2=dp[p2]*2;
            int num3=dp[p3]*3;
            int num5=dp[p5]*5;
            dp[i]=_min(num2,num3,num5);

            if(dp[i]==num2)
                p2++;   //p2所指向的元素已经使用完 乘2 这个机会了
            if(dp[i]==num3)
                p3++;   //p3所指向的元素已经使用完 乘3 这个机会了
            if(dp[i]==num5)
                p5++;   //p5所指向的元素已经使用完 乘5 这个机会了  
        }
        return dp[n];
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

心之所向便是光v

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值