大厂高频面试系列04- 寻找丑数

12 篇文章 0 订阅
12 篇文章 0 订阅

大厂高频面试系列04- 寻找丑数

我们把只包含因子2,3,5的数称为丑数。例如6,8都是丑数,而14不是丑数,因为它包含因子7。通常我们也把1当作丑数。编程找出1500以内的全部丑数。注意:使用的算法效率应尽量高。

题目难度:★★

题目分析:

本题最直观的解法是采用穷举筛选法:遍历1~1500这1500个整数,判断每一个数是否是丑数,如果该数是丑数则输出,否则跳过该数继续向下遍历。

这个解法的关键是如何判断丑数。根据题目已知,丑数只包含因子2,3,5,而不包含其他任何因子,同时1也是丑数,因此可以把一个非1的丑数形式化地表示为:

UglyNumber= 2×2×2×…×2 × 3×3×3×…×3 × 5×5×5×…×5

不难看出,如果将该丑数循环除以2,直到除不尽为止;再循环除以3,直到除不尽为止;再循环除以5,那么最终得到的结果一定为1。如果按照此法循环相除而最终得到的结果不为1,则说明该数中除了包含2,3,5这三个因子外,还包含其他因子,所以该数不是丑数。上述判断丑数的方法可用下面的这段代码描述。

int isUglyNumber(int number) {
    while(number % 2 == 0) {
        number /= 2;
	}
    while(number % 3 == 0) {
        number /= 3;
	}
    while(number % 5 == 0) {
        number /= 5;
	}
    return number == 1;		/*如果是丑数返回1,不是返回0 */
}

接下来遍历1~1500这1500个整数,判断每个数是否是丑数,并将丑数输出。算法描述如下。

int printUglyNumbers(int limit) {
	int count = 0, i;
	for (i = 1; i <= limit; i++) {
		if (isUglyNumber(i)) {
			count++;
			printf("%5d", i);
		}
	}
    return count;
}

函数printUglyNumbers可将1~limit之间的丑数输出,并返回1~limit之间丑数的个数。如果要计算1500以内的全部丑数,只需将1500作为参数传递给函数printUglyNumbers。

这个方法的实现固然简单,但是题目中要求算法效率应尽量高,而上述算法还有优化空间。虽然该算法的时间复杂度是O(n),但是里面有很多冗余计算。因为通过穷举筛选法在指定的范围内逐一判断每个数是否是丑数,就无法避免对不是丑数的数字进行判断。实践证明,整数区间越向后移,丑数的个数越少,例如[1,100]包含34个丑数,而[8000,9000]仅包含6个丑数。所以采用穷举筛选法搜索丑数,搜索范围越大,无用的计算越多。

采用穷举筛选法是无法避免对搜索区间内的每个整数进行判断的,这是该算法的局限。其实可以通过计算直接获取某一范围内的丑数,这种方法比穷举筛选法更高效。

根据丑数的特性可知,丑数中只包含因子2,3,5,所以一个丑数乘以2或乘以3或乘以5之后得到的仍是丑数,而任何除1外的丑数,都能通过一个丑数再乘以2或乘以3或乘以5获得,所以可以采用将已有的丑数乘以(2,3,5)得到新丑数的方法来计算某一范围内的丑数,这样每次计算得到的都是丑数,效率要比穷举筛选法高很多。

我们可以将计算出来的丑数放入到一个数组当中,这样就保证了数组中保存的整数都是丑数,那么数组中的下一个丑数一定是数组中已存在的某个丑数乘以2或乘以3或乘以5所得。这是使用该算法计算丑数的核心思想,如图(1)所示。

如图(1)所示,数组中的每一个新的丑数都是通过数组中已有的丑数乘以(2,3,5)得到的,这样丑数不断地在数组内增加,直到达到我们预期查找的范围为止。

那么问题来了:新的丑数要通过数组中已有的丑数获得,那么第一个丑数怎样获取呢?由于数组中已有的任何一个丑数乘以(2,3,5)都是丑数,下一个丑数要怎样获得才能保证数组中的丑数没有遗漏呢?

第一个问题很好解决,因为约定1是丑数,而1乘以(2,3,5)得到的也是丑数,所以数组中的第一个元素设置为1即可,以此为基础衍生出后续的丑数。

第二个问题则相对复杂一些。我们先看一个例子,看看这样计算丑数会不会有问题。

假设要计算[1,10]范围内的丑数,最初数组中只存放1,数组内容为{1};再用1*2得到第二个丑数2,数组内容变为{1,2};再用2*2得到第三个丑数4,数组内容变为{1,2,4};再用4*2得到第四个丑数8,数组内容变为{1,2,4,8};再用2*5得到第五个丑数10,数组内容变为{1,2,4,8,10}。最终得到[1,10]范围内的丑数为{1,2,4,8,10}。

这种计算方法当然是有问题的,至少3和5没有算进去。出现这个错误的原因是我们计算“下一个丑数”的方法不对,不能保证计算出来的结果不重不漏。那么怎样保证计算结果没有重复和遗漏呢?用一句话概括就是:保证计算出来的“下一个丑数”是顺序递增且增量最小。换句话说,每次计算的“下一个丑数”都是大于数组中最后丑数的所有丑数中最小的一个。我们仍以计算[1,10]范围内的丑数为例介绍这个方法。如图(2)所示。

通过上面的一系列计算,得到的丑数集合是不重和不漏的。但是新的问题又出来了:怎样计算大于数组中最后丑数的所有丑数中最小的一个呢?如果像图21-14所示那样把数组中已有的丑数都计算一个遍(分别乘2,3,5)再找出合适的取值,显然是存在冗余计算的。因为数组元素一定是顺序递增的,所以每次计算时只要从上次计算的点向后继续计算即可,前面的元素没有必要重复计算了。那么具体应该怎样做呢?

为了讲清楚这个算法,我们先给出算法的代码描述,然后再进行详细的讲解。

int getNext(int *loc2, int *loc3, int *loc5, int array[], int index) {
	while (array[*loc2]*2 <= array[index]) {
		(*loc2)++;
	}
	while (array[*loc3]*3 <= array[index]) {
		(*loc3)++;
	}
	while (array[*loc5]*5 <= array[index]) {
		(*loc5)++;
	}
	if (array[*loc2]*2 < array[*loc3]*3) {
		return array[*loc2]*2 < array[*loc5]*5 ? array[*loc2]*2 : array[*loc5]*5;
	} else {
		return array[*loc3]*3 < array[*loc5]*5 ? array[*loc3]*3 : array[*loc5]*5;
	}
}

函数getNext的作用是根据数组中已有的丑数获取下一个丑数,并将其返回。参数array为该数组的数组名,index为当前数组中最后一个元素的下标,参数*loc2, *loc3, *loc5分别为数组array中三个丑数元素的下标指针,通过这三个下标指针可以计算得到下一个丑数。

在函数中loc2指向的元素只做乘2操作,loc3指向的元素只做乘3操作,loc5指向的元素只做乘5操作。

while (array[*loc2]*2 <= array[index]) {
    (*loc2)++;
}

通过上面的while循环,最终array[*loc2]*2会大于array[index],并且array[*loc2]是数组已有元素中乘以2大于array[index]的最小值。同理可知,array[*loc3]是数组已有元素中乘以3且大于array[index]的最小值,array[*loc5]是数组已有元素中乘以5且大于array[index]的最小值。

if (array[*loc2]*2 < array[*loc3]*3) {
	return array[*loc2]*2 < array[*loc5]*5 ? array[*loc2]*2 : array[*loc5]*5;
} else {
	return array[*loc3]*3 < array[*loc5]*5 ? array[*loc3]*3 : array[*loc5]*5;
}

然后再通过上面的条件判断,计算出array[*loc2]*2,array[*loc3]*3和array[*loc5]*5中的最小值,该值就是我们求得的“下一个丑数”。

注意,由于采用指针传递,数组下标loc2,loc3和loc5将在getNext中被修改,在下一次计算“下一个丑数”时,只需要从loc2,loc3和loc5指向的数组元素开始继续向下计算查找即可,因为它们前面的数组元素乘以(2或3或5)都是小于当前数组中最后一个丑数的,所以是没必要再重复计算比较的了。

按照上面步骤逐个计算数组中的“下一个丑数”,每次得到的丑数都是从数组中已有丑数里衍生出来的丑数中的最小的一个。通过这种方式获得丑数的算法可描述如下:

int printUglyNumbers(int limit) {
	int count = 1;
	int uglyNumberArray[1000];
	int loc2 = 0, loc3 = 0, loc5 = 0;
	int m2, m3, m5, max;
	int index = 0;
	int i;

	uglyNumberArray[0] = 1; 				/*初始化数组,第一个赋值为1*/
	max = getNext(&loc2,&loc3,&loc5,uglyNumberArray,index); /*计算下一个丑数*/

	while (max <= limit) {			/*循环计算下一个丑数,直到下一个丑数大于上限limit为止*/
		index++;						/*index指向数组中最后一个元素*/
		uglyNumberArray[index] = max;   	/*将得到的丑数放入数组*/
		count++;						/*记录数组中元素的个数*/
		max = getNext(&loc2,&loc3,&loc5,uglyNumberArray,index);
	}
	for (i=0; i<count; i++) {
		printf("%5d",uglyNumberArray[i]);	/*打印出数组中的全部元素*/
	}
	return count;
}

函数printUglyNumbers可将1~limit之间的丑数输出,并返回该范围内丑数的个数。

如果我们用上述两种方法计算1~1500之间的丑数,可得到如图(3)所示的结果。

想要阅读更多算法面试类精选文章,可关注我的微信公众号 @算法匠人

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值