题目:输入一个整数n,求从1到n这n个整数的十进制表示中1出现的次数。例如输入12,从1到12这些整数中包含1的数字有1,10,11和12,1一共出现了5次。
这道题目开始是在《编程之美》这本书上面看到的,当时对于它的解析有点不理解,后来在参加大众点评在线笔试的时候正好遇到了这个题目,还好在有限的时间内将解题的时间复杂度达到了最优。最近又在《剑指offer》上面再次遇到这个题目,这里小小的总结一下。
解法一:这个题目看上去并不困难,最直观的解法就是从1开始遍历到n,将其中含有‘1’的个数加起来,自然就得到了从1到n所有‘1’的个数的和。实现代码如下:
unsigned long count(unsigned long n)
{
long num = 0;
while(n != 0)
{
num += (n%10 == 1)? 1:0;
n /= 10;
}
return num;
}
unsigned long f(unsigned long n)
{
unsigned long count = 0;
for(unsigned long i = 1; i < n; i++)
{
count += count(i);
}
return count;
}
这个方法实现起来很简单,但是我们对每个数字都要做除法和求余运算以求出该数字中1出现的次数,时间复杂度为O(n*lg n),当n非常大的时候,计算效率很低。所以要提高效率,我们必须摒弃这种遍历的方法,采用另外的思路。
解法二:我们首先分析1位数的情况,n = 3时,f(n)=1;n=0时,f(n)=0; 再看2位数的情况,如果n=13,个位上出现1的次数有2次,1和11,十位上出现1的个数有4次,10,11,12和13,所以f(n)=2+4=6;这里需要注意的是11这个数在十位和个位都出现了1,但是我们对11恰好计算了2次,所以11并不需要特殊处理。再考虑n = 23时的情况,它和n=13有点不同,十位上出现1的次数为10次,从10到19.个位出现1的次数为3次,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(99) = 10 + 10 = 20;
接下来我们分析百位数,千位数,万位数。。。。很快我们能发现一些规律,建议自己去观察总结会更有效果。下面我们以n=abcde为例,a,b,c,d,e分别代表十进制数n的各个数位上的数字。如果要计算百位上出现1的个数。分析:如果百位上面为0,则百位上面可能出现1的次数由更高位决定,比如n=12013,百位出现1的的情况可能有100~199,1100~1199,2100~2199...........11100~11199,一共12*100=1200个。如果百位上面的数字为1,可知百位上面出现1的次数就不仅仅收到高位的影响,还收到低位的影响,比如n = 12113,出现1的情况有前面列举过的1200种,还要加上12100~12113这13+1=14种。如果百位上数字大于1,则百位上可能出现1的次数仅由高位决定,同样除了前面列举的1200种,还要加上12100~12199这100种,一共为(12+1)*100=1300个。通过对百位的归纳和总结,我们对这个规律已经有了很清晰的了解,下面是实现代码:
long sum(unsigned long n)
{
unsigned long count = 0;
unsigned long factor = 1;
unsigned long lowerNum= 0;
unsigned long currNum= 0;
unsigned long higherNum= 0;
while(n /factor != 0)
{
lowerNum = n - (n / factor)*factor;
currNum = (n / factor) % 10;
higherNum = n /(factor * 10);
switch(currNum)
{
case 0:
count += higherNum * factor;
break;
case 1:
count += higherNum * factor + lowerNum + 1;
break;
default:
count += (higherNum + 1) * factor;
break;
}
factor *=10;
}
return count;
}
这个方法只要分析n就能得到f(n),避开了从1到n的遍历,一个数字n有lg(n)位,所以这种方法的时间复杂度为O(lg n)。
在linux下编辑运行结果如下:
上面的解法是《编程之美》上面采取的思路,从低位开始往高位遍历,《剑指offer》上面采用的是从高位往低位遍历,有异曲同工之效,时间复杂度同样是O(lg n)。