【读书笔记】《编程之美》

【读书笔记】《编程之美》

1的数目

题目:给定一个十进制的正整数N,写下从1开始,到N的所有整数,然后数一下其中出现的所有“1”的个数

这一道题有两种解法。

解法1

暴力解法

分析

题目中给了N,那么需要计算的区间范围就是[1,N],我们可以进行遍历这个区间,然后处理区间的每一个数字,并记录下1出现的次数。

代码

 public static long count(long n){
        long cnt = 0;//记录1出现的次数
        for (int i=1;i<=n;i++){//遍历区间[1,n]
            int j = i;
            while (j!=0){//循环遍历j的每一位数
                if (j%10==1){//如果当前位是1,cnt++;
                    ++cnt;
                }
                j/=10;//进入下一位判断
            }
        }
        return cnt;
    }

在博主的电脑上当前n=500000000,计算出结果需要6871ms。

解法2

数学找规律求解

无疑,一个数字是由个位,十位,百位......构成,

1,2,3,4,5,6,7,8,9,10,11,12,13......,n

先数上面个位数的1,然后数十位的1,然后数百位的1,然后数......

答案就出来了。

所以是否可以使用 个位上1出现的次数 加上 十位上出现1的个数 加上 百位上出现1的个数 .......

那么我们就看看能否求出个位,十位,百位......上的1的个数吧

可以求出来,那么自然这就是一种解法。

个位数1的出现次数

切记下面只统计个位的1,其他位不统计。

数字有一位

  • 假设当前n为4

    那么出现1的次数有1次(1),

数字有两位

  • 假设当前n为14

    那么出现1的次数有2(1,11,)

  • 如果n为24呢?

    出现的次数为3(1,11,21),易发现1+2(十位上的数)=3,

    因为14,24都是在遍历十位上的数

  • 如果当前n为20呢?

    出现次数为2(1,11),没有21了,所以我们需要判断个位数是否大于等于1。

  • 如果数字有两位,n=ba

    情况 结果
    个位数等于0 1 + b - 1 = b
    个位数大于等于1 1 + b

数字有三位

  • 假设当前n为114

    那么出现1的次数12(1,11,21,...91,101,111),

    其中(11,21,...,91,101,111)可以发现百位十位是从1-11,

  • 如果n为124呢?

    那么相对于上面多了一个121,也就是13次,

    其中(11,21,...,91,101,111,121)可以发现百位十位是从1-12。然后再加上1即可。

    可以发现出现的次数为个位数左边的数+1,即次数=n/10+1;

    我们可以发现这个公式在数字有两位时,且个位数不等于0的时候同样适用。

    可以发现一位数的时候同样适用。(题目:N(n)的范围为正整数,n即N)

  • 如果n为130呢?

    那么出现13次,和上面n为124结果一样,,即次数=n/10,

    我们发现,这个公式在数字有两位时,且个位数为0的时候同样适用

个位通用公式

那么数字位数<=3时,个位数出现1的次数,N(n)属于正整数。

假设n=cba,位不存在用0表示。

情况 结果
当个位数等于0 cba/10=cb
当个位数大于等于1 1 + cba/10=cb+1

我们发现者个公式兼容了一位,两位的情况,

假设当前n为4位数?发现表格中的公式仍然适用。

所以求个位出现1的次数,无论有几位数,都可以使用这个公式。

十位数出现1的次数

切记:下面只统计十位数的1,其他位数不统计。

数字有一位

  • 假设当前n为4

    那么出现1的次数有0次

数字有两位

  • 假设当前n为14

    那么出现1的次数有5(10,11,12,13,14),次数=个位数+1

    • 如果n为24呢?

      出现的次数为10(10,11,12,....,19)

    • 如果当前n为35呢?

      发现出现的情况和24一样,我们发现n如果为99,出现的情况和上面也一样,10-19已经是所有百位的1了。

      易发现一下规律

      如果n=ba

    情况 结果
    十位数等于0 即 b=0 0
    十位数等于1 即 b=1 个位数 + 1= a + 1
    十位数大于1 即 b>1 10

数字有三位

  • 假设当前n为114

    那么出现1的次数15(1-99,100,101,102,..,109,110,111,112,113,114),

    1-99:按照数字有两位求,结果为10

    110,...114:结果位5

    故总次数为15,

    • 如果n为124呢?

      出现20次,(10-19,110-119)

    • 如果n为130呢?

      那么出现还是20次,和上面n为124结果一样,可以发现1都是出现在10-19(110-119)这个区间中,

      10-19这个区间出现的次数即为答案。

      每100中都会出现一次,10-19区间中十位出现1的次数为10次。

      114=100+14

      假设n=abc

      [1,abc]=[1,a*100]+[a*100+1,abc]

      [1,a*100]:那么其中有a个100,1的次数为10*a。

      [a*100+1,abc]:中十位出现1的次数与十位是否为0,1有关,结合数字有两位的公式

      若b=0,次数为0,

      若b=1,次数为c+1

      若b>1,次数为10

      可以发现十位出现的次数与a,b,c的取值都有关

十位通用公式

那么数字位数为三位时,十位数出现1的次数,N(n)属于正整数。
n=cba

情况 结果
b=0 10*c
b=1 10*c+a+1
b>1 10*(c+1)

我们发现数字为一位,两位情况被数字为三位的情况所兼容。

当n为四位数时,仍然成立.

所以求十位出现1的次数,无论有几位数,都可以使用这个公式。

百位出现1的次数

切记:下面只统计百位的1,其他位不统计。

数字有一位

次数为0

数字有两位

次数为0

数字有三位

  • 假设当前n为123

    那么出现1的次数为24(100,101,...,123),23+1

  • 假设当前n为159

    那么出现1的次数为60次,59+1

    我们易发现出现次数与百位以下(更低位)有关,低位+1为结果

数字有四位

  • 假设当前n为1123

    那么出现1的次数为124(100-199,1100-1123)

    即每次1000中都会有100个,既和更高位有关,也和更低位有关

百位通用公式

n=dcba

情况 结果
c=0 d*100
c=1 d*100+ba+1
c>1 (d+1)*100

我们发现公式也兼容三位的情况。

当n为五位数时,仍然成立.

所以求百位出现1的次数,无论有几位数,都可以使用这个公式。

综合

我把上面三个通用公式表格都拿下来进行分析,且n=dcba,位不存在使用0表示

个位:(例:dcb不是相乘关系,不存在使用0表示即可)

情况 结果
当个位数等于0 dcba/10=dcb
当个位数大于等于1 1 + dcba/10=dcb+1

十位:

情况 结果
b=0 10*dc [10*更高位]
b=1 10*dc+a+1 [10*更高位+更低位+1]
b>1 10*(dc+1) [10*(更高位+1)]

百位:

情况 结果
c=0 100*d [100*更高位]
c=1 100*d+ba+1 [100*更高位+更低位+1]
c>1 100*(d+1) [100*(更高位+1)]
  • 我们发现百位乘100,十位乘10,个位乘1(略),
  • 我们发现十位与百位的情况如此相似

我们将个位重新等价变形一下,

个位:

情况 结果
a=0 dcba/10=dcb [1*更高位]
a=1 1 + dcba/10=dcb+1 [1*更高位+更低位+1]
a>1 1 + dcba/10=dcb+1 [1*(更高位+1)]

发现个位,十位,百位都是同样的公式,该公式具有普遍性,千位也是......

按照公式可以写出如下代码。

代码

综上可以发现:求当前位出现1的次数,只需要知道当前是第几位,更高位,更低位即可求出当前位出现1的次数。

private static long count(long n) {
        long count = 0;   //出现1的次数
        long factor = 1;   //标记当前是个位还是十位还是百位等,就是表格中的1,10,100数字
        long lowernum = 0;  //更低位
        long currnum = 0;   //当前位
        long hightnum = 0;  //更高位
        //循环遍历求出每一位
        while (n / factor != 0) {
            lowernum = n - (n / factor) * factor;//求出更低位
            currnum = (n / factor) % 10;//求出当前位
            hightnum = n / (factor * 10);   //求出更高位
            switch ((int) currnum) {    //判断当前位
                case 0://当前位为0,出现次数,当前[个,百,千....]位*更高位
                    count += hightnum * factor;
                    break;
                case 1://当前位为1,出现次数,当前[个,百,千....]位*更高位+更低位+1
                    count += hightnum * factor + lowernum + 1;
                    break;
                default://当前位大于1,出现次数,当前[个,百,千....]位*(更高位+1)
                    count += (hightnum + 1) * factor;
                    break;
            }
            //处理下一位
            factor *= 10;
        }
        return count;
    }

在博主的电脑上当前n=500000000,计算出结果需要0.2911ms。

相比较两个程序的执行时间,执行速度提高了30000倍左右。

posted @ 2022-08-22 19:17  TanAn  阅读(54)  评论(0)    收藏  举报