问题描述
我们把只包含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个月就会翻一番,由于内存的容量增加迅速,在软件开发的过程中,更多的时候对于牺牲一定空间优化时间性能的做法是允许的,以尽可能地缩短软件的响应时间。也就是算法中长提及的“空间换时间”。