【JZ49 丑数】

描述

把只包含质因子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
官方解释~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

千北@

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

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

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

打赏作者

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

抵扣说明:

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

余额充值