剑指OFFER第49题——丑数

问题描述

        我们把只包含2、3、5的数称作丑数(Ugly Number)。实现一个函数求从小大到的顺序的第n个丑数。例如,6、8都是丑数,但是14就不是,因为它包含了因子7 。习惯上我们把1当作第一个丑数。


方法一:
        根据丑数的定义,丑数只能被2、3、5整除,因此,如果一个数能够被2整除,就连续除以2,能被3整除就连续整除3,能被5整除就连续除以5 。如果最后得到的结果为1,那么这个数就是整数,否则不是。因此通过一个函数用于判断该数是否为丑数,然后遍历找到对应位置的数字就好了。

class Solution1
{
   /// <summary>
   /// 用于遍历数字是否为丑数,从而找出指定位置的丑数
   /// </summary>
   /// <param name="index">指定要找的丑数的位置</param>
   /// <returns>返回固定位置的丑数</returns>
   public int GetUglyNumber(int index)
   {
       if (index <= 0)
           return 0;

       int number = 0;                         //递增的数字,用于判断是否为丑数
       int uglyNumber = 0;                     //丑数的数量

       //如果丑数的数量到达所要求的第index位了,则输出它
       while (uglyNumber < index)
       {
           number++;
           if (IsUgly(number))
               uglyNumber++;
       }

       return number;
   }

   /// <summary>
   /// 用于判断一个数是否为丑数,即能够整除2、3、5之后,最后得到的因子为1,则为丑数
   /// </summary>
   /// <param name="number">检查的数</param>
   /// <returns>返回是否为丑数</returns>
   public bool IsUgly(int number)
   {
       //能被2整除,则连续除以2
       while (number % 2 == 0)
           number /= 2;
       //能被3整除,则连续除以3
       while (number % 3 == 0)
           number /= 3;
       //能被5整除,则连续除以5
       while (number % 5 == 0)
           number /= 5;
       //除完之后的结果如果为1,说明其不包含其他因子,返回真,否则返回假
       return number == 1 ? true : false;
   }
}

        优缺点:方法十分直观,输入1500之后就能够得到第1500个丑数,代码也十分简洁。但是最大的问题就是计算量太大,时间效率不够高,对于每一个数不管是否为丑数我们都会去进行求余和除法操作,因此需要寻找新的更优算法才行。


方法二:
        可以通过创建一个数组来保存已经找到的丑数,通过空间换时间。
        根据丑数的定义,丑数应该是另一个丑数乘以2、3、5的结果(1除外)。因此,可以通过一个数组,里面的数字是排好序的丑数,每个丑数都是前面的丑数乘以2、3、5得到的。这样输出数组中相应下标的数字即可得到某一位置的丑数了。
        有这种思路之后,其关键就在于——如何去确保数组里面的丑数都是排好序的。假设数组中已经有若干个丑数了,并且把已有的最大的丑数记为M,接下来分析如何生成下一个丑数。该丑数肯定是前面的某个丑数乘以2、3或5的结果,所以首先考虑把已有的每个丑数乘以2,在乘以2的时候,能得到若干个小于或者等于M的结果,因为此刻希望丑数是从小到大生成的,其他更大的结果暂且不提。把得到的第一个乘以2后大于M的结果记为M_2。同样,把已有的每个丑数乘以3和5,能得到第一个大于M的结果M_3和M_5。那么下一个丑数应该是M_2,M_3,M_5这三个数的最小者。
        在前面分析的时候提到把已有的每个丑数分别乘以2、3、5,实际上是没必要的,因为已有的丑数是按照顺序存放在数组中的,对于乘以2而言,肯定会存在一个丑数T_2,排在它之前的每个丑数乘以2得到的而结果都会小于已有最大的丑数,在它之后的每个丑数乘以2得到的结果都会大于已有最大的丑数。因此只需要记住这个丑数的位置,在每次生成新的丑数的时候去更新这个位置T_2即可。对于3和5同理可得T_3和T_5。
综上,得出以下解决代码:

class Solution2
{
    /// <summary>
    /// 找到指定位置的丑数
    /// </summary>
    /// <param name="index">指定的位置</param>
    /// <returns>相对应的丑数</returns>
    public int GetUglyNumber(int index)
    {
        //检验输入是否合法
        if (index <= 0)
            return 0;

        int[] uglyNumbers = new int[index];
        uglyNumbers[0] = 1;

        int T_2 = 0, T_3 = 0, T_5 = 0;          //分别用于储存前一个乘以2、3、5得到的丑数大于已有最大丑数的丑数下标
        int nextUglyIndex = 1;
        //如果还没计算得到第index个丑数,则继续找下一个丑数添加进数组
        while (nextUglyIndex < index)
        {
            //找到当前丑数*2、*3、*5中最小但是又比已获得的最大丑数大的丑数,添加进数组
            int min = GetMinNumber(uglyNumbers[T_2] * 2, uglyNumbers[T_3] * 3, uglyNumbers[T_5] * 5);
            uglyNumbers[nextUglyIndex] = min;

            //找到数组中*2、*3、*5刚好大于已有的最大丑数
            while (uglyNumbers[T_2] * 2 <= uglyNumbers[nextUglyIndex])
                T_2++;
            while (uglyNumbers[T_3] * 3 <= uglyNumbers[nextUglyIndex])
                T_3++;
            while (uglyNumbers[T_5] * 5 <= uglyNumbers[nextUglyIndex])
                T_5++;
            //已找到的丑数+1
            nextUglyIndex++;
        }
        //用uglyNumber储存第index位置的丑数,然后返回它
        int uglyNumber = uglyNumbers[nextUglyIndex - 1];

        return uglyNumber;
    }

    /// <summary>
    /// 取出三个数字间的最小值
    /// </summary>
    private int GetMinNumber(int v1, int v2, int v3)
    {
        if (v1 < v2)
            return v1 < v3 ? v1 : v3;
        else
            return v2 < v3 ? v2 : v3;
    }
}

        与第一种思路相比,第二种思路不需要在非丑数的整数上进行任何计算,因此时间效率会有显著的提升。但是,第二种算法由于需要保存已经生成的丑数,因此需要一个数组,从而增加了空间消耗。如果是求第1500个丑数,则将创建一个能够容纳1500个整数的数组,这个数组占据6KB的内容空间。而第一种思路则没有这样的内存开销。总体上来说,第二种思路相当于用较小的空间消耗换取了时间效率的提升,还是比较好的了。
        在这里有点提及就是: 硬件的发展一直遵循着摩尔定律,内存的容量基本上每个18个月就会翻一番,由于内存的容量增加迅速,在软件开发的过程中,更多的时候对于牺牲一定空间优化时间性能的做法是允许的,以尽可能地缩短软件的响应时间。也就是算法中长提及的“空间换时间”。

猜你喜欢

转载自blog.csdn.net/qq_33575542/article/details/80949980