这个结果我等了电脑一辈子,但祂一下就算出来了,这是什么计算方法?!

目录

题目

原题目

题目拓展

需求分析

原题目需求分析

拓展内容需求分析

思路分析

数据举例

规律分析

边界处理

代码实现

穷举法实现

原题与扩展需求代码实现

细节迭代

边际处理

测试

测试代码

固定范围对比测试

随机范围值测试

引用列表

碎碎念


题目

原题目

  • 请统计 1~100 之间 9 出现的次数

题目拓展

  • 请统计 任意数值区间内任意进制内的任意一个数值出现的次数,进制不能小于2

需求分析

原题目需求分析

  • 数值1~100之间9出现的次数
    • 找到所有位各自9出现的次数
    • 统计所有位出现的次数

拓展内容需求分析

  • 数值begin ~ 数值end 之间 于进制BaseNum下 数值value 出现的次数
    • 数值可能存在的范围
      • 数值可能异号
        • 数值一正一负
      • 数值可能同号
        • 数值可能同时为正数
        • 数值可能同时为负数
    • 进制BaseNum
      • 当进制小于2时进制无意义,如二进制的单个位表示范围为 0~1,则1进制的单个位只能表示0,所以进制小于2时无意义
    • 被查询的数值 value
      • 被查询的值为在该进制下一个位能表示的范围中的一个
        • 十六进制下,该值的可取范围为 0 ~ F
        • 十进制下,该值的可取范围为 0 ~ 9
        • 八进制下,该值的可取范围为 0 ~ 7
        • 二进制下,该值的可取范围为 0 ~ 1
        • 任意有意义的进制BaseNum下,该值的可取范围为 0 ~ (BaseNum - 1)
    • 变量的取值范围
      • 进制越小,则在明确的有限范围下需要查询的数值出现的次数越多,所以存放结果的变量应尽量大,我使用 signed long long
      • 数值的范围越大,结果溢出的可能性越高,所以我们存放数值范围的变量应尽量足够使用且单个数值的上限尽量低,所以使用 signed int
      • 为了防止溢出之后得到错误结果,我们没有使用 unsigned ,牺牲了一半的数值存储上限我们换来了数据的可靠性,因为我们可以用正值表达正确的结果负数表示数据溢出并退出计算

思路分析

数据举例

  • 1~100
    1. 个位上拥有9的数值
      1.         9 19 ... 89 99
      2. 总计 10 个数值个位上拥有数值 9
    2. 十位上拥有9的数值
      1.         90 91 ... 98 99
      2. 总计 10 个数值十位上拥有数值 9
    3. 百位上拥有9的数值
      1.        
      2. 总计 0 个数值百位上拥有数值 9
    4. 总计 个位 + 十位 + 百位 = 10 + 10 + 0 = 20
  • 1~1000
    1. 个位上拥有9的数值
      1.         9 19 ... 99 109 ... 989 999
      2. 总计 100 个数值个位上拥有数值 9
    2. 十位上拥有9的数值
      1.         90 91 ... 99 190 ... 998 999
      2. 总计 100 个数值十位上拥有数值 9
    3. 百位上拥有9的数值
      1.         900 901 ...  998 999
      2. 总计 100 个数值百位上拥有数值 9
    4. 千位上拥有9的数值
      1.        
      2. 总计 0 个数值千位上拥有数值 9
    5. 总计 个位 + 十位 + 百位 + 千位 = 100 + 100 + 100 + 0 = 300
  • -1~-100
    1. 个位上拥有9的数值
      1.         -9 -19 ... -89 -99
      2. 总计 10 个数值个位上拥有数值 9
    2. 十位上拥有9的数值
      1.         -90 -91 ... -98 -99
      2. 总计 10 个数值十位上拥有数值 9
    3. 百位上拥有9的数值
      1.        
      2. 总计 0 个数值百位上拥有数值 9
    4. 总计 个位 + 十位 + 百位 = 10 + 10 + 0 = 20
  • 1~2987
    1. 个位上拥有9的数值      
      1.         9 19 ... 99 109 ... 999                                                                         1009 ... 1999                                                                                       2009 ... 2899                                                                                     2909 ... 2979
      2. 总计 100 * 2 + 10 * 9 + (7 + 1) = 298 个数值个位上拥有数值 9
    2. 十位上拥有9的数值
      1.         90 ... 99 190 ... 199 290 ... 999                                                    1090 ... 1999                                                                                      2090 ... 2099 2190 ... 2199 2290 ... 2890 ... 2899
      2. 总计 100 * 2 + 10 * (8 + 1) = 290  个数值十位上拥有数值 9
    3. 百位上拥有9的数值
      1.         900 ... 999                                                                                          1900 ... 1999                                                                                 2900 ... 2987
      2. 总计 100 * 2 + (87 + 1) = 288 个数值百位上拥有数值 9
    4. 千位上拥有9的数值
      1.         
      2. 总计 0 个数值千位上拥有数值 9
    5. 总计 个位 + 十位 + 百位 + 千位 = 298 + 290 + 288 + 0 = 876 个

规律分析

  • 取1~2987之间 9 出现的次数
    • 十位的数值为 8 ,小于需查找的数值 9
      • 十位上出现9的次数分为 整次 (10 * 10 * 2) + 整次 (10 * (8 + 1))
        • 拆开来看,10 * 10 为 90~99 中 出现的次数 10 乘上 90~999(90~99) 这个区间出现的次数 10,而最外层的 * 290~1999 中 (90~999) 这个区间出现的次数 2
        • 拆开来看 10 * (8 + 1) 为 (90~99)这个区间 在 2090~2899 这个区间内出现的次数 90 次
      • 它们加起来的结果就为 9十位上总计出现的次数,而这个数值为290,也就是十位之前的所有数位的值但之前的数位的最低位为10位的值——这个区间内 9 出现的次数就为 (29) * 10 = 290
    • 百位数值为 9 ,等于需查找的数值9
      • 百位上出现9的次数分为 整次 (10 * 10 * 10 * 2) + 整次 (87 + 1) 
        • 拆开来看,10 * 10 为 90~99 中 出现的次数出现的次数 10 乘上 90~999(90~99) 这个区间出现的次数 10,而最外层的 * 290~1999 中 (90~999) 这个区间出现的次数 2
        • 拆开来看 (87 + 1) 为 9 在 2900~2987 这个区间内出现的次数 87 次
  • 综上,我们可以得出每一位需要统计出现次数的数值出现的次数都为三部分组成
    • 之前位和以当前位的位阶为之前位最低位的位阶2999,当前位为十位,之前位和为290
    • 当前位和如果当前位数值大于需查询的数值,则为一个进制基本值,如十进制为102999,当前位为十位9,需查询数值为3,进制为10,当前位和位为10
    • 之后位和如果当前位数值等于需查询的数值,则为之后位和的数值2999,当前位为百位9,需查询数值为9,之后位为99
    • 把之前位和与当前位和与之后位和加起来,结果就为当前位阶需查询的数值出现的总次数

边界处理

如上,我们得出了每一位的需查询数值出现的次数为 (之前位和) + (当前位和) + (之后位和)

但是除了当前位是确定的存在的数值,之前位与之后位都不一定存在,所以我们将要处理此边界

  1. 之前位和
    1. 之前位如果没有就是0
  2. 之后位和
    1. 之后位如果没有就是0
  3. 当前位和
    1. 如果待查询值大于当前位数值则当前位和为进制基数,否则为0
  4. 进制数的起始值
    1. 进制的起始值为 2 ,2进制的表达范围为 0~1 ,1进制的表达范围为 0一进制无法表达变化,所以是无意义的
  5. 位阶的起始值
    1. 位阶的起始值为 1 ,每次往左移动一位时 *= 进制数,如果位阶起始值为0,则无法往左移
  6. 存放结果的变量大小
    1. 在输入的最大范围确定为 INT_MINI ~ INT_MAX; 进制确定的情况下,为不丢失精度我们使用 signed long long 类型的变量 sum存储结果
  7. 数据溢出的错误处理
    1. 为了能监测到数据溢出,我们只能牺牲掉一部分的数据存储上限换取数据的可靠性;即使抛弃的一半的存储空间;即便如此, signed long long 的数据存储范围依然很大,足够常规情况的计算。当数值为负值时,数据就溢出了,此时我们结束运算并返回 EOF来表示数据溢出

代码实现

穷举法实现

我将以上所提到的内容替换为

  1. 进制为 10 的内容变更为常变量 nBaseValue
  2. 需查询的数值 9 内容变更为常变量 nFindValue

穷举法实现任意数值范围查询以验证之后算法的正确性

//我暂时还没弄明白运行代码怎么弄,所以这里只贴上代码与运行结果截图

signed long long FindValueNumbers_Test(signed int begin, signed int end, const unsigned int nFindValue, const unsigned int nBaseValue)
{
	signed int i = 0;
	signed long long sum = 0;

	//先对数据排序,再进行计算
	signed int temp = begin;
	if (begin > end)
	{
		begin = end;
		end = temp;
	}

	//穷举法
	for (i = begin; i <= end; i++)
	{
		signed int tempI = abs(i);
		while (tempI > 0)
		{
			if (tempI % nBaseValue == nFindValue)
			{
				sum++;
			}
			tempI /= nBaseValue;
		}
	}

	return sum;
}

原题与扩展需求代码实现

signed long long FindValueUnsignedNumbersStaringFromZero	//求从0开始到end这个区间内nFindValue在每一位出现的次数,进制数为nBaseValue
(	const unsigned int end,						    		//范围结束限定值
	const unsigned int nFindValue,							//需要寻找的数值
	const unsigned int nBaseValue							//进制,不能小于2
){
	assert(nBaseValue >= 2);			//进制不萌小于2,这是无意义的

	signed int nTempDigit = 1;
	signed int nCurNumberEnd = abs(end);

	signed int nDigitNextNumber = 0;
	signed int nDigitPrevNumber = 0;
	signed int nCurrentDigitNumber = 0;

	signed long long sum = 0;

	//用循环遍历每一位 - 拿到之前位与剩下位进行计算
	while (nCurNumberEnd)
	{
		signed long long nPrevSum = sum;	//溢出判定控制用

		nCurrentDigitNumber = nCurNumberEnd % nBaseValue;			//拿到当前位的数值

		//整体数值 % 当前位位阶 = 之后位数值
		//9 999 % 1000 = 999
		//99 99 % 100 = 99
		//999 9 % 10 = 9
		//9999  % 0 = 0
		nDigitNextNumber = (nTempDigit >= nBaseValue) ? (end % nTempDigit) : 0;							//拿到之后位的数值

		//整体数值 / (当前位的前一位的位阶) = 之前位数值
		//999 9  / 10 = 999
		//99 9 9 / 100 = 99
		//9 9 99 / 1000 = 9
		// 9 999 / 1000 = 0
		nDigitPrevNumber = end / (nBaseValue * nTempDigit);		//拿到之前位的数值

		//计算
		{

			//每一位需要统计出现次数的数值出现的次数都为三部分组成
			//之前位和:以当前位的位阶为之前位最低位的位阶;2999,当前位为十位,之前位和为290
			//当前位和:如果当前位数值大于需查询的数值,则为一个进制基本值,如十进制为10;2999,当前位为十位9,需查询数值为3,进制为10,当前位和位为10
			//之后位和:如果当前位数值等于需查询的数值,则为之后位和的数值;2999,当前位为百位9,需查询数值为9,之后位为99

			sum += ((signed long long)nDigitPrevNumber * nTempDigit);						//之前位和
			sum == (nCurrentDigitNumber > nFindValue) ? nBaseValue : 0;					//当前位和
			sum += (nCurrentDigitNumber == nFindValue) ? nDigitNextNumber + 1 : 0;		//剩下位和
		}

		nTempDigit *= nBaseValue;		//进制位阶增加
		nCurNumberEnd /= nBaseValue;		//数值位指向数值段向左移动
		// 1   9  >5<  6 指向数值段为5
		// 1  >9<  5   6 指向数值段为9

		//数据溢出/异常处理
		if (nPrevSum > sum)
		{
			//printf("单项总和溢出上限\n");
			return (signed long long)EOF;
		}
	}

	return sum;
}

以上变量变更逻辑应该还算通俗易懂,根据变量名可知一二

大体就是当前位阶不断左移,由nCurNumberEnd控制当前位;

不断将指向当前位的那根手指向左移,拿到手指左边的数值、手指中间的数值以及手指右边的数值;根据上面总结的规律对这三个数值进行运算得到结果加进sum内


细节迭代

边际处理

现在需要解决的问题

  1. 数值范围只有结束点,起始点已经被固定了
  2. 数值只能为正数,不支持负数

问题解决方案

  1. 假定 abs(begin) 小于 abs(end) 且都为同号,则 begin~end 区间内 nFindValue 出现的次数为0~abs(end) - 0~abs(begin - 1)
  2. 开始与结束的数值异号nFindValue 出现的次数为 0~abs(begin) + 0~abs(end) 的和,如果nFindValue 为 0,则 0 被计算了两次,应在结束时 -1 
  3. 解决了以上问题之后,出现了新的问题
    1. 如果begin、end、nFindValue都同时为0,则算法不会将唯一的0计算进sum内此时我们需要对这类情况单独处理
    2. 当开始值与结束值相等时,再走以上流程就会显得过分繁杂,只计算一个数值却进行了很多余的计算,所以我们可以将仅开始值与结束值相等的情况单独计算

在外嵌套一层函数

signed long long FindValueUnsignedNumbersStaringFromZero(unsigned int end, const unsigned int nFindValue, const unsigned int nBaseValue);    //函数声明,这是我们之前实现的函数

signed long long FindValueNumbers(signed int begin, signed int end, const unsigned int nFindValue, const unsigned int nBaseValue)
{
	//正常返回结果值
	//超过longlong的最大值返回EOF

	//进制数有效性确认
	assert(nBaseValue >= 2);

	signed long long nBigSum = 0;
	signed long long nSmallSum = 0;
	signed long long nRetSum = 0;

	//统计begin - 0之间nFindValue出现的次数
	//统计end - 0之间nFindValue出现的次数
	//如果异号,则相加两值,如查找值为0,则将统计值减去1,但开始、结束、查找值都为0时,直接返回1

	//开始与结束都为同一数值的情况,单独计算这一个数值节省算力
	if (begin == end)
	{
		if (nFindValue == begin)
		{
			//查找值等于起始值等于结束值,就只能查找到1个需查找值
			return 1;
		}
		//开始与结束都为同一个值时,统计这个值中nFindValue出现的次数
		signed int temp = begin;
		while (temp > 0)
		{
			if (temp % nBaseValue == nFindValue)
			{
				//统计每一位上出现9的次数
				nRetSum++;
			}
			temp /= nBaseValue;
		}
		return nRetSum;
	}

	if (begin < 0 && end >= 0 || begin >= 0 && end < 0)	//nSum1与nSum2异号,则sum和为绝对值的和
	{
		begin = abs(begin);
		end = abs(end);

		//拿到0到两端的sum
		nBigSum = FindValueUnsignedNumbersStaringFromZero(begin, nFindValue, nBaseValue);
		nSmallSum = FindValueUnsignedNumbersStaringFromZero(end, nFindValue, nBaseValue);

		if ((signed long long)EOF == nBigSum || (signed long long)EOF == nSmallSum)
		{		//算术溢出返回EOF
			return (signed long long)EOF;
		}

		nRetSum = nBigSum + nSmallSum;

		if (nRetSum < nBigSum || nRetSum < nSmallSum)
		{
			//算术溢出返回EOF
			return (signed long long)EOF;
		}

		if (0 == nFindValue)
		{
			//如果查找的数值就是0,那么查找的范围两端异号时会重复查找1次0,需去除
			nRetSum -= 1;
		}
	}
	else		//同号为绝对值大的值减去绝对值少的值
	{
		//排序一下
		begin = abs(begin);
		end = abs(end);

		//进行一个大小的排序
		if (begin > end)
		{
			int temp = begin;
			begin = end;
			end = temp;
		}

		//统计begin~end之间9的数量 0~end - 0~(begin - 1)
		if (begin > 0)
		{
			begin -= 1;
		}

		//获取到两头的数值的数量
		nBigSum = FindValueUnsignedNumbersStaringFromZero(begin, nFindValue, nBaseValue);
		nSmallSum = FindValueUnsignedNumbersStaringFromZero(end, nFindValue, nBaseValue);

		if ((signed long long)EOF == nBigSum|| (signed long long)EOF == nSmallSum)
		{		//算术溢出返回EOF
			return (signed long long)EOF;
		}

		nRetSum = nSmallSum - nBigSum;		//拿到绝对值大的与绝对值小的sum,用大的减去小的得到区间内的和
	}

	return nRetSum;	//返回值为大值减去小值
}

测试

测试代码

signed long long FindValueNumbers_Test(signed int begin, signed int end, const unsigned int nFindValue, const unsigned int nBaseValue);

signed long long FindValueNumbers(signed int begin, signed int end, const unsigned int nFindValue, const unsigned int nBaseValue);

void Test_06()
{

	printf("TEST_06_OUT\n");

	const unsigned int nFindValue = 9;
	const unsigned int nBaseValue = 10;

	signed int begin = 0;
	signed int end = 0;

	while (1)
	{
		printf("请依次输入 begin 与 end\n");
		if (EOF == scanf("%d%*c%d", &begin, &end))
		{
			return;
		}

		printf("计数法: %d ~ %d 之间 %d 出现的次数是 %lld\n", begin, end, nFindValue, FindValueNumbers(begin, end, nFindValue, nBaseValue));
		printf("穷举法: %d ~ %d 之间 %d 出现的次数是 %lld\n", begin, end, nFindValue, FindValueNumbers_Test(begin, end, nFindValue, nBaseValue));
	}

}

固定范围对比测试

输入:

1~100 -1~-100 9999~90 -1000~2345

输出:

随机范围值测试

想要一个随机的大数值,可以将两个字符串以数值的形式读取,这两个字符串请尽量每个字符串都写满4字节

具体实现方法我贴在这里

	char szBegin[] = "你好";
	char szEnd[] = "世界";

	begin = -(*((signed int*)szBegin));
	end = *((signed int*)szEnd);

测试结果:

这是一个非常大的数值范围的计算,可以看得出来穷举法已经把我的cpu干爆了,而计数法瞬间就能够得到结果。


引用列表


碎碎念

总算敲完了,我好像真的很想讲清楚我的思路,555555

在写博客的时候顺着过去的思路一步步推演,找到了不少我代码和注释里存在的隐患,一一解决再提出新问题的过程非常的快乐

⚡至少我这次屁股没有坐麻⚡

但……其实今天吃饭我又咬到舌头了

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值