描述

题目描述

输入一个整数 n ,求1~n 这 n 个整数的十进制表示中1出现的次数。
例如,1~13 中包含1的数字有1、10、11、12、13 因此共出现6次。

示例

输入:n = 13
输出:6

这道题简单意思就是:找出小于等于 n 的非负整数中数字 1 出现的个数

初看这道题,无非都会想到一个简单解法:要么从 1 开始遍历到 n 统计 1 出现的次数。可惜时间复杂度过高,基本都会超时。

既然简单的方法不行,看起来也跟动态规划、递归、数据结构等等无关,也排除贪心或者暴力解法,那不妨考虑用笔在纸上画一画,推导出规律

知识点:数学推导
难度:三星


题解

解题思路

很多问题,都可以将复杂问题简单化,大问题化解为多个小问题,最终推算出最终问题的解法,类似分治法。而且很多问题,如果对于变量过多或者过于负责,我们可以将变量 n 用 简单的常数表示,先解决常见情况,再解决所有情况的问题,也叫化繁为简,逐个击破。

学习解题思路前,不妨先了解下我们使用的行李箱的 “ 滚动密码锁 ” 的开锁,脑海里要有个印象,方便待会理解思路

我们不妨先剖解为小问题,分情况剖析问题:找出小于等于 1234 的非负整数中的百位上,数字 1 出现的个数。即只统计百位数上1出现的次数?

这个小问题主要可以分三种情况,cur 为 0、12~9

1. cur = 0 :

  • 设 n = 1034 的百位数 “0” 为 cur,作为当前位,则 “1” 为 high,作为高位数,“34” 为 low,表示低位数

image-20210619170517503

  • 公式解释:

    因为 cur 始终不变,而我们只统计出现不同数字情况的数量,因此可忽略不变量,此处可通过刚刚说过的行李箱 “滚动密码锁” 的开锁帮助理解

    对高低位分别计算:如上面的 0100-0199,忽略当前位: 0-099,高位 0 忽略,低位 99

    而 99 有 0~99 的情况,则有 100 种结果

    公式 count = high * bitNum 可以这样理解:忽略不变的 cur,相当于 99,count = 高位数的结果 + 低位数的结果,即 0 + (99 + 1) = 1 * 100 = 0 + (99 + 1) = (high - 1) * bitNum + (99 + 1),也是计算高位和低位的结果数之和

2. 当 cur = 1

  • 设 n = 1134 的百位数 “1” 为 cur,作为当前位,则最左边的 “1” 为 high,作为高位数,“34” 为 low,表示低位数

image-20210619172233763

  • 公式解释:

    因为 cur 始终不变,而我们只统计出现不同数字情况的数量,因此可忽略不变量,此处可通过刚刚说过的行李箱 “滚动密码锁” 的开锁帮助理解

    对高低位分别计算:如上面的 0100-1134,忽略当前位: 000 - 134,对于134, 高位为 1,低位为 34

    而 134 有 0 ~ 134 的情况,则有 135 种结果

    好比使用滚轮密码锁,保持 cur 为 1 不变,不断从 0100 到 1134 改变高位和低位的数字,总共就有 135 种结果

    公式 count = high * bitNum + low + 1 可以这样理解:忽略不变的 cur,相当于 134,count = 高位数的结果 + 低位数的结果,即 134 + 1 = 1 * 100 + 34 + 1 = (1 * 100) + (34 + 1) = (high * bitNum) + (low + 1), 最后的 (low + 1) 表示低位数都为 0 的情况

3. 当 cur > 1

  • 设 n = 1334 的百位数 “3” 为 cur,作为当前位,则 “3” 为 high,作为高位数,“34” 为 low,表示低位数

image-20210619170554375

  • 公式解释:

    因为 cur 始终不变,而我们只统计出现不同数字情况的数量,因此可忽略不变量,此处可通过刚刚说过的行李箱 “滚动密码锁” 的开锁帮助理解

    对高低位分别计算:如上面的 0100-0199,忽略当前位: 0-099,高位 0 忽略,低位 99

    而 99 有 0~99 的情况,则有 100 种结果

    公式 count = (high + 1) * bitNum 可以这样理解:忽略不变的 cur,相当于 199,count = 高位数的结果 + 低位数的结果,即 199 + 1 = (1 + 1) * 100 = (1 * 100) + (99 + 1) = (high * bitNum) + (低位数结果数)

如果只计算百位数的情况,代码如下:

public class Solution {
    // 这里假设n>=1000的情况
    public int NumberOf1Between1AndN_Solution(int n) {
        int count = 0, bitNum = 100, high = n / 100, cur = (n / 10) % 10, low = n % 10;
        if(cur == 0) {
            // case 1: cur == 0
            // cur=0时,高位需要减去一位用于低位进行计算
            // 相当于 count = (high - 1) * bitNum + (99 + 1)
            count += high * bitNum;
        } else if(cur == 1) {
            // case 2: cur == 1
            // 相当于高位+低位计算结果,即(high * bitNum) + (low + 1)     
            count += high * bitNum + low + 1;
        } else {
            // case3: cur > 1
            // 相对于cur=0的情况,就不需要高位减去一位来计算低位的结果数了
            // 相当于(high * bitNum) + (低位数结果数)
            count += (high + 1) * bitNum;
        }
        return count;
    }
}

理解了计算某一个位为 1 的结果数这个小问题,那么接下来计算所有位的结果数就简单多了

方法:数学推导

image-20210619173857536

Java 版本代码如下:
public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        int count = 0, bitNum = 1, high = n / 10, cur = n % 10, low = 0;
        // cur遍历n每一数位
        while(cur != 0 || high != 0) {
            if(cur < 1) {
                // case 1: cur == 0
                // cur=0时,高位需要减去一位用于低位进行计算
                // 相当于 count = (high - 1) * bitNum + (99 + 1)
                count += high * bitNum;
            } else if(cur == 1) {
                // case 2: cur == 1
                // 相当于高位+低位计算结果,即(high * bitNum) + (low + 1)     
                count += high * bitNum + low + 1;
            } else {
                // case3: cur > 1
                // 相对于cur=0的情况,就不需要高位减去一位来计算低位的结果数了
                // 相当于(high * bitNum) + (低位数结果数)
                count += (high + 1) * bitNum;
            }
            // low、cur、high都像左偏移一个位
            // bitNum表示cur的数位
            low += cur * bitNum;
            cur = high % 10;
            high = high / 10;
            bitNum = bitNum * 10;
        }
        return count;
    }
}
Go 版本代码如下:
package main

/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 * 
 * @param n int整型 
 * @return int整型
*/
func NumberOf1Between1AndN_Solution( n int ) int {
    // write code here
    bitNum, count := 1, 0
    high, cur, low := n/10, n%10, 0
    for high != 0 || cur != 0 {
        if cur < 1 {
            // case 1: cur == 0
            // cur=0时,高位需要减去一位用于低位进行计算
            count += high * bitNum
        } else if cur == 1 {
            // case 2: cur == 1
            // 相当于高位+低位计算结果,即(high * bitNum) + (low + 1)   
            count += high*bitNum + low + 1
        } else {
            // case3: cur > 1
            // 相对于cur=0的情况,就不需要高位减去一位来计算低位的结果数了
            // 相当于(high * bitNum) + (低位数结果数)
            count += (high + 1) * bitNum
        }
        // low、cur、high都像左偏移一个位
        // bitNum表示cur的数位
        low = low + cur*bitNum
        high, cur = high/10, high%10
        bitNum = bitNum * 10
    }
    return count

}

时间复杂度 O(log n) : 时间复杂度只需要看 while 循环的级别,循环内的操作使用 O(1) 的时间,而循环数字 n 的位数,相当于使用了 log10 (n),因此时间复杂度为 O(log n)
空间复杂度 O(1):没有用到类似数组、队列等需要开辟大量空间的数据类型,只使用了几个变量,因此空间复杂度为 O (1)

总结:分界问题,化繁为简,逐个击破