[未做出]剑指 Offer 49. 丑数(中等)

我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
示例:
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
说明:  
1 是丑数。
n 不超过1690。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/chou-shu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思考过程

  1. 因为是做过的题目,所以一开始一直在回想题目的套路
  2. 能思考到的大致思路是从1,2,3,5开始,下一个丑数肯定是这几个数继续乘以2、3、5,但是如何产生下一个最小的丑数这一点没有思考出来,并且对于空间复杂度一直举棋不定
  3. 最终因为时间问题放弃直接看了官方答案,其实思考中的答案就是动态规划,并且其实最小堆这个想法才是更简朴的解法

题解

func nthUglyNumber(n int) int {

    uglyNum := make([]int, n+1, n+1)
    uglyNum[1]=1
    p2, p3, p5 := 1,1,1
    for i:=2;i<=n;i++{
        uglyNum[i] = min(uglyNum[p2]*2, uglyNum[p3]*3, uglyNum[p5]*5)
        // 如果能够得到相同的min值,一定要同时更新,否则会重复,比如2*3和3*2
        if uglyNum[p2]*2 == uglyNum[i] {
            p2++
        }
        if uglyNum[p3]*3 == uglyNum[i] {
            p3++
        }
        if uglyNum[p5]*5 == uglyNum[i] {
            p5++
        }
    
    }
    return uglyNum[n]
}

func min(param1, param2, param3 int) int{
    if param1 < param2 {
        if param1 < param3 {
            return param1
        }
    } else {
        if param2 < param3 {
            return param2
        }
    }
    return param3
}

反思

  1. 就如思考过程展示的那样,感觉自己又回到了一开始刷题的状态,太过于追求技巧,一上来就想写出时间复杂度或者空间复杂度最优的解法,容易脱离实际,反而不如踏踏实实地先把“笨”解法写出来。
  2. 实际工作中往往也没有那么理想的情景,做到比做好更重要。
  3. 看到当年的题解,哇一声哭出来,当年还是能想到用堆来解题的

附录

C++题解

  一开始我想着循环*2 *3 *5,但是突然发现不对啊,比如4,4应该优先*2而不是*3,
也就是说得到的下一个值应该先*2,然后再考虑上一个值*3之类的
当然马上发现也不是绝对,比如1*2后应该继续1*3而不是2*2
似乎可以用一个大顶堆hhh

  然后就是书里的办法吧,类似于动态规划,首先从乘2 乘3 乘5的位置出发,比较三者的最小值,只有最小那个可以前进一步
暴力循环法的话就是理解取余再除以的思想就行,然后超时了,但是ide中通过了数据比

class Solution {
public:
    /*
    //暴力循环法,超时
    int GetUglyNumber_Solution(int index){
        if(index<1)
            return 0;
        int count,res;
        count=1;
        res=1;
        while(count<index){
            ++res;
            int temp=res;
            while(temp%2==0)
                temp/=2;
            while(temp%3==0)
                temp/=3;
            while(temp%5==0)
                temp/=5;
            if(temp==1)
                ++count;
        }
        return res;
    }
    */
    
    
    //动态规划
    int GetUglyNumber_Solution(int index) {
        if(index<1)
            return 0;
        vector<int> uglynum(index,1);
        int multiby2,multiby3,multiby5,count;
        multiby2=0;
        multiby3=0;
        multiby5=0;
        count=1;
        while(count<index){
            int cur_min=min(uglynum[multiby2]*2,uglynum[multiby3]*3,uglynum[multiby5]*5);
            uglynum[count]=cur_min;
            while(uglynum[multiby2]*2<=cur_min)
                ++multiby2;
            while(uglynum[multiby3]*3<=cur_min)
                ++multiby3;
            while(uglynum[multiby5]*5<=cur_min)
                ++multiby5;
            ++count;
        }
        return uglynum[index-1];
    }
    
    int min(int multiby2,int multiby3,int multiby5){
        int temp=std::min(multiby2,multiby3);
        return std::min(temp,multiby5);
    } 
};

书本题解

  逐个判断每个整数是不是丑数的解法,直观但不够高效
  所谓一个数m是另一个数n的因子,是指n能被m整除 也就是n%m = 0。根据丑数的定义,丑数只能被2、3和5整除。也就是说,如果一个数能被2整除,就连续除以2;如果能被3整除,就连续除以3;如果能被5 整除,就除以连续5。如果最后得到的是1,那么这个数就是丑数;否则不是。

bool IsUgly(int number)
{
    while(number % 2 == 0)
        number /= 2;
    while(number % 3 == 0)
        number /= 3;
    while(number % 5 == 0)
        number /= 5;

    return (number == 1) ? true : false;
}

int GetUglyNumber_Solution1(int index)
{
    if(index <= 0)
        return 0;

    int number = 0;
    int uglyFound = 0;
    while(uglyFound < index)
    {
        ++number;

        if(IsUgly(number))
            ++uglyFound;
    }

    return number;
}

创建数组保存已经找到的丑数,用空间换时间的解法
  前面的算法之所以效率低,很大程度上是因为不管一个数是不是丑数, 我们都要对它进行计算。接下来我们试着找到一种只计算丑数的方法,而不在非丑数的整数上花费时间。根据丑数的定义,丑数应该是另一个丑数乘以2、3或者5的结果(1除外)。因此,我们可以创建一个数组,里面的数字是排好序的丑数,每个丑数都是前面的丑数乘以2、3或者5得到的。
  这种思路的关键在于怎样确保数组里面的丑数是排好序的。假设数组中已经有若干个排好序的丑数,并且把已有最大的丑数记作M,接下来分析如何生成下一个丑数。该丑数肯定是前面某一个丑数乘以2、3或者5的 结果,所以我们首先考虑把已有的每个丑数乘以2。在乘以2的时候,能得到若干个小于或等于M的结果。由于是按照顺序生成的,小于或者等于M 肯定己经在数组中了,我们不需再次考虑;还会得到若干个大于M的结果, 但我们只需要第一个大于M的结果,因为我们希望丑数是按从小到大的顺序生成的,其他更大的结果以后再说。我们把得到的第一个乘以2后大 M的结果记为M2,同样,我们把已有的每个丑数乘以3和5,能得到第一 个大于M的结果M3和M5。那么下一个丑数应该是M2、M3和M5这3个数的最小者。
  在前面分析的时候提到把已有的每个丑数分别乘以2、3和5。事实上这不是必需的,因为已有的丑数是按顺序存放在数组中的。对于乘以2而言,肯定存在某一个丑数T2,排在它之前的每个丑数乘以2得到的结果都会小于已有最大的丑数,在它之后的每个丑数乘以2得到的结果都会太大。 我们只需记下这个丑数的位置,同时每次生成新的丑数的时候去更新这个T2即可。对于乘以3和5而言,也存在同样的T3和T5。

int GetUglyNumber_Solution2(int index)
{
    if(index <= 0)
        return 0;

    int *pUglyNumbers = new int[index];
    pUglyNumbers[0] = 1;
    int nextUglyIndex = 1;

    int *pMultiply2 = pUglyNumbers;
    int *pMultiply3 = pUglyNumbers;
    int *pMultiply5 = pUglyNumbers;

    while(nextUglyIndex < index)
    {
        int min = Min(*pMultiply2 * 2, *pMultiply3 * 3, *pMultiply5 * 5);
        pUglyNumbers[nextUglyIndex] = min;

        while(*pMultiply2 * 2 <= pUglyNumbers[nextUglyIndex])
            ++pMultiply2;
        while(*pMultiply3 * 3 <= pUglyNumbers[nextUglyIndex])
            ++pMultiply3;
        while(*pMultiply5 * 5 <= pUglyNumbers[nextUglyIndex])
            ++pMultiply5;

        ++nextUglyIndex;
    }

    int ugly = pUglyNumbers[nextUglyIndex - 1];
    delete[] pUglyNumbers;
    return ugly;
}

int Min(int number1, int number2, int number3)
{
    int min = (number1 < number2) ? number1 : number2;
    min = (min < number3) ? min : number3;

    return min;
}

和第一种思路相比,第二种思路不需要在非丑数的整数上进行任何计算,因此时间效率有明显提升。但也需要指出,第二种算法由于需要保存已经生成的丑数,则因此需要一个数组,从而增加了空间消耗。如果是求第1500个丑数,则将创建一个能容纳1500个丑数的数组,这个数组占据 6KB的内容空间。而第一种思路没有这样的内存开销。总的来说,第二种思路相当于用较小的空间消耗换取了时间效率的提升。

posted @ 2021-11-14 23:36  lixin_longway  阅读(17)  评论(0)    收藏  举报