题目描述(困难难度)
给一个数 n
,输出 0 ~ n
中的数字中 1
出现的个数。
解法一 暴力
直接想到的当然就是暴力的方法,一个数一个数的判断,一位一位的判断。
public int countDigitOne(int n) {
int num = 0;
for (int i = 1; i <= n; i++) {
int temp = i;
while (temp > 0) {
if (temp % 10 == 1) {
num++;
}
temp /= 10;
}
}
return num;
}
但这个解法会超时。
解法二
自己也没想到别的方法,讲一下 这里 的思路。
总体思想就是分类,先求所有数中个位是 1
的个数,再求十位是 1
的个数,再求百位是 1
的个数...
假设 n = xyzdabc
,此时我们求千位是 1
的个数,也就是 d
所在的位置。
那么此时有三种情况,
d == 0
,那么千位上1
的个数就是xyz * 1000
d == 1
,那么千位上1
的个数就是xyz * 1000 + abc + 1
d > 1
,那么千位上1
的个数就是xyz * 1000 + 1000
为什么呢?
当我们考虑千位是 1
的时候,我们将千位定为 1
,也就是 xyz1abc
。
对于 xyz
的话,可以取 0,1,2...(xyz-1)
,也就是 xyz
种可能。
当 xyz
固定为上边其中的一个数的时候,abc
可以取 0,1,2...999
,也就是 1000
种可能。
这样的话,总共就是 xyz*1000
种可能。
注意到,我们前三位只取到了 xyz-1
,那么如果取 xyz
呢?
此时就出现了上边的三种情况,取决于 d
的值。
d == 1
的时候,千位刚好是 1
,此时 abc
可以取的值就是 0
到 abc
,所以多加了 abc + 1
。
d > 1
的时候,d
如果取 1
,那么 abc
就可以取 0
到 999
,此时就多加了 1000
。
再看一个具体的例子。
如果n = 4560234
让我们统计一下千位有多少个 1
xyz 可以取 0 到 455, abc 可以取 0 到 999
4551000 to 4551999 (1000)
4541000 to 4541999 (1000)
4531000 to 4531999 (1000)
...
21000 to 21999 (1000)
11000 to 11999 (1000)
1000 to 1999 (1000)
总共就是 456 * 1000
如果 n = 4561234
xyz 可以取 0 到 455, abc 可以取 0 到 999
4551000 to 4551999 (1000)
4541000 to 4541999 (1000)
4531000 to 4531999 (1000)
...
1000 to 1999 (1000)
xyz 还可以取 456, abc 可以取 0 到 234
4561000 to 4561234 (234 + 1)
总共就是 456 * 1000 + 234 + 1
如果 n = 4563234
xyz 可以取 0 到 455, abc 可以取 0 到 999
4551000 to 4551999 (1000)
4541000 to 4541999 (1000)
4531000 to 4531999 (1000)
...
1000 to 1999 (1000)
xyz 还可以取 456, abc 可以取 0 到 999
4561000 to 4561999 (1000)
总共就是 456 * 1000 + 1000
至于其它位的话是一样的道理。
代码的话就很好写了。
public int countDigitOne(int n) {
int count = 0;
//依次考虑个位、十位、百位...是 1
//k = 1000, 对应于上边举的例子
for (int k = 1; k <= n; k *= 10) {
// xyzdabc
int abc = n % k;
int xyzd = n / k;
int d = xyzd % 10;
int xyz = xyzd / 10;
count += xyz * k;
if (d > 1) {
count += k;
}
if (d == 1) {
count += abc + 1;
}
//如果不加这句的话,虽然 k 一直乘以 10,但由于溢出的问题
//k 本来要大于 n 的时候,却小于了 n 会再次进入循环
//此时代表最高位是 1 的情况也考虑完成了
if(xyz == 0){
break;
}
}
return count;
}
然后代码的话,可以再简化一下,注意到 d > 1
的时候,我们多加了一个 k
。
我们可以通过计算 long xyz = xyzd / 10;
的时候,给 xyzd
多加 8
,从而使得当 d > 1
的时候,此时求出来的 xyz
会比之前大 1
,这样计算 xyz * k
的时候就相当于多加了一个 k
。
此外,上边 k
溢出的问题,我们可以通过 long
类型解决。
public int countDigitOne(int n) {
int count = 0;
for (long k = 1; k <= n; k *= 10) {
// xyzdabc
int abc = n % k;
int xyzd = n / k;
int d = xyzd % 10;
int xyz = (xyzd + 8) / 10;
count += xyz * k;
if (d == 1) {
count += abc + 1;
}
}
return count;
}
而这个代码,其实和 Solution 高赞-C%2B%2BJavaPython) 中的解法是一样的,只不过省去了 xyz
和 d
这两个变量。
public int countDigitOne(int n) {
int count = 0;
for (long k = 1; k <= n; k *= 10) {
long r = n / k, m = n % k;
// sum up the count of ones on every place k
count += (r + 8) / 10 * k + (r % 10 == 1 ? m + 1 : 0);
}
return count;
}
总
这道题的话,就是数学的分类、找规律的题目了,和 172 题 找阶乘中 0
的个数一样,没有一些通用的算法,完全靠分析能力,如果面试碰到很容易卡主。