题目描述(困难难度)

给一个数 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 可以取的值就是 0abc ,所以多加了 abc + 1

d > 1 的时候,d 如果取 1,那么 abc 就可以取 0999,此时就多加了 1000

再看一个具体的例子。

如果n = 4560234
让我们统计一下千位有多少个 1
xyz 可以取 0455, abc 可以取 0999
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 可以取 0455, abc 可以取 0999
4551000 to 4551999 (1000)
4541000 to 4541999 (1000)
4531000 to 4531999 (1000)
...
1000 to 1999 (1000)
xyz 还可以取 456, abc 可以取 0234
4561000 to 4561234 (234 + 1)
总共就是 456 * 1000 + 234 + 1

如果 n = 4563234
xyz 可以取 0455, abc 可以取 0999    
4551000 to 4551999 (1000)
4541000 to 4541999 (1000)
4531000 to 4531999 (1000)
...
1000 to 1999 (1000)
xyz 还可以取 456, abc 可以取 0999
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) 中的解法是一样的,只不过省去了 xyzd 这两个变量。

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 的个数一样,没有一些通用的算法,完全靠分析能力,如果面试碰到很容易卡主。

results matching ""

    No results matching ""