happyhippy

这个世界的问题在于聪明人充满疑惑,而傻子们坚信不疑。--罗素

 

    好久没更新博客了,今天也来客博一下。
    前两天在首页看到题目:给出一个数n,O(1)求解1到n这些数中1出现的次数。

 

    看到这个题目,联想到另一个题目:给出一个数n,求1到n这些数之和.哇靠,小学算术题,这不是侮辱咱的智商嘛~计算机最擅长干重复的劳动了,于是乎,潇洒地抖出了下面code,把循环交给计算机去做:

1Int32 sum = 0;
2Int32 number = 100;
3for (Int32 i = 1; i <= number; i++)
4    sum += i;


    OK,结果是正确的,思路相当地清晰,完了吗?没完!为了显示洒家精通C#,咱把循环改造下:

1 for (Int32 i = 1; i <= number; sum += i++) ;


    结果依然是正确的,完了吗?完了!不过是完蛋的“完”.难道我们一定要让自己的代码使别人费解,来显示我们多牛X吗!难道我们一定要让计算机拼命地干傻活儿,来证明现在处理器的性能多NB吗!为什么我们不把能做的做好,剩下的...让那坨废铁干它该干的活儿:

1 sum = number * (1 + number) / 2;

 

 

    扯远了,拉回来说“正”题:给出一个数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如下:

 1//计算[1..number]中,共有多个字符c
 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出现的次数;

 

posted on 2008-10-19 23:02  Silent Void  阅读(2277)  评论(4编辑  收藏  举报