本题是一道某研究院的题目,看似简单,但想要求出高效算法,是也有一定难度的
给定一个十进制正整数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。
分析与解法
【问题1的解法一】
看上去并不复杂,一个最简单的方法就是遍历从1到N,把每一个数中含有“1”的个数加起来就是最终结果。参考如下代码(JAVA)
private int count(int n) { int total = 0; while (n != 0) { total += (n % 10 == 1) ? 1 : 0; n /= 10; } return total; } public int f(int n) { int total = 0; for (int i = 1; i <= n; i++) { total += count(i); } return total; }
此方法虽简单易懂,但却非常低效,其时间复杂度为O(N*log2N),随着N的增大而线性增长,如下为我机器上的测试结果(本机为win10 64bit)
long start = System.nanoTime(); System.out.println("在N=100000000的时,其中1的个数为:" + t.f(100000000)); long end = System.nanoTime(); System.out.println("运行时间(毫秒):" + (double) (end - start) / 1000000); // 输出结果 // 在N=100000000的时,其中1的个数为:80000001 // 运行时间(毫秒):2579.713748
【问题1的解法二】
先从简单情况开始观察,总结规律。
先看1位数的情况。
如果N=3,即从1到3所有数字1,2,3, 只有个位数字上可能出现1,且只会出现1 次,
结论:如果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. 再考虑23的情况,它和N=13有点不同,十位出现1的次数为10次,从10到19,个位出现1的次数为1,11,21,所以f(N)=3+10=13,
通过分析不难看出,个位数出现1的次数不仅与个位数相关,还与十位数相关。如果N的个位数=0,则个位出现1的次数=十位数的数字,如果个位>=1,则个位数出现1的次数=十位数的数字+1。 十位数上出现1的次数不仅与十位数有关,还与个位数有关:如果十位数字=1,则十位数上出现1的次数=个位数的数字+1;如果十位数>1,则十位数上出现1的次数为10。得出如下结论
f(13)=2+4=6
f(23)=3+10=13
f(33)=4+10=14
..
f(93)=10+10=20
再看3位数情况。
如果N=123, 个位数出现1的个数为13。 十位出现1的个数为20。百位出现1的个数为24
f(123) = 个位数出现1的个数 + 十位数出现1的个数 + 百位数出现1的个数=13+20+24=54
同理可分析4位数,5位数, 经过推导得出一般情况下, 从N到f(N)的计算方法为:
假设N=abcde, 这里a, b, c, d, e分别是十进制N的各个数位上的数字。计算百位上出现1的次数将受3个因素影响:百位上的数字, 百位以下的数字,百位以上的数字。
如果百位以上的数字为0,则百位上出现1的次数由更高位决定,比如12013,则百位出现1的情况可能是100-199, 1100-1199, 2100-2199,。。。,11100-11199,共1200个。也就是说由更高位(12)决定,并且等于更高位12X当前位数100,
如果百位上数字为1,则百位上出现1的次数不仅受更高位影响,还受低位影响,也就是由高位和低位共同决定 。如12113, 百位出现1的情况是100-199,1100-1199,2100-2199,。。。,11100-11199,共12000个,和上面一样, 等于高位数字12X当前位数100, 但它还受低位影响,百位出现1的情况是12100-12113, 共124个,等于位数113+1;
如果百位上数字大于1,则百位上出现1的次数也是由高位决定,如12213,出现1的可能性为100-199,1100-1199, 2100-2199, 。。。,11100-11199, 12100-12199,共1300个,并且等于更高位数字+1(12+1)X当前位数(100)。
通过归纳总结,我们可以写出更高效的算法。如下所示(JAVA)
public int sum1(int n) { int iCount = 0; int iFactor = 1; int iLowerNum = 0; int iCurrNum = 0; int iHigherNum = 0; while (n / iFactor != 0) { iLowerNum = n - (n / iFactor) * iFactor; iCurrNum = (n / iFactor) % 10; iHigherNum = n / (iFactor * 10); switch (iCurrNum) { case 0: iCount += iHigherNum * iFactor; break; case 1: iCount += iHigherNum * iFactor + iLowerNum + 1; break; default: iCount += (iHigherNum + 1) * iFactor; break; } iFactor *= 10; } return iCount; }
此方法只要分析N就可以得到f(N), 避开了1到N的遍历,时间复杂度为O(len), 即O(ln(n)/ln(10) + 1). 在本地环境(win10 + 64bit) 测试结果如下, 可见速度提升了几个数量级。。。long start = System.nanoTime(); System.out.println("在N=100000000的时,其中1的个数为:" + t.sum1(100000000)); long end = System.nanoTime(); System.out.println("运行时间(毫秒):" + (double) (end - start) / 1000000); // 输出结果 // 在N=100000000的时,其中1的个数为:80000001 // 运行时间(毫秒):0.272