经典面试题目——找到第n个丑数(参考《剑指offer(第二版)》面试题49)

时间:2022-10-21 08:28:57

一.题目大意

  给你一个数n,要求返回第n个丑数。其中,丑数的定义如下:

  丑数是指只包含因子2、3和5的数。(数字1也是丑数,不过是个特例)引用《剑指offer》上的话来说,对于一个数M,如果M能被2整除,就连续除以2;若果能被3整除,就连续除以3;如果能被5整除,就连续除以5。如果最终的结果是1的话,那么M就是丑数,否则M不是丑数。以下是判断一个数是否为丑数的代码:

bool IsUglyNumber(int num)
{
while(num % 2 == 0)
num /= 2;
while(num % 3 == 0)
num /= 3;
while(num % 5 == 0)
num /= 5;
return (num == 1)? true : false; }

二.思路分析

方法1.

  最直观的解法,就是从1开始进行遍历,对于每个数分别判断是否为丑数,直到找到n个丑数为止。

此方法,对于n比较大的情况,往往都会超时的。所以,这种方法并不最优选择。

方法2:

  实质是对方法1的优化,分析方法1,会发现浪费的操作主要在于非丑数的数。所以想方设法对非丑数的数进行排除,即只对丑数进行操作。《剑指offer》上给了这样一种思路:

(1)首先,根据丑数的定义,任何一个丑数都应该是另一个丑数乘以2或者3或者5的结果(1除外)。因此,根据这种特性,我们可以从最小的丑数开始逐渐生成新的丑数,并且要求新生成的丑数是排好序的(升序),如果用一个数组来存储它们的话,那么数组的第n个元素就是我们想要的结果。

(2)这种方法的重点在于如何确保新生成的丑数是排好序的:

假设这个有序的丑数数组为A[0]、A[1]、A[2]、... A[M],那么,A[M + 1]一定是A[0]或A[1]或 ... A[M]中的某个元素(假设这个元素为A[i])乘以2或者乘以3或者乘以5得到的,至于A[i]具体是哪个值,我们现在是不知道的。不过我们可以有以下的分析:

1.我们首先考虑把A[0]、A[1]、A[2]、...A[M]这M+1个数都乘以2,那么我们就能得到M + 1个新的丑数,但是这M+1个丑数中,一定有一部分与A[0]、A[1]、A[2]、... A[M]中的某些数是重复的(这M+1个丑数中小于A[M]的数一定就是重复的,因为数组A就是有序的丑数序列),这些重复的丑数就不用考虑了,也一定有一部分数是大于A[M]的,而我们要找的就是从这些大于A[M]的丑数中,选出最小的一个来,并把这个最小的丑数命名为N2。

2.我们再把A[0]、A[1]、A[2]、...A[M]这M+1个数都乘以3,那么我们也能得到M + 1个新的丑数,这M+1个丑数中,同理,一定有一部分与A[0]、A[1]、A[2]、... A[M]中的某些数是重复的(这M+1个丑数中小于A[M]的数一定就是重复的),这些重复的丑数就不用考虑了,也一定有一部分数是大于A[M]的,而我们要找的就是从这些大于A[M]的丑数中,选出最小的一个来,并把这个最小的丑数命名为N3。

3.我们最后再把A[0]、A[1]、A[2]、...A[M]这M+1个数都乘以5,那么我们同样能得到M + 1个新的丑数,这M+1个丑数中,同理,一定也有一部分与A[0]、A[1]、A[2]、... A[M]中的某些数是重复的(这M+1个丑数中小于A[M]的数一定就是重复的),这些重复的丑数就不用考虑了,也一定有一部分数是大于A[M]的,而我们要找的就是从这些大于A[M]的丑数中,选出最小的一个来,并把这个最小的丑数命名为N5。

4.经过前3步,我们得到了三个最小的数N2、N3、N5,那么,A[M+1]一定是这三个数中最小的那个数。

5.我们就可以重复第1步~第4步,直到数组A中的元素达到n个时,A[n - 1]就是我们想要的结果。

注:我们在以上的理论分析中每次都需要把A[0]、A[1]、A[2]、... A[M](M在每次迭代后都会加1)分别乘以2、3和5,这样太麻烦了啊,能不能进一步优化呢?我们下面来详细分析下:

1.首先,我们要了解上面的第1~3步,主要是为了找出N2、N3、N5这三个数来,而实际上,只需找到原数组A(即已知的丑数序列)中对应的元素,令该元素乘以2或者乘以3或者乘以5就可以得到N2、N3或者N5了。以下进一步分析。

2.对于有序的丑数数组A[0]、A[1]、A[2]、... A[M],如果把数组中每个元素都乘以2的话,就会得到M+1个新的丑数,对于这M+1个新的丑数,我们之前已经给分析了,有一部分是与数组中原来的元素是重复的,另一部分则是大于A[M]的。所那么在原数组A(即已有的丑数)中,这M+1个旧的丑数中,一定存在这么一个丑数T2,排在它前面的丑数乘以2都小于等于A[M],排在它后面的丑数,都大于A[M]。那么,根据之前的1中的叙述,就可以得到T2 * 2 = N2。也就是说确定了T2,就能确定N2了。我们在进一步分析,如果N2、N3、N5这三个数中最小的是N2的话,那么A[M+1] = N2(令M1 =M +1),那么此时的T2就不满足之前的要求了(即排在它前面的丑数乘以2都小于等于A[M1],排在它后面的丑数,都大于A[M1],此时M1=M+1)所以此时,需要更新T2,即T2变成了原数组A中T2之后的那个元素(相当于下标索引加1)。每次选出相应的N值时,必须要更新相应的T值。

3.同理的话,我们根据1,可知同样在原数组A中可以得到一个丑数T5,使得排在它前面的丑数乘以5都小于等于A[M],排在它后面的丑数,都大于A[M]。同理可得,T5 * 5 = N5。同样的,如果N5是N2、N3、N5中最小的丑数的话,那么A[M+1] = N5,此时T5就更新成原数组A中T5之后的那个元素了。

4.同理,T3也是同样的更新准则。

5.综合1、2、3我们只需找到并更新T2、T3、T5的值即可,并不用把所有的已知丑数都乘一遍。(当然了,如果N2、N3、N5中存在两个相同的值(假设N2 == N3),并且都是最小的话,那么对应的两个数(T2和T3)都需要更新)

6.由第5步可知,我们需要的是获得T2、T3、T5的初值,并对他们进行更新,就能获取排好序的数组之后的元素了(A[M+1]、A[M+2]、A[M+3]、... A[n-1])。那么如何获得T2、T3、T5的初始值?由于刚开始数组中的元素只有1个,即A[0] =1,此时的T2 = T3 = T5 =1的,所以按照之前的规则进行更新就可以了。

================================================================================================================================================

综合以上分析,可得代码如下:

 int GetUglyNumber_Solution(int index) {
if(index < 7)
return index; //由于数字1~6都是丑数,而且是排好序的,所以可以直接返回
vector<int> rs(index);
rs[0] = 1;//初始化数组,1是个特例
int t2 =0, t3 = 0, t5 = 0;//rs[t2]对应T2,rs[t3]对应T3,rs[t5]对应T5
for(int i = 1; i < index; i++)
{
rs[i] = min(rs[t2] * 2,min(rs[t3] * 3,rs[t5] * 5));//选出T2、T3、T5中的最小值
if(rs[i] == rs[t2] * 2)
t2++;//更新T2,T2变成数组中T2之后的那个元素了
if(rs[i] == rs[t3] * 3)
t3++;
if(rs[i] == rs[t5] * 5)
t5++;
}
return rs[index - 1]; }

该方法的时间复杂度为O(n),空间复杂度O(n)。相比思路一时间效率得到了极大的提高。

观察改代码可知,实际上通过t2、t3、t5这三个索引下标就实现了对原数组中所有的元素乘以2、乘以3、乘以5的操作,只不过避免重复操作,才引入了T2、T3、T5这三个丑数。(一旦选定了N中的最小值,假设是N2,一定要去更新对应的T值,即N2对应的是T2)

可以具体分析下这段代码:

 (1)首先,在第一轮循环中,数组rs中只有元素1,即rs[0] = 1,且t2 = t3 = t5 = 0,T2 = T3 =T5 = rs[0] = 1,由于N2是三者中最小值,故数组中新添加的元素rs[1] = N2 = 2。此时T2需要更新,即t2 = t2 +1(T2变成了T2之后的那个元素)

 (2)在第二轮循环中,数组rs有两个元素了,且t2 =1,t3 = t5 = 0,T2 =rs[1] = 2,T3 = T5 = rs[0] = 1,由于N3是三者中的最小值,故数组中新添加的元素rs[2] = N3 = 3。此时T3需要更新,即t3 = t3 +1(T3变成了T3之后的那个元素)

 (3)在第三轮循环中,数组rs有三个元素了,且t2 = t3 = 1,t5 = 0,T2 =T3 =rs[1] =2,T5 = rs[0] = 1,由于N2= T2 *2 =4,N3 = T3 * 3 = 6,T5= N5 * 5 = 5,N2是三者之中最小值,故数组中新添加的元素rs[3] = N2 =4,同时,T2需要更新,即t2 = t2 + 1

 (4)在四轮循环汇总,数组rs有四个元素了,且t2 = 2,t3 = 1,t5 = 0,T2 = rs[2] = 3,T3 = rs[1] = 2,T5 = rs[0] = 1,由于N2 = 6,N3 = 6,N5 =5,即N5是三者之中的最小值,故数组中新添加的元素rs[4] = N5  = 5,同时,T5需要更新,即t5 = t5 +1

.......

 按照此规律依次进行下去。

  

经典面试题目——找到第n个丑数(参考《剑指offer(第二版)》面试题49)的更多相关文章

  1. &lbrack;互联网面试笔试汇总C&sol;C&plus;&plus;-9&rsqb; 实现赋值运算符函数-剑指offer

    题目:如下为类型CMyString的声明,请为该类型添加赋值运算符函数. class CMyString { public: CMyString(char* pData = NULL); CMyStr ...

  2. 【剑指Offer】剑指offer题目汇总

      本文为<剑指Offer>刷题笔记的总结篇,花了两个多月的时间,将牛客网上<剑指Offer>的66道题刷了一遍,以博客的形式整理了一遍,这66道题属于相对基础的算法题目,对于 ...

  3. 面试题目——《剑指Offer》

    1.把一个字符串转换成整数——<剑指Offer>P29 2.求链表中的倒数第k个结点——<剑指Offer>P30 3.实现Singleton模式——<剑指Offer&gt ...

  4. 33条C&num;、&period;Net经典面试题目及答案

    33条C#..Net经典面试题目及答案[zt] 本文集中了多条常见的C#..Net经典面试题目例如".NET中类和结构的区别"."ASP.NET页面之间传递值的几种方式? ...

  5. 33条C&num;、&period;Net经典面试题目及答案&lbrack;zt&rsqb;

    33条C#..Net经典面试题目及答案[zt] 本文集中了多条常见的C#..Net经典面试题目例如“.NET中类和结构的区别”.“ASP.NET页面之间传递值的几种方式?”,并简明扼要的给出了答案,希 ...

  6. 经典面试题目——250M内存处理10G大小的log文件

    前言 周末逛知乎的时候,看到的一个经典面试题目:http://www.zhihu.com/question/26435483.非常经典的一道分而治之的题目. 题目描写叙述例如以下: 有次面试遇到一个问 ...

  7. 【剑指Offer面试编程题】题目1214:丑数--九度OJ

    把只包含因子2.3和5的数称作丑数(Ugly Number).例如6.8都是丑数,但14不是,因为它包含因子7. 习惯上我们把1当做是第一个丑数.求按从小到大的顺序的第N个丑数. 输入: 输入包括一个 ...

  8. 【强烈推荐】《剑指Offer:名企面试官精讲典型编程题》一书中IT名企经典面试题

    各位程序猿:         <剑指Offer>一书源自该书作者何海涛坚持更新与编写的博客(http://zhedahht.blog.163.com/),该博客收集整理了大量如微软.Goo ...

  9. 面试经典算法题集锦——《剑指 offer》小结

    从今年 3 月份开始准备找实习,到现在校招结束,申请的工作均为机器学习/数据挖掘算法相关职位,也拿到了几个 sp offer.经历这半年的洗礼,自己的综合能力和素质都得到了一个质的提升. 实话说对于未 ...

随机推荐

  1. C&num;集合--Dictionary

    字典(dictionary)是一个集合,其中每个元素都是一个键/值对.字典(Dictionaries)是常用于查找和排序的列表. .NET Framework通过IDictionary接口和IDict ...

  2. 【二叉树、堆】15轻院校赛-J-堆

    原题:http://acm.zzuli.edu.cn/problem.php?cid=1099&pid=9 [描述] [输入] [输出] Sample Input 3 1 10 3 10 5 ...

  3. Linux查看磁盘块大小

    首先,使用df命令查看所在磁盘 df -hT 显示: Filesystem Type Size Used Avail Use% Mounted on /dev/vda1 ext4 15G .1G 12 ...

  4. HOJ1087

    Self Numbers My Tags   (Edit)   Source : ACM ICPC Mid-Central USA 1998   Time limit : 5 sec   Memory ...

  5. 20 你应该知道的PHP库

    下面是一些非常有用的PHP类库,相信一定可以为你的WEB开发提供更好和更为快速的方法. 图表库 下面的类库可以让你很简的创建复杂的图表和图片.当然,它们需要GD库的支持. pChart – 一个可以创 ...

  6. &lbrack;Kubernetes&rsqb;关于 Kubernetes &comma;你想要的&comma;都在这儿了

    陆陆续续,关于 Kubernetes 写了有 20+ 篇文章了. 今天这篇文章来一个整合,从实践到理论,可以按需查看(我是按照博客发表时间来排序的,如果后续有想要更新的内容,也会及时更新到这篇文章中) ...

  7. 函数式编程-compose与pipe

    函数式编程中有一种模式是通过组合多个函数的功能来实现一个组合函数.一般支持函数式编程的工具库都实现了这种模式,这种模式一般被称作compose与pipe.以函数式著称的Ramda工具库为例. cons ...

  8. Loadrunner与idea编写加密的java Vusers脚本总结

    Loadrunner与idea编写加密的java Vusers脚本总结 准备工作:   jdk版本的选择:       Loadrunner11 使用版本jdk1.6 32位(如果使用1.7的Load ...

  9. Linux 小知识翻译 - 「X Window系统」

    X Window System是给Unix系的OS提供的一套窗口管理软件或者说是组件.X Window System已经成为了在Linux上使用GUI环境的不可或缺的东西了. X Window Sys ...

  10. Spring 注解驱动(二)Servlet 3&period;0 注解驱动在 Spring MVC 中的应用

    Spring 注解驱动(二)Servlet 3.0 注解驱动在 Spring MVC 中的应用 Spring 系列目录(https://www.cnblogs.com/binarylei/p/1019 ...