一个关于“1到100中所有整数共出现多少次数字9”的突发奇想

1、题目

        编写程序数一下 1 到 100 中所有整数共出现多少次数字 9。

2、正常算法

        这题只需用 for 循环遍历 1~100 中所有数,对每个数通过模 10 判断个位是否为 9 ,并且除以 10 判断十位是否为 9 即可:

#include <stdio.h>

int main() {

	int count = 0;
	for (int i = 1; i <= 100; i++)
	{
		count += i / 10 == 9;
		count += i % 10 == 9;
	}
	printf("%d\n", count);

	return 0;
}

//输出 20

2.1、逻辑漏洞

        但是这种算法有个不算 BUG 的 BUG 。当遍历到 100 的时候,100 / 10 == 9 这语句虽然结果正确,但逻辑上是有问题的。假设题目问的是 1 到 1000 中所有整数共出现多少次数字 9 ,这段代码也没法准确计数:

#include <stdio.h>

int main() {

	int count = 0;
	for (int i = 1; i <= 1000; i++)
	{
		count += i / 10 == 9;
		count += i % 10 == 9;
	}
	printf("%d\n", count);

	return 0;
}

//输出 110

        正如算法所构思的,只判断了个位和十位,而判断十位的方法仅仅是通过除以 10 实现的。也就是说,如果是个三位数以上的数字,则会将十位含以上的数值当作十位数进行判断。 

2.2、代码修正 1.0

        因此,必须确定判断的位数进行准确提取,依然通过除法和取模实现:

#include <stdio.h>

int main() {

	int count = 0;
	for (int i = 1; i <= 1000; i++)
	{
		
		count += i % 10 == 9;		//判断个位数
		count += i / 10 % 10 == 9;	//判断十位数
		count += i / 100 % 10 == 9;	//判断百位数
		count += i / 1000 % 10 == 9;//判断千位数
		//......
		
	}
	printf("%d\n", count);

	return 0;
}

//输出300

        但是,另一个问题接踵而至,如果判断的不是 9 出现的次数,而是 0 呢:

#include <stdio.h>

int main() {

	int count = 0;
	for (int i = 1; i <= 1000; i++)
	{
		
		count += i % 10 == 0;		//判断个位数
		count += i / 10 % 10 == 0;	//判断十位数
		count += i / 100 % 10 == 0;	//判断百位数
		count += i / 1000 % 10 == 0;//判断千位数
		//......
		
	}
	printf("%d\n", count);

	return 0;
}

//输出1299

       统计到 1299 个 0 显然是出了问题。

        当 i 是1~9 时,i / 10 %10 = 0,因此,count 错误地加 1 ,当 i 是 10~99 时,i / 100 % 10 = 0,count 又错误地加 1 。如果在上面代码中再加一行:

count += i / 10000 % 10 == 0;

则统计为 2299 个 0 。

2.3、代码修正 2.0

        修正以上BUG只需要先判断 i / 10 、i / 100 、i / 1000 等的值是否大于 0 ,若不大于 0 则不执行操作即可,此时最容易想到的便是引入 if 语句:        

#include <stdio.h>

int main() {

	int count = 0;
	for (int i = 1; i <= 1000; i++)
	{
		
		if (i / 1 > 0)
			count += i % 10 == 0;
		if (i / 10 > 0)
			count += i / 10 % 10 == 0;
		if (i / 100 > 0)
			count += i / 100 % 10 == 0;
		if (i / 1000 > 0)
			count += i / 1000 % 10 == 0;
		if (i / 10000 > 0)
			count += i / 10000 % 10 == 0;
		//......
		
	}
	printf("%d\n", count);

	return 0;
}

//输出192

2.4、代码优化        

        观察上述代码,其中 count += i % 10 == 0 可以写作 count += i / 1 % 10 == 0 ,即是说,i 的除数以10倍递增,类似位移操作符。

        而假如当 i 取 20 时,后续 i / 100 、i / 1000 的判断是没有意义的。如果引入一个变量替代 i ,引入位移操作符的构思(每次判断之后都自除以 10),通过循环判断 ,当自身不大于 0 时循环停止:

#include <stdio.h>

int main() {

	int count = 0;
	int value = 0;
	for (int i = 1; i <= 1000; i++)
	{
		value = i;
		while (value > 0)
		{
			count += value % 10 == 9;
			value /= 10;
		}		
	}
	printf("%d\n", count);

	return 0;
}

//输出300

        由于这段代码对于任意的最小值、最大值及判断数都有效果,甚至能将其改为手动输入这三个参数:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main() {

	int count = 0;
	long long value = 0;

	long long i_min, i_max;
	int i_checkNum;

	printf("Input the limit and checking number (min max number):\n");
	scanf("%lld%lld%d", &i_min, &i_max, &i_checkNum);

	for (long long i = i_min; i <= i_max; i++)
	{
		value = i;
		while (value > 0)
		{
			count += value % 10 == i_checkNum;
			value /= 10;
		}		
	}
	printf("%d\n", count);

	return 0;
}

2.5、代码修正 3.0

        测试时发现,由于 while (value > 0) 的存在,这段代码不支持 0 和负数。

        先解决负数的问题。出现负数情况分两种:

                1、取值范围最小值小于0,最大值大于0;

                2、最小值最大值均大于0。

        解决思路是,当出现第一种情况时,最小值取绝对值,取值范围为 1~最小值的绝对值的统计结果,再累加上 0~最大值 的统计结果。

        如果是第二种情况,则直接将取值范围定为:最大值的绝对值~最小值的绝对值即可。

        但如果这样写即成屎山代码了。重新查看代码后,发现 i 上述构思核心是从取值范围最小值开始取值,如果取值范围包含负数,则将取值范围的负数部分变为绝对值。那不就是说,只要 value 取 i 的绝对值就可以了?

        因此将 value = i 改为:

value = (i < 0 ? -i : i);

        Nice。

        但这样,若判断数取 0 ,循环到 i = 0 时,仍旧会少统计一个 0 。因此在 for 循环中另行判断是否判断数与 i 同时为 0 即可:

#include <stdio.h>

int main() {

	int count = 0;
	long long value = 0;

	long long i_min, i_max;
	int i_checkNum;

	printf("Input the limit and checking number (min max number):\n");
	scanf("%lld%lld%d", &i_min, &i_max, &i_checkNum);

	for (long long i = i_min; i <= i_max; i++)
	{
		 value = (i < 0 ? -i : i);

		while (value > 0)
		{
			count += value % 10 == i_checkNum;
			value /= 10;
		}
		if (i_checkNum == 0 && i == 0)
			count++;
	}
	printf("%d\n", count);

	return 0;
}

3、再次拓展

3.1、新的问题

        代码虽然完美了,但是,既然是自由输入取值范围,假如,取值范围扩大到上亿?

//#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main() {

	int count = 0;
	long long value = 0;

	long long i_min, i_max;
	int i_checkNum;

	//printf("Input the limit and checking number (min max number):\n");
	//scanf("%lld%lld%d", &i_min, &i_max, &i_checkNum);

	i_min = 1;
	i_max = 1000000000;
	i_checkNum = 0;

	for (long long i = i_min; i <= i_max; i++)
	{
		 value = (i < 0 ? -i : i);

		while (value > 0)
		{
			count += value % 10 == i_checkNum;
			value /= 10;
		}
		if (i_checkNum == 0 && i == 0)
			count++;
	}
	printf("%d\n", count);

	return 0;
}

//输出788888898

        控制台已经得出结果前已经开始有二三十秒的延迟了,穷举法的弊端得以显现。如果是百亿乃至千亿更是不可想象。

        此外:

        因此,开整!

3.2、算法构思

        将问题回退至 1 到 100 中所有整数共出现多少次数字 9 。

        个位出现 9 的数字:

        9、19、29、39、49、59、69、79、89、99 。

        十位出现 9 的数字:

        90、91、92、93、94、95、96、97、98、99 。

        貌似是有规律的。

        而且,好像是可以通过计算,求出各个数位之中判断数出现的次数,最后再相加,这不就能得到最终结果。而且若取值上限为十亿,穷举法需要循环十亿次,而这种方式但凡能找出规律,只需要循环十次即可得到结果,计算量大大大大大大地减少了。

3.2.1、准备工作

        说干就干。为了方便一探究竟,先将穷举法的程序改进一下,使其能分别显示每一位对应数字各出现多少次,以方便进行验证:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main() {

	int count = 0;
	long long value = 0;
	long long i_min, i_max;
	int i_checkNum;
	int digit = 0;													//新增
	int site[10] = { 0, 0, 0, 0, 0, 0, 0 , 0 , 0 , 0 };				//新增

	printf("Input the limit and checking number (min max number):\n");

	while (scanf("%lld%lld%d", &i_min, &i_max, &i_checkNum) != EOF)	//偷懒改的
	{
		for (long long i = i_min; i <= i_max; i++)
		{
			value = (i < 0 ? -i : i);
			digit = 0;												//新增

			while (value > 0)
			{
				count += value % 10 == i_checkNum;
				site[digit] += value % 10 == i_checkNum;			//新增
				value /= 10;
				digit++;											//新增
			}
			if (i_checkNum == 0 && i == 0)
			{
				count++;
				site[0]++;											//新增
			}
		}
		system("CLS");												//新增

		for (int i = 0; i <= 9; i++)								//新增
		{
			printf("%d\t%d\n", 10 - i, site[10 - i - 1]);			//新增
			site[10 - i - 1] = 0;									//新增.初始化
		}
		printf("---------------\nTotal\t%d\n", count);				//改动
		count = 0;													//新增.初始化
	}

	return 0;
}

3.2.2、初步构思

        随便假定取值范围是 1 ~ 35796 ,判断数为 3,则判断数在各数位出现的范围是:

                万位:30000 ~ 35796,共 5797 个;

                千位:3000 ~ 3999、13000 ~ 13999、23000 ~ 23999、33000 ~ 33999,共 4000 个;

                百位:300 ~ 399、1300 ~ 1399、2300 ~ 2399……35300 ~ 35399,共 3600 个;

                十位:30 ~ 39、130 ~ 139、230 ~ 239……35730 ~ 35739 ,共 3580 个;

                个位:3、13、23、33……35793,共 3580 个。

        而如果判断数为 6,则判断数在各数位出现的范围是:

                万位:共 0 个;

                千位:6000 ~ 6999、16000 ~ 16999、26000 ~ 26999,共 3000 个;

                百位:600 ~ 699、1600 ~ 1699 、2600 ~ 2699……35600 ~ 35699,共 3600 个;

                十位:60 ~ 69、160 ~ 169、260 ~ 269……35760 ~ 35769,共 3580 个;

                个位:6、16、26、36……35796,共 3580 个。

3.2.3、寻找规律

        上述列举的各数位判断数出现次数,其总结过程发现以下规律(假设取值下限是 1 ,且判断数不为 0 ):

        (c:判断数,t:统计结果,max:取值上限,m1:取值上限个位,m2:取值上限十位,以此类推)

       a、 如果取值上限只有一位数:

                (a)、当 m1 ≥ c,则 t += 1;

                (b)、当 m1 < c,则 t += 0。

        b、如果取值上限有两位数:

                (a)、当 m2 > c,则 t += 10 ;

                (b)、当 m2 = c,则 t += max % 10 + 1 ;

                (c)、当 m2 < c,则 t += 0;

                (d)、当 m1 ≥ c,则 t += max / 10 + 1 ;

                (e)、当 m1 < c,则 t += max / 10。

        c、如果取值上限有三位数: 

                (a)、当 m3 > c,则 t += 100 ;

                (b)、当 m3 = c,则 t += max % 100 + 1 ;

                (c)、当 m3 < c,则 t += 0;

                (d)、当 m2 > c,则 t += max / 100 * 10 + 10;

                (e)、当 m2 = c,则 t += max / 100 * 10 + max % 10 + 1;

                (f)、当 m2 < c,则 t += max / 100 * 10;

                (g)、当 m1 ≥ c,则 t += max / 10 + 1 ;

                (h)、当 m1 < c,则 t += max / 10 。

        d、如果取值上限有四位数:

                (a)、当 m4 > c,则 t += 1000 ;

                (b)、当 m4 = c,则 t += max % 1000 + 1 ;

                (c)、当 m4 < c,则 t += 0;

                (d)、当 m3 > c,则 t += max / 1000 * 100 + 100;

                (e)、当 m3 = c,则 t += max / 1000 * 100 + max % 100 + 1;

                (f)、当 m3 < c,则 t += max / 1000 * 100;

                (g)、当 m2 > c,则 t += max / 100 * 10 + 10 ;

                (h)、当 m2 = c,则 t += max / 100 * 10 + max % 10 + 1;

                (i)、当 m2 < c,则 t += max / 100 * 10;

                (j)、当 m1 ≥ c,则 t += max / 10 + 1;

                (k)、当 m1 < c,则 t += max / 10。

3.2.4、统一规律

        先以四位数为标准,不满足四位数的在高位补 0 。因此,以 0004 作为上限,判断数为 2 ,代入上述总结的四位数规律中:

                a、因为 m4 < 2,所以 t = t + 0,t = 0;

                b、因为 m3 < 2,所以 t = t + 0004 / 1000 * 100 = t + 0,t = 0;

                c、因为 m2 < 2,所以 t = t + 0004 / 100 * 10 = t + 0 = 0,t = 0;

                d、因为 m1 > 2,所以 t = t + 0004 / 10 + 1 = t + 1,t = 1。

        结果正确。而且在推导上述式子的过程中,根据四位数的(d)(e)(f)三点对比三位数的(a)(b)(c)三点发现,三位数这几条完全是因为千位为 0 而可以省略了一部分,当千位为 0 时,这两个式子完全等价。以此推导,四位数的规律不就能写成:

                (a)、当 m4 > c,则 t += max / 10000 * 1000 + 1000 ;

                (b)、当 m4 = c,则 t += max / 10000 * 1000 + max % 1000 + 1 ;

                (c)、当 m4 < c,则 t += max / 10000 * 1000;

                (d)、当 m3 > c,则 t += max / 1000 * 100 + 100;

                (e)、当 m3 = c,则 t += max / 1000 * 100 + max % 100 + 1;

                (f)、当 m3 < c,则 t += max / 1000 * 100;

                (g)、当 m2 > c,则 t += max / 100 * 10 + 10 ;

                (h)、当 m2 = c,则 t += max / 100 * 10 + max % 10 + 1;

                (i)、当 m2 < c,则 t += max / 100 * 10;

                (j)、当 m1 > c,则 t += max / 10 * 1 + 1;

                (k)、当 m1 = c,则 t += max / 10 * 1 + max % 10 + 1;

                (l)、当 m1 < c,则 t += max / 10 * 1。

        再往上推导,五位数无非是在开头增加:

                (a)、当 m5 > c,则 t += max / 100000 * 10000 + 10000 ;

                (b)、当 m5 = c,则 t += max / 100000 * 10000 + max % 10000 + 1 ;

                (c)、当 m5 < c,则 t += max / 100000 * 10000;

        所以,对于任意取值上限的第 n 位数,该位置上判断数出现的次数均有:

m_{n}\begin{Bmatrix} >c & \\ =c & \\ <c & \end{Bmatrix}\Rightarrow max\div 10^{n}\times 10^{n-1}+ \left\{\begin{matrix} 10^{n-1} & \\ mod(max,10^{n-1})+1 & \\ 0 & \end{matrix}\right.

3.2.5、基础功能代码

        用代码描述:

#include <math.h>

//i_max:取值范围上限
//i_checkNum:判断数

	int digitValue = 0;
	int countLoop = 0;
	for (int i = i_max; i > 0; i /= 10)
	{
		digit++;
		digitValue = i_max % (int)pow(10, digit) / pow(10, digit - 1);

		countLoop = i_max / pow(10, digit);
		countLoop *= pow(10, digit - 1);
		
		if (digitValue > i_checkNum)
		{
			countLoop += pow(10, digit - 1);
		}
		else if (i_checkNum == digitValue)
		{
			countLoop += i_max % (int)pow(10, digit - 1) + 1;
		}
		count += countLoop;
	}

	printf("total:%d\n", count);

        而对于判断数为 0 时,假如取值范围为 1~23875 ,根据上式,得出:

万位10000
千位3000
百位2400
十位2390
个位2388

        但是然而实际上:

万位0
千位2000
百位2300
十位2380
个位2387

        两表相减:

万位10000
千位1000
百位100
十位10
个位1

        规律显而易见。之所以 0 会出现这种情况源于最高位不得为 0 ,所以统计结果的万位少了 00000~09999 共10000个,千位少了 00000~00999 共 1000 个数,以此类推。因此上述代码只需要在 if 语句之后加上一句:

countLoop -= (i_checkNum == 0) * pow(10, digit - 1);

         至此,基础部分便已经实现。

3.3、细化

        先将上述代码封装为一个函数:

int NumAppearTime(int i_checkNum, int i_max)
{
	int count = 0;		//统计结果
	int digit = 0;		//取值上限的位数
	int digitValue = 0;	//取值上限各数位对应的值
	int countLoop = 0;	//单个数位统计结果

	for (int i = i_max; i > 0; i /= 10)
	{
		digit++;
		digitValue = i_max % (int)pow(10, digit) / pow(10, digit - 1);

		countLoop = i_max / pow(10, digit);
		countLoop *= pow(10, digit - 1);

		if (digitValue > i_checkNum)
		{
			countLoop += pow(10, digit - 1);
		}
		else if (i_checkNum == digitValue)
		{
			countLoop += i_max % (int)pow(10, digit - 1) + 1;
		}
		countLoop -= (i_checkNum == 0) * pow(10, digit - 1);
		count += countLoop;
	}

	return count;
}

        封装函数作用,便可以使取值下限不仅限于 1 。比如,可以用 NumAppearTime(9, 255264) - NumAppearTime(9, 6729) 的值计算 6729~255264 中 9 的出现次数,用 NumAppearTime(9, 255264) + NumAppearTime(9, 6729) 的值计算 -6729~255264 中 9 的出现次数。

        对于取值范围上下限有三种情况:

        a、下限、上限均大于0:

//NumAppearTime函数的上限返回值减去下线返回值
count = NumAppearTime(i_checkNum, i_max) - NumAppearTime(i_checkNum, i_min);

        b、下限小于等于0、上限大于0:

//NumAppearTime函数上限返回值加上下限相反数的返回值
count = NumAppearTime(i_checkNum, i_max) + NumAppearTime(i_checkNum, -i_min);

        c、下限、上限均小于等于0:

//NumAppearTime函数下限相反数的返回值减去上限相反数的返回值
count = NumAppearTime(i_checkNum, -i_min) - NumAppearTime(i_checkNum, -i_max);

        此外,若判断数为 0 ,取值范围中也包含 0 这个数,由于 NumAppearTime 函数中的取值范围下限是从 1 开始,所以统计结果无论如何都不会统计到 0 这个数,因此会比实际情况少 1 。因此需要额外加上一段:

	if (0 >= i_min && 0 <= i_max && 0 == i_checkNum)
	{
		count += 1;
	}

         至此,main 函数已经基本完成了:

int main()
{
	long long int i_min = 0;
	long long int i_max = 0;
	int i_checkNum = 0;
	int count = 0;

	printf("Input the limit and checking number (min max number):\n");
	scanf("%lld%lld%d", &i_min, &i_max, &i_checkNum);

	if (0 >= i_min && 0 < i_max)
	{
		count = NumAppearTime(i_checkNum, i_max) + NumAppearTime(i_checkNum, -i_min);
	}
	else if (0 >= i_min && 0 >= i_max)
	{
		count = NumAppearTime(i_checkNum, -i_min) - NumAppearTime(i_checkNum, -i_max);
	}
	else
	{
		count = NumAppearTime(i_checkNum, i_max) - NumAppearTime(i_checkNum, i_min);
	}

	if (0 >= i_min && 0 <= i_max && 0 == i_checkNum)
	{
		count += 1;
	}

	printf("Total:\t%d\n", count);

	return 0;
}

         此外,再加上几句防止错误输入的语句即可,以下是完整代码:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <math.h>

int main()
{
	long long int i_min = 0;
	long long int i_max = 0;
	int i_checkNum = 0;
	int count = 0;

	printf("Input the limit and checking number (min max number):\n");
	while (scanf("%lld%lld%d", &i_min, &i_max, &i_checkNum) == 3)
	{
		i_checkNum = abs(i_checkNum) % 10;	//判断数只取个位,且不为负数

		if (i_max < i_min)					//取值范围上限必须大于等于下限
		{
			int temp = i_min;
			i_min = i_max;
			i_max = temp;
		}

		if (0 >= i_min && 0 < i_max)
		{
			//NumAppearTime 函数上限返回值 + 下限相反数的返回值
			count = NumAppearTime(i_checkNum, i_max) + NumAppearTime(i_checkNum, -i_min);
		}
		else if (0 >= i_min && 0 >= i_max)
		{
			//NumAppearTime 函数下限相反数的返回值 - 上限相反数的返回值
			count = NumAppearTime(i_checkNum, -i_min) - NumAppearTime(i_checkNum, -i_max);
		}
		else
		{
			//NumAppearTime 函数的上限返回值 - 下线返回值
			count = NumAppearTime(i_checkNum, i_max) - NumAppearTime(i_checkNum, i_min);
		}

		if (0 >= i_min && 0 <= i_max && 0 == i_checkNum)	//取值范围中也包含 0 ,则统计结果 + 1
		{
			count += 1;
		}

		printf("Total:\t%d\n", count);

	}

	return 0;
}

int NumAppearTime(int i_checkNum, long long int i_max)
{
	int count = 0;		//统计结果
	int digit = 0;		//取值上限的位数
	int digitValue = 0;	//取值上限各数位对应的值
	int countLoop = 0;	//单个数位统计结果

	for (int i = i_max; i > 0; i /= 10)
	{
		digit++;
		digitValue = i_max % (int)pow(10, digit) / pow(10, digit - 1);

		countLoop = i_max / pow(10, digit);
		countLoop *= pow(10, digit - 1);

		if (digitValue > i_checkNum)
		{
			countLoop += pow(10, digit - 1);
		}
		else if (i_checkNum == digitValue)
		{
			countLoop += i_max % (int)pow(10, digit - 1) + 1;
		}
		countLoop -= (i_checkNum == 0) * pow(10, digit - 1);
		count += countLoop;
	}

	return count;
}

        完结!撒花! 

  • 19
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值