题目描述
输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
一道看上去很简单的题目,我的暴力解法毫无意外的超时了,没法子,只能看题解了,好家伙,看了半个小时才弄懂。
数学规律题目真心是最难的:没看过肯定写不来,看过了也不一定记得住规律。。。
一、找规律
规律1:
在1~10中,他们的个位数上出现1的次数为1;
在11~100中,他们十位数出现1的个数为10;
在101~1000中,他们的百位数出现1的个数为100;
。。。
在100***1【共n位】~100***00【共n + 1】,他们的第n位上出现1的个数为10^(n-1)
假设所求整数为23014:
从1~23010中,共包含2301个10,从23011~23014各位数上含1的有1个,故按照规律1,个位数上含义的有 2301 + 1 = 2302;
从1 ~ 23000中,含有230个100, 从23001 ~ 23014中,含有一个十位数的1的个数有4个【即个位数的数目】,故十位数上1的个数为10 * 230 + 4 = 231【之所以含有,是因为十位数大于等于1】
从1 ~ 23000中,含有23个1000,从23001 ~ 23014中,百位数不含1,故百位数上含一的个数为: 100 * 23 = 2300
从1 ~ 20000中,含有2个10000,从20001 ~ 23014中,千位数上含一的个数有1000个,故
千位数含一的个数有 2 * 1000 + 1000 = 30000
万位数含一的从10000~19999共10000个,
综上,每个数位上的和即为23014中含有1的数字总和。
规律2:
对于数字n:
从右向左数的第i位数字上含有1的个数为:
假设数字n的各数位从左到右排列为n_z, n_y. …n_i, n_(i-1)…,n_2,n_1,设n_i = x,
n_z,n_y…n_(i + 1)组成的数字为high,n_(i - 1)…n_2, n_1组成的数字为low:
- 若x == 0,则第i位不必参与第i位的1的计数,i位所含1的个数只与高位有关:
如上面的n_3 == 0,则只需要将高位*100即可,可推出一般规律:
x == 0, 第i位上1的总数目为high * 10^(i - 1)
- 若x == 1,则第i位的1的计数不仅与高位有关,还与低位的数字有关,低位出现了多少次1xxx,就有多少个1【还要加上xxx全为0的情况】
不失一般性,x==1时,第i位上1的个数为high * 10^(i - 1) + low + 1
- 若x > 1,则低位所含1的个数是确定的,可以理解为这个数位上可以完整的取一次,
如上面的千位为3,则从1000 ~ 1999这1000个数都能取一次,加上前面所含的2个10000,共有 (2 + 1) * 1000 = 3000个千位1.
规律:若x > 1,则i位所含1的个数为(high + 1) * 10^(i - 1)
二、代码
设当前为cur,cur的前面几个数字组成high,后几个数字组成low,当前数位对应的基数设为digit【如百位设置为100】
初始化 : cur = n % 10, high = n / 10, low = 0,digit = 1
迭代:
- cur = high % 10; //下个cur为high的最低位
- high = high / 10; //下个high为high的前m - 1位
- low = low + cur * digit //low为当前数位后面的余数
- digit = digit * 10
循环跳出条件:
high==0 && cur == 0 ,表示指针已经移到数位的左外。
复杂度分析:
时复:循环内部都是常数阶,循环外部与digit有关,即O(log10(n))
空复:几个变量,原地工作。
public int countDigitOne(int n) {
int digit = 1, high = n / 10, cur = n % 10, low = 0;
int count = 0;
while (high != 0 || cur != 0) {
if (cur == 0) count += high * digit;
else if (cur == 1) count += high * digit + low + 1;
else count += (high + 1) * digit;
low += cur * digit;
cur = high % 10;
high /= 10;
digit *= 10;
}
return count;
}