第2章 数字之魅——1的数目

1的数目

问题描述

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

例如:

  N= 2,写下1,2。这样只出现了1个"1"。

  N= 12,我们会写下1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12。这样,1的个数是5。

问题是:

  1. 写一个函数f(N),返回1到N之间出现的"1"的个数,比如f(12)=5。

  2. 在32位整数范围内,满足条件"f(N)= N"的最大的N是多少?

分析与解法

【问题1的解法一】

这个问题看上去并不是一个困难的问题,因为不需要太多的思考,我想大家都能找到一个最简单的方法来计算f(N),那就是从1开始遍历到N,将其中每一个数中含有"1"的个数加起来,自然就得到了从1到N所有"1"的个数的和。写成程序如下:

 1 package chapter2shuzizhimei.countof1;
 2 /**
 3  * 1的数目
 4  * 【问题1的解法一】
 5  * @author DELL
 6  *
 7  */
 8 public class CountOfone {
 9     //计算一个整数中1的个数
10     public static long count1InAInt(int n){
11         long count = 0;
12         while(n!=0){
13             if(n%10==1){
14                 count++;
15             }
16             n /= 10;
17         }
18         return count;
19     }
20     //统计从1到N所有整数中1的个数
21     public static long f(int N){
22         long countAll = 0;
23         for(int i=1;i<=N;i++){
24             countAll += count1InAInt(i);
25         }
26         return countAll;
27     }
28     public static void main(String[] args) {
29         int n = 12;
30         System.out.println("从1到"+n+"中所有1的个数为:"+f(n));
31
32     }
33
34 }

程序运行结果如下:

从1到12中所有1的个数为:5

  这个方法很简单,只要学过一点编程知识的人都能想到,实现也很简单,容易理解。但是这个算法的致命问题是效率,它的时间复杂度是O(N)×计算一个整数数字里面"1"的个数的复杂度 = O(N * log2 N)

  如果给定的N比较大,则需要很长的运算时间才能得到计算结果。比如在笔者的机器上,如果给定N=100 000 000,则算出f(N)大概需要40秒的时间,计算时间会随着N的增大而线性增长。

  看起来要计算从1到N的数字中所有1的和,至少也得遍历1到N之间所有的数字才能得到。那么能不能找到快一点的方法来解决这个问题呢?要提高效率,必须摈弃这种遍历1到N所有数字来计算f(N)的方法,而应采用另外的思路来解决这个问题。

【问题1的解法二】

  仔细分析这个问题,给定了N,似乎就可以通过分析"小于N的数在每一位上可能出现1的次数"之和来得到这个结果。让我们来分析一下对于一个特定的N,如何得到一个规律来分析在每一位上所有出现1的可能性,并求和得到最后的f(N)。

  先从一些简单的情况开始观察,看看能不能总结出什么规律。

先看1位数的情况。

  如果N = 3,那么从1到3的所有数字:1、2、3,只有个位数字上可能出现1,而且只出现1次,进一步可以发现如果N是个位数,如果N>=1,那么f(N)都等于1,如果N=0,则f(N)为0。

再看2位数的情况。

  如果N=13,那么从1到13的所有数字:1、2、3、4、5、6、7、8、9、10、11、12、13,个位和十位的数字上都可能有1,我们可以将它们分开来考虑,个位出现1的次数有两次:1和11,十位出现1的次数有4次:10、11、12和13,所以f(N)=2+4=6。要注意的是11这个数字在十位和个位都出现了1,但是11恰好在个位为1和十位为1中被计算了两次,所以不用特殊处理,是对的。再考虑N=23的情况,它和N=13有点不同,十位出现1的次数为10次,从10到19,个位出现1的次数为1、11和21,所以f(N)=3+10=13。通过对两位数进行分析,我们发现,个位数出现1的次数不仅和个位数字有关,还和十位数有关:如果N的个位数大于等于1,则个位出现1的次数为十位数的数字加1;如果N的个位数为0,则个位出现1的次数等于十位数的数字。而十位数上出现1的次数不仅和十位数有关,还和个位数有关:如果十位数字等于1,则十位数上出现1的次数为个位数的数字加1;如果十位数大于1,则十位数上出现1的次数为10。

f(13) = 个位出现1的个数 + 十位出现1的个数 = 2 + 4 = 6;

f(23) = 个位出现1的个数 + 十位出现1的个数 = 3 + 10 = 13;

f(33) = 个位出现1的个数 + 十位出现1的个数 = 4 + 10 = 14;

f(93) = 个位出现1的个数 + 十位出现1的个数 = 10 + 10 = 20;

接着分析3位数。

  如果N = 123:

  个位出现1的个数为13:1, 11, 21, …, 91, 101, 111, 121

  十位出现1的个数为20:10~19, 110~119

  百位出现1的个数为24:100~123

  f(23)= 个位出现1的个数 + 十位出现1的个数 + 百位出现1的次数 = 13 + 20 + 24 = 57;

  同理我们可以再分析4位数、5位数。读者朋友们可以写一写,总结一下各种情况有什么不同。

  根据上面的一些尝试,下面我们推导出一般情况下,从N得到f(N)的计算方法:

  假设N=abcde,这里a、b、c、d、e分别是十进制数N的各个数位上的数字。如果要计算百位上出现1的次数,它将会受到三个因素的影响:百位上的数字,百位以下(低位)的数字,百位(更高位)以上的数字。

  如果百位上的数字为0,则可以知道,百位上可能出现1的次数由更高位决定,比如12 013,则可以知道百位出现1的情况可能是100~199,1 100~1 199,2 100~2 199,…,11 100~11 199,一共有1 200个。也就是由更高位数字(12)决定,并且等于更高位数字(12)×当前位数(100)。

  如果百位上的数字为1,则可以知道,百位上可能出现1的次数不仅受更高位影响,还受低位影响,也就是由更高位和低位共同决定。例如对于12 113,受更高位影响,百位出现1的情况是100~199,1 100~1 199,2 100~2 199,…,11 100~11 199,一共1 200个,和上面第一种情况一样,等于更高位数字(12)×当前位数(100)。但是它还受低位影响,百位出现1的情况是12 100~12 113,一共114个,等于低位数字(113)+1。

  如果百位上数字大于1(即为2~9),则百位上可能出现1的次数也仅由更高位决定,比如12 213,则百位出现1的可能性为:100~199,1 100~1 199,2 100~2 199,…,11 100~11 199,12 100~12 199,一共有1 300个,并且等于更高位数字+1(12+1)×当前位数(100)。

  通过上面的归纳和总结,我们可以写出如下的更高效算法来计算f(N):

 1 package chapter2shuzizhimei.countof1;
 2 /**
 3  * 1的数目
 4  * 【问题1的解法二】
 5  * @author DELL
 6  *
 7  */
 8 public class CountOfone2 {
 9
10     //统计从1到N所有整数中1的个数
11     public static long f(int n){
12         long iCount = 0;  //1的个数
13         int iFactor = 1; //分位
14         int iLowerNum = 0; //当前位以下的数
15         int iCurrNum = 0;  //当前位的值
16         int iHigherNum = 0;  //当前位上面的数大小
17         while(n/iFactor!=0){
18             iLowerNum = n - (n/iFactor)*iFactor;
19             iCurrNum = (n/iFactor)%10;
20             iHigherNum = n/(iFactor*10);
21             switch(iCurrNum){
22             case 0:
23                 iCount += iHigherNum*iFactor;
24                 break;
25             case 1:
26                 iCount += iHigherNum*iFactor + iLowerNum + 1;
27                 break;
28             default:
29                 iCount += (iHigherNum + 1) * iFactor;
30                 break;
31             }
32             iFactor *= 10;
33         }
34         return iCount;
35     }
36     public static void main(String[] args) {
37         int n = 12;
38         System.out.println("从1到"+n+"中所有1的个数为:"+f(n));
39
40     }
41
42 }

程序运行结果如下:

从1到12中所有1的个数为:5

  这个方法只要分析N就可以得到f(N),避开了从1到N的遍历,输入长度为Len的数字N的时间复杂度为O(Len),即为O(ln(n)/ln(10)+1)。在笔者的计算机上,计算N=100 000 000,相对于第一种方法的40秒时间,这种算法不到1毫秒就可以返回结果,速度至少提高了40 000倍。

【问题2的解法】

要确定最大的数N,满足f(N)=N。我们通过简单的分析可以知道(仿照上面给出的方法来分析):


  因此,问题转化为如何证明上界N确实存在,并估计出这个上界N。容易从上面的式子归纳出:f(10n-1)= n * 10n-1。通过这个递推式,很容易看到,当n = 9时候,f(n)的开始值大于n,所以我们可以猜想,当n大于某一个数N时,f(n)会始终比n大,也就是说,最大满足条件在0~N之间,亦即N是最大满足条件f(n)= n的一个上界。如果能估计出这个N,那么只要让n从N往0递减,每个分别检查是否有f(n)= n,第一个满足条件的数就是我们要求的整数。

  证明满足条件f(n)= n的数存在一个上界

  首先,用类似数学归纳法的思路来推理这个问题。很容易得到下面这些结论(读者朋友可以自己试着列举验证一下):

当n增加10时,f(n)至少增加1;

当n增加100时,f(n)至少增加20;

当n增加1 000时,f(n)至少增加300;

当n增加10 000时,f(n)至少增加4 000;

……

当n增加10k时,f(n)至少增加k*10k-1。

  首先,当k>=10时,k*10 k-1> 10 k,所以f(n)的增加量大于n的增加量。

  其次,f(1010-1)=1010>1010-1。如果存在N,当n = N时,f(N)-N>1010-1成立时,此时不管n增加多少,f(n)的值将始终大于n。

  具体来说,设n的增加量为m:当m小于1010-1时,由于f(N)-N>1010-1,因此有f(N + m)> f(N)> N + 1010-1 > N + m,即f(n)的值仍然比n的值大;当m大于等于1010-1时,f (n)的增量始终比n的增量大,即f(N + m)- f(N)>(N+m)- N,也就是f(N + m)> f(N)+ m > N + 1010-1+ m > N + m,即f(n)的值仍然比n的值大。

  因此,对于满足f(N)- N > 1010-1成立的N一定是所求该数的一个上界。

  求出上界N:

又由于f(1010-1)= n *1010-1,不妨设N = 10K-1,有f(10K-1)-(10K-1)> 1010-1,即K*10K-1 -(10K-1)> 1010-1,易得K > =11时候均满足。所以,当K = 11时,N=1011-1即为最小一个上界。

计算这个最大数n

令N = 1011-1=99 999 999 999,让n从N往0递减,每个分别检查是否有f(n)= n,第一满足条件的就是我们要求的整数。很容易解出n = 1 111 111 110是满足f(n)= n的最大整数。

代码如下:

 1 package chapter2shuzizhimei.countof1;
 2 /**
 3  * 1的数目
 4  * 【问题二的解法】
 5  * @author DELL
 6  *
 7  */
 8 public class CountOfone3 {
 9
10     //统计从1到N所有整数中1的个数
11     public static long f(long n){
12         long iCount = 0;  //1的个数
13         long iFactor = 1; //分位
14         long iLowerNum = 0; //当前位以下的数
15         int iCurrNum = 0;  //当前位的值
16         long iHigherNum = 0;  //当前位上面的数大小
17         while(n/iFactor!=0){
18             iLowerNum = n - (n/iFactor)*iFactor;
19             iCurrNum = (int) ((n/iFactor)%10);
20             iHigherNum = n/(iFactor*10);
21             switch(iCurrNum){
22             case 0:
23                 iCount += iHigherNum*iFactor;
24                 break;
25             case 1:
26                 iCount += iHigherNum*iFactor + iLowerNum + 1;
27                 break;
28             default:
29                 iCount += (iHigherNum + 1) * iFactor;
30                 break;
31             }
32             iFactor *= 10;
33         }
34         return iCount;
35     }
36     public static void main(String[] args) {
37         long n = (long) (1e11-1);
38         long i;
39         for(i=n;i>0;i--){
40             if(f(i)==i)
41                 break;
42         }
43         System.out.println("在32位整数范围内,满足条件\"f(n)=n\"的最大n为:"+n);
44
45     }
46
47 }

程序运行结果如下:

在32位整数范围内,满足条件"f(n)=n"的最大n为:1111111110
时间: 2024-08-24 01:18:29

第2章 数字之魅——1的数目的相关文章

第2章 数字之魅——求二进制中1的个数

求二进制中1的个数 问题描述 对于一个字节(8bit)的变量,求其二进制表示中“1”的个数,要求算法的执行效率尽可能地高. [解法一] 可以举一个八位的二进制例子来进行分析.对于二进制操作,我们知道,除以一个2,原来的数字将会减少一个0.如果除的过程中有余,那么就表示当前位置有一个1. 以10 100 010为例: 第一次除以2时,商为1 010 001,余为0. 第二次除以2时,商为101 000,余为1. 因此,可以考虑利用整型数据除法的特点,通过相除和判断余数的值来进行分析.于是有了如下的

第2章 数字之魅——不要被阶乘吓倒

不要被阶乘吓倒 问题描述 阶乘(Factorial)是个很有意思的函数,但是不少人都比较怕它,我们来看看两个与阶乘相关的问题: 问题1. 给定一个整数N,那么N的阶乘N!末尾有多少个0呢?例如:N=10,N!=3 628 800,N!的末尾有两个0. 问题2. 求N!的二进制表示中最低位1的位置. 分析与解法 有些人碰到这样的题目会想:是不是要完整计算出N!的值?如果溢出怎么办?事实上,如果我们从"哪些数相乘能得到10"这个角度来考虑,问题就变得简单了. 首先考虑,如果N!= K×10

第2章 数字之魅——快速寻找满足条件的两个数

快速寻找满足条件的两个数 问题描述 能否快速找出一个数组中的两个数字,让这两个数字之和等于一个给定的数字,为了简化起见,我们假设这个数组中肯定存在这样一组或以上符合要求的解. 分析与解法 [解法一] 代码如下: 1 package chapter2shuzizhimei.findtwonumber; 2 /** 3 * 快速寻找满足条件的两个数 4 * [解法一] 5 * @author DELL 6 * 7 */ 8 public class FindTowNumber1 { 9 //定义一个

第2章 数字之魅——数字中的技巧

2.7最大公约数问题 问题:求两个数的最大公约数. 对于该问题:首先映入眼帘的就是两个数n m中寻找一个最小的值.然后从该值遍历到1.一旦 n%i==0&&m%i==0 那么i就是这个最大公约数啦.原理不言而喻.代码就不附上了. 之后一种就是比较经典的欧几里德算法.其中本质上的原理是这样的.gcd(n,m)表示n和m的最大公约数. 1:gcd(n,m) = gcd(n%m,m)  (n>m) 2:gcd(0,a) = a 这是合法的.因为0可以当做被除数(废话,但是为了严谨一点再次说

第2章 数字之魅——找符合条件的整数

找符合条件的整数 问题描述 任意给定一个正整数N,求一个最小的正整数M(M>1),使得N*M的十进制表示形式里只含有1和0. 解决这个问题首先考虑对于任意的N,是否这样的M一定存在.可以证明,M是一定存在的,而且不唯一.简单证明:因为 这是一个无穷数列,但是数列中的每一项取值范围都在[0, N-1]之间.所以这个无穷数列中间必定存在循环节.即假设有s,t均是正整数,且s<t,有 .于是循环节长度为t-s.于是10^s = 10^t.因此有:,所以 例如,取N=3,因为10的任何非负次方模3都为

第2章 数字之魅——子数组之和的最大值(二维)

子数组之和的最大值(二维) 问题描述 我们在前面分析了一维数组之和的最大值问题,那么如果是二维数组又该如何分析呢? 分析与解法 最直接的方法,当然就是枚举每一个矩形区域,然后再求这个矩形区域中元素的和. [解法一] 完整代码如下: 1 package chapter2shuzizhimei.maxsumsubarraytwodimensional; 2 /** 3 * 求数组的子数组之和的最大值(二维) 4 * [解法一] 5 * @author DELL 6 * 7 */ 8 public c

第2章 数字之魅——寻找数组中的最大值和最小值

寻找数组中的最大值和最小值 问题描述 对于一个由N个整数组成的数组,需要比较多少次才能把最大和最小的数找出来呢? 分析与解法 [解法一] 可以把寻找数组中的最大值和最小值看成是两个独立的问题,我们只要分别求出数组的最大值和最小值即可解决问题.最直接的做法是先扫描一遍数组,找出最大的数以及最小的数.这样,我们需要比较2*(N-1)次才能找出最大的数和最小的数.代码如下: 1 package chapter2shuzizhimei.findminmax; 2 /** 3 * 寻找数组中的最大值和最小

第2章 数字之魅——最大公约数问题

最大公约数问题 问题描述 写一个程序,求两个正整数的最大公约数.如果两个正整数都很大,有什么简单的算法吗? 分析与解法 [解法一] 最简单的实现,就是直接用代码来实现辗转相除法.从上面的描述中,我们知道,利用递归就能够很轻松地把这个问题完成. 具体代码如下: 1 package chapter2shuzizhimei.gcd; 2 /** 3 * 最大公约数问题 4 * [解法一]辗转相除法 5 * @author DELL 6 * 7 */ 8 public class GCD { 9 //求

第2章 数字之魅——寻找最近点对

寻找最近点对 问题描述 给定平面上N个点的坐标,找出距离最近的两个点. 分析与解法 我们不妨先看一看一维的情况:在一个包含N个数的数组中,如何快速找出N个数中两两差值的最小值?一维的情况相当于所有的点都在一条直线上.虽然是一个退化的情况,但还是能从中得到一些启发. [解法一] 数组中共包含N个数,我们把它们两两之间的差值都求出来,那样就不难得出最小的差值了.这样一个直接的想法,时间复杂度为O(N2).具体代码如下: 1 package chapter2shuzizhimei.findminpoi