leetcode题库☞ 233. 数字 1 的个数

在这里插入图片描述
贴一段比较好的方式:代码越简单,思维越复杂……可以自己画个函数图

class Solution {
    public int countDigitOne(int n) {
        int sum = 0;
        for (int i = 1; i <= n; i *= 10) {
            /*
             n/i*10  n%(i*10)=x
             x < i  =  0
             x < 2i  =  x-i+1
             x >= 2i  =  i
             */
            sum += n / (i * 10) * i + Math.min(Math.max(0, (n % (i * 10) - i + 1)), i);
        }
        return sum;
    }
}

下面这段解释比较易懂,官方的不太好理解,官方传送门见文末

方法一、暴力(超时)

我们考虑使用暴力法来解,只需要枚举每一个比n小的数,然后算他们的含有1的数量,相加即可,代码比较简单:

class Solution {
    public int countDigitOne(int n) {
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            int count = 0;
            int num = i;
            while (num != 0) {
                // 从个位数开始一位一位的和1比较
                if (num % 10 == 1) {
                    count++;
                }
                num /= 10;
            }
            ans += count;
        }
        return ans;
    }
}

但是,当用例非常大的时候,比如,n=824883294,会超时,过不了所有用例,放弃!

方法二、动态规划(超内存)

第二种方法,我们考虑使用动态规划来解,比如,要求123这个数含有的1,其实只要看12和3含有的1相加就可以了,用DP方程表示为:dp[i]=dp[i/10]+dp[i%10],代码也很简单,如下:

class Solution {
    public int countDigitOne(int n) {
        int ans = 0;
        int[] dp = new int[n + 1];
        dp[1] = 1;
        for (int i = 1; i <= n; i++) {
            dp[i] = dp[i % 10] + dp[i / 10];
            ans += dp[i];
        }
        return ans;
    }
}

但是,当用例非常大,比如,n=824883294,申请n+1的dp数组,直接把内存干爆了。。

方法三:找规律

经过上面两种血的教训,考虑我们肯定不能一个数一个数的去遍历,去找他们含有1的数量,那么,能不能一批一批地去找呢?

比如,把所有的数一起来考虑,先找他们个位可能出现1的数量,再找十位可能出现1的数量?

我们以n=2021为例,所有小于等于 2021 的数中个位一共会出现多少个 1 呢?

我们可以很容易地发现,个位数出现1的频率是每10个数出现一次,对不对?

所以,个位数出现多少 1 就取决于,一个有多少个 10,比如 2021 一共用 202 个 10,所以,个位出现 1 的数一共有 202 次(1, 11, 21,2011)+ 1次(2021)。

为什么最后一个 1 次要单独拿出来计算呢?

因为这个 1 次是比较特殊的,如果把 n 换成 2020 ,这样最后的 1 次是没有的,你要仔细考虑一下。

只有 n 的个位数大于等于 1 的时候,才需要计算最后的这个 1 次。

同理,我们考虑十位数一个有多少个 1。

很简单,每 100 个数会出现 10 个十位数为 1 的数字,同样地,如果 n 的后面两位小于 10,则不用额外加次数,如果后两位大于等于 10,则需要额外加次数。

比如,n=2021时,最后要加 10 次,n=2009时,最后不要加 10次,而n=2015时,最后要加 15-10+1=6次,这一块,你仔细体会一下。

同样地道理,可以推断出千位数出现多少个 1,就很简单了,用公式统一表示为(n 表示题目指定的参数,i 为统计哪位上的1):

count = (n / (i * 10) * i) + ?,?处的数量就要看 i 及其右边的位数,即n % (i * 10)(记为 x),是小于 i 、大于等于 i 了,具体大多少了:

  • x < i,? = 0
  • i <= x < 2 * i, ? = x - i + 1
  • x >= 2 * i,? = i

写成一行:? = min(max(x - i + 1, 0), i),请仔细体会。

完整公式为:count = (n / (i * 10) * i) + min(max(n % (i * 10) - i + 1, 0), i)。

有了公式,我们很快就能计算出来 n = 2021时,百位数一共会出现 2 * 100 + min(max(21-100+1, 0), 100)=200 个1了,它们分别是100,101,…,199,1100,1101,1199。

好了,这时候写代码就简单得多了:

class Solution {
    public int countDigitOne(int n) {
        // 2021
        int ans = 0;
        for (int i = 1; i <= n; i *= 10) {
            ans += (n / (i * 10)) * i + Math.min(Math.max(n % (i * 10) - i + 1,0), i);
        }
        return ans;
    }
}

时间复杂度:O(log nlogn),跟 n 的位数正相关,这里其实是以10为底的对数,不过复杂度统一使用log。
空间复杂度:O(1)。

官方解释传送门

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值