好久没更新博客了,今天也来客博一下。
前两天在首页看到题目:给出一个数n,O(1)求解1到n这些数中1出现的次数。
看到这个题目,联想到另一个题目:给出一个数n,求1到n这些数之和.哇靠,小学算术题,这不是侮辱咱的智商嘛~计算机最擅长干重复的劳动了,于是乎,潇洒地抖出了下面code,把循环交给计算机去做:
2Int32 number = 100;
3for (Int32 i = 1; i <= number; i++)
4 sum += i;
OK,结果是正确的,思路相当地清晰,完了吗?没完!为了显示洒家精通C#,咱把循环改造下:
结果依然是正确的,完了吗?完了!不过是完蛋的“完”.难道我们一定要让自己的代码使别人费解,来显示我们多牛X吗!难道我们一定要让计算机拼命地干傻活儿,来证明现在处理器的性能多NB吗!为什么我们不把能做的做好,剩下的...让那坨废铁干它该干的活儿:
扯远了,拉回来说“正”题:给出一个数n,求解1到n这些数中1出现的次数。
最清晰直观的做法,就是遍历这N个数,便利N个数中的数位,累计求出1出现的次数;至于所谓的递归,没去看,也懒得去看;这里,我们先分析下这些阿拉伯数字的规律,然后根据出现1的概率来进行求解:
(1) 当n=9时,我们考虑0,1,2...9(共10个数字),我们不难得出结论:字符'0','1'...'9'出现的概率个占1/10(即0.1);
(2) 当n=99时,我们考虑0,1,2...99(共100个数字),十位上,'1'出现的概率个占1/10;个位上,'1'出现的概率也为1/10, 于是1出现的次数=0.1*100 + 0.1 * 100 = 20;
(3) 当n=100000000时,我们考虑0,1,2...100000000(共100000001个数字),最高位上,'1'只可能出现1次;其余各位上,'1'出现的概率各占1/10, 于是1出现的次数=1 + (0.1*8) * 100000000 = 80000001;
当然,上面都是列举的最简单的例子,但我们不难看出,'1'的出现概率的确是存在规律的。下面我们来看更复杂的例子('0','1'...'9',下面统称为阿拉伯数字符):设n=XYZ(其中X、Z表示零个或多个阿拉伯数字符,Y为一个阿拉伯数字),这样XYZ就可以表示任意数字了,例如n=5可以表示为:X=Z="",Y=5;n=123456可以表示成:X="",Y=1,Z=23456或X=12345,Y=6,Z=""等6种方式。
这里我取这种表达方式,是为了从Y上得出一个通用的统计字符出现概率的计算方法。Y为一个阿拉伯数字,所以其只能取0,1...9中的一个数字,其存在如下三种可能:
(1) Y<1
(2) Y=1
(3) Y>1
以n=123Y45(X=123,Y在百位上,可以任意取值,Z=45)来分别讨论上面的三种情况:
(1) Y<1:(此时Y只能取0值了),我们可以把区间[0..n]中的n+1个数分成两段区间来分析,[000000..122999],即[000000..122999],即[000000...(X-1)999],
[123000..123045],即[X000..XYZ],
其中第一段区间中Y位(百位)上,共(X * Pow(10, Length(Z)+1))个数字,出现'1'的概率占1/10,而第二段区间中,Y位上出现'1'的概率为0,因此可以得出结论:Y<1时,'1'在该位上出现的数量=0.1 * (X * Pow(10, Length(Z)+1));
(2) Y=1:(此时Y只能取1值了),我们可以把区间[0..n]中的n+1个数分成三段区间来分析,[000000..122999],即[000000..122999],同(1),Y位上出现'1'的概率为1/10;
[123000..123099],即[X000..X099],同(1),Y为上出现'1'的概率为0;
[123100..123145],即[XY00..XYZ],Y上出现'1'的概念为1,此区间工(Z+1)个数字
因此可以得出结论:Y=1时,'1'在该位上出现的数量=0.1 * (X * Pow(10, Length(Z)+1)) + (Z + 1);
(3) Y>1:(此时Y只能取2..9中的任意一个值了),我们可以把区间[0..n]中的n+1个数分成四段区间来分析,
[000000..122999],即[000000..(X-1)999],同(1),Y位上出现'1'的概率为1/10;
[123000..123099],即[X000..X099],同(1),Y位上出现'1'的概率为0;
[123100..123199],即[X100..X199],Y上出现'1'的概念为1,此区间共Pow(10, Length(Z))个数字;
[123200..123Y45],即[X200..XYZ],此时Y恒大于0,Y位上出现'1'的概率为0;
因此可以得出结论:Y>1时,'1'在该位上出现的数量=0.1 * (X * Pow(10, Length(Z)+1)) + Pow(10, Length(Z));
通过对上面3种情况的分析,我们可以看出,对于任意给定的整数n,我们都可以拆分成XYZ形式,并计算出Y位上'1'出现的数量;将Y从最高向最低位(个位)移动,可以计算出每位上'1'出现的数量,累计求和即为原题中要求的结果。Code如下:
2private static Int64 Calc(Int64 number, char c)
3{
4 if (!Char.IsDigit(c) || c == '0')
5 throw new Exception("暂只支持统计1..9出现的次数");
6
7 String s = number.ToString();
8 Int32 digit = (Int32)c - 48;//将字符转换成数字(0对应的ASCII码值为48)
9 Int64 totalCount = 0;
10
11 for (Int32 i = 0; i < s.Length; i++)//忽略掉刚加上的前缀0
12 {
13 Int32 num = (Int32)s[i] - 48;
14 switch (num.CompareTo(digit))
15 {
16 case -1://num<digit
17 String preStr = s.Substring(0, i);
18 totalCount += (Int64)(0.1
19 * Math.Pow(10, s.Length - i)
20 * (String.IsNullOrEmpty(preStr) ? 0 : Convert.ToInt64(preStr)));
21 break;
22 case 0: //num=digit
23 String mantissa = s.Substring(i + 1);
24 totalCount += 1 + (String.IsNullOrEmpty(mantissa) ? 0 : Convert.ToInt64(mantissa));
25 goto case -1;
26 case 1: //num>digit
27 totalCount += (Int64)Math.Pow(10, s.Length-i-1);
28 goto case -1;
29 }
30 }
31 return totalCount;
32}
33//调用
34Int64 count = Calc(number,'1');
直接从数字n上就可以得出结果,时间复杂度O(1)。
离开了人脑,“电脑”只是一坨废铁,别把啥东西都扔给它干,让废铁干它该干的活儿吧...
延伸思考:
1. 给出一个正整数n,求解1到n这些数对应的8进制中1出现的次数;
2. 给出一个正整数n,求解1到n这些数对应的16进制中A出现的次数;