面试题34 丑数

问题描述:

     我们把只包含因子235的数称作丑数(Ugly Number)。例如68都是丑数,但14不是,因为它包含因子7。习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第1500个丑数。(昨天突然发现个不错的博客:http://blog.csdn.net/v_JULY_v,突然知道丑数这个题,于是搜之)

     当然,最简单的肯定是遍历啊,想当年初学的时候,什么水仙花数,完数,质数,都遍历搞定。遍历存在的问题就是效率太低,如同暴力破密码似的,以前用bt4破一个wep的有时候都要10多分钟,破个WAP加密的半个小时,这不蛋疼吗,破了就为蹭个网。像这个吧,到第1500个丑数的时候,用时就要42s多(win7+vc6),效率上肯定是有折扣的了,下面是代码:

[cpp]  view plain copy
  1. #include <iostream>  
  2. #include <climits>  
  3. using namespace std;  
  4. //遍历法找丑数   
  5. int IsUgly(int num)//判断是否是  
  6. {  
  7.     while (num %2 == 0)  
  8.     {  
  9.         num /= 2;  
  10.     }  
  11.     while (num %3 == 0)  
  12.     {  
  13.         num /= 3;  
  14.     }  
  15.     while (num %5 == 0)  
  16.     {  
  17.         num /= 5;  
  18.     }  
  19.     if (num == 1)  
  20.         return 1;  
  21.     else  
  22.         return 0;//not an ugly number  
  23. }  
  24. void  GetUglyNumber(int index)  
  25. {//找到第index个丑数  
  26.     int i , time =0 ;  
  27.     if (index < 1)  
  28.     {  
  29.         cout << "error input " << endl;  
  30.         exit(EXIT_FAILURE);  
  31.     }  
  32.     for (i=1 ; i< INT_MAX && time < index ; i++)  
  33.     {  
  34.         if ( IsUgly(i) )  
  35.         {  
  36.             time ++ ;  
  37.         //  cout << i << " " ;  
  38.         }  
  39.     }  
  40.     cout << i-1 << endl;  
  41. }  
  42. int main()  
  43. {  
  44.     int Number;  
  45.     cout << "Input a number : " ;  
  46.     cin >> Number ;  
  47.     GetUglyNumber(Number);  
  48.     return 0;  
  49. }  
        遍历法很大的问题在于对每个数都进行判断,进行取余和除的运算了,如果换种思路的话,只对丑数进行计算呢?根据 http://www.cnblogs.com/mingzi/archive/2009/08/04/1538491.html 的思路,虽然从代码上来看 http://www.cppblog.com/zenliang/articles/131094.html 的更简洁易懂,不过第一个链接的变量命名会好很多,而且思路交代更清晰。

        根据丑数的定义,丑数应该是另一个丑数乘以23或者5的结果(1除外)。因此我们可以创建一个数组,里面的数字是排好序的丑数。里面的每一个丑数是前面的丑数乘以23或者5得到的。那关键就是确保数组里的丑数是有序的了。我们假设数组中已经有若干个丑数,排好序后存在数组中。我们把现有的最大丑数记做M。现在我们来生成下一个丑数,该丑数肯定是前面某一个丑数乘以23或者5的结果。我们首先考虑把已有的每个丑数乘以2。在乘以2的时候,能得到若干个结果小于或等于M的。由于我们是按照顺序生成的,小于或者等于M肯定已经在数组中了,我们不需再次考虑;我们还会得到若干个大于M的结果,但我们只需要第一个大于M的结果,因为我们希望丑数是按从小到大顺序生成的,其他更大的结果我们以后再说。我们把得到的第一个乘以2后大于M的结果,记为M2。同样我们把已有的每一个丑数乘以35,能得到第一个大于M的结果M3M5。那么下一个丑数应该是M2M3M5三个数的最小者。(来自http://www.cnblogs.com/mingzi/archive/2009/08/04/1538491.html),则可以得到以下代码:

[cpp]  view plain copy
  1. #include <iostream>     
  2. using namespace std;     
  3.     
  4. int Min(int a, int b, int c)     
  5. {     
  6.     int temp = (a < b ? a : b);     
  7.     return (temp < c ? temp : c);     
  8. }     
  9. int FindUgly(int n) //  
  10. {     
  11.     int* ugly = new int[n];     
  12.     ugly[0] = 1;     
  13.     int index2 = 0;     
  14.     int index3 = 0;     
  15.     int index5 = 0;     
  16.     int index = 1;     
  17.     while (index < n)     
  18.     {     
  19.         int val = Min(ugly[index2]*2, ugly[index3]*3, ugly[index5]*5); //竞争产生下一个丑数     
  20.         if (val == ugly[index2]*2) //将产生这个丑数的index*向后挪一位;    
  21.             ++index2;     
  22.         if (val == ugly[index3]*3)   //这里不能用elseif,因为可能有两个最小值,这时都要挪动;  
  23.             ++index3;     
  24.         if (val == ugly[index5]*5)     
  25.             ++index5;     
  26.         ugly[index++] = val;     
  27.     }     
  28. /* 
  29.     for (int i = 0; i < n; ++i)    
  30.         cout << ugly[i] << endl;    
  31. //*/  
  32.     int result = ugly[n-1];     
  33.     delete[] ugly;     
  34.     return result;     
  35. }     
  36. int main()     
  37. {     
  38.     int num;  
  39.     cout << "input the number : " ;  
  40.     cin >> num;  
  41.     cout << FindUgly(num) << endl;  
  42.     return 0;     
  43. }  
     代码来自: http://www.cppblog.com/zenliang/articles/131094.html 。看到他的new,才想起,以前写排序的时候,由于数组大小可变,直接用了vector,让它直接去vector的size()就知道大小了,而没有想到还有更初级的new,对于不定大小,new就好了啊,虽说new出来的是是在堆上,直接定义的是在栈上,不过用起来也是毫无影响的,果然自己还是太菜了点。
      另外还可以采用的方法很多,参考 http://www.iteye.com/topic/832545 。本帖子列出了5种方法:

  1. * 1.method1是最基础的遍历,唯一的优点估计就是简单易懂。<br/> 
  2.  * 2.method2,method3的思想是先人工估算范围值,将一定范围内的值乘2,3,5排重增加,不同的地方在于method2重新遍历, 
  3.  * method3排序求下标<br/> 
  4.  * 3.method4的思想是将已经获取的值分别遍历,乘以2,3,5,当比最大值大就停止,比较这3个数的最小值,增加到定义的有序数组中。<br/> 
  5.  * 4.method5的思想是将数进行评估,评估出该数包含丑数的数量,当超过丑数要求数量时,进行2分法进行缩小范围,直至求出解。 
      代码直接参考,实际上,搜到的C++的代码就是method1和method4,其实吧,method2和method3的精髓在于i < Integer.MAX_VALUE / 5 ,也是利用了所有丑数肯定是由丑数产生这一思想,虽然不同之处在于遍历和求下标,不过总体是产生足够大的丑数集合,再直接取需要的位置。C++实现如下:    

[cpp]  view plain copy
  1. #include <set>  
  2. #include <iostream>  
  3. #include <climits>  
  4. using namespace std;  
  5. const int MAX = INT_MAX/5;  
  6.   
  7. void GetUgly(int Index)  
  8. {  
  9.     int i;  
  10.     set<int,less<int> > s;  
  11.     set<int, less<int> >::iterator It;  
  12.     s.insert(1);  
  13.     for (i=1 ; i<MAX ; i++)  
  14.     {  
  15.         if (s.find(i) != s.end() )  
  16.         {  
  17.             s.insert(2*i) ;  
  18.             s.insert(3*i) ;  
  19.             s.insert(5*i) ;  
  20.         }  
  21.     }  
  22.     for (It = s.begin() ,i=1 ; It != s.end() && i < Index; It++)  
  23.         i++;  
  24.     cout << *It << endl;  
  25. }  
  26. int main(int argc,char *argv[])  
  27. {  
  28.     int Number;  
  29.     cout << "Input a number : " ;  
  30.     cin >> Number ;  
  31.     GetUgly(Number);  
  32.     return 0;  
  33. }  
    说到这个,本打算用vector的,还用到了algorithm头文件的find和sort。不过问题在于vector怎么删除重复元素呢?哪怕加入是否在vector中的判断,仍然难以阻止,效率不高。不过一不小心找到了STL的 set ,高级货啊, set自动删除重复元素 这一特性,还是很给力的。和Java的set一样,不过这个算法的问题在于,直接将所有的丑数都找出来了,再取下标,在vc6和gcc测试下,速度着实很慢,莫非是C++STL的set不如Java的set高效么?这个方法让我想到对于1000个数,找出其中最小的5个,但是将这1000个数都进行排序了再直接取前5个,虽然可行,但未免开销太大,不经济。运行的时候,等的时间太长,以至于直接关掉,将MAX换为2w,随便测试了下对于100等数是否正确来判断程序是否大致准确。

    下面来改写Java的method5为C++版本,代码如下:

[cpp]  view plain copy
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. int nums5(int val)  
  5. {  
  6.     int n=0 ;   
  7.     while (val >= 5)  
  8.     {  
  9.         n++ ;  
  10.         val /= 5;  
  11.     }  
  12.     return n;  
  13. }  
  14. int nums35(int val)  
  15. {  
  16.     int n=0 ;  
  17.     while (val >= 3)  
  18.     {  
  19.         n += 1+nums5(val);  
  20.         val /= 3;  
  21.     }  
  22.     return n;  
  23. }  
  24. //基于因数分解求出val以内有多少个丑数(不包含1)   
  25. int nums235(int val)  
  26. {  
  27.     int n=0 ;  
  28.     while (val >= 2)  
  29.     {  
  30.         n += 1+nums35(val);  
  31.         val /= 2 ;  
  32.     }  
  33.     return n;  
  34. }  
  35. //用二分法查找第n个丑数    
  36. //对于X,如果X以内的丑数个数是n,而X-1以内的丑数个数是n-1,那么X就是第n个丑数    
  37. int numOfIndex(int n)    
  38. {    
  39.     if(n == 1)    
  40.         return 1;    
  41.     n--;    
  42.     int val1 = 1;    
  43.     int nums1 = 0;    
  44.     int val2 = 2;    
  45.     int nums2 = nums235(val2); //nums2为val2的因数个数   
  46.     while( nums2 < n )    
  47.     {    
  48.         val1 = val2;    
  49.         nums1 = nums2;    
  50.         val2 = val1*2;    
  51.         nums2 = nums235(val2);    
  52.     }    
  53.     if( nums1 == n )  
  54.         return val1;    
  55.     if( nums2 == n )  
  56.         return val2;    
  57.   
  58.     while(true)    
  59.     {    
  60.         long mid = (val1 + val2)/2;    
  61.         int nums = nums235(mid);    
  62.         if(val2 == mid+1 && nums == n-1 && nums2==n)    
  63.             return val2;    
  64.         if(mid == val1+1 && nums1 == n-1 && nums==n)    
  65.             return mid;    
  66.         if(nums >= n)    
  67.         {    
  68.             val2 = mid;    
  69.             nums2 = nums;    
  70.         }    
  71.         else    
  72.         {    
  73.             val1 = mid;    
  74.             nums1 = nums;    
  75.         }    
  76.     }    
  77. }  
  78. int check(int val)    
  79. {    
  80.     long v = val;    
  81.     while( v%2==0 )   
  82.         v/=2;    
  83.     while( v%3==0 )   
  84.         v/=3;    
  85.     while( v%5==0 )   
  86.         v/=5;    
  87.     if( v != 1 )  
  88.         cout << " v is not an ugly number! " << endl;  
  89.     return val;    
  90. }    
  91.   
  92. void calc(int n)    
  93. {    
  94.     long val = numOfIndex(n);    
  95.     cout << n <<  " : " << val << endl;;    
  96.     check(val);    
  97. }    
  98.   
  99. int main(int argc ,char *argv[])  
  100. {  
  101.     int Number;  
  102.     cout << "Please input a number : " ;  
  103.     cin >> Number ;  
  104.     calc(Number);   
  105.     return 0;  
  106. }  
         想不到这算法很是高级货啊,直接因数分解,其实也是充分利用丑数是由丑数产生这一原理,用nums235统计出val内丑数个数。虽然也是都大量计算,不过比第一种的好很多,加上引入二分查找,效率还是不错的。经过测试,与method4在1500的时候都能在5ms内完成,各有所长。不过有个不足的地方, http://www.iteye.com/topic/832545 虽然说这方法是最优解(如果在calc中去掉check调用,1500-1545都是1ms或2ms完成,震惊啊),不过在输入1546开始,会很慢,更不用说在1692这样会溢出的点,会很慢(没等,不知道具体时间)不过在1545以内,的确是最优,作者 taolei0628 果然牛。

          总结起来,就是最简陋的遍历,从小到大的只算丑数,统计全部丑数,计算丑数个数,方法不同,算起来,搞程序还是很有意思的嘛,可惜没早点发现,就这样了吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值