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 位数,该位置上判断数出现的次数均有:
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;
}
完结!撒花!