1. 问题背景与理解
今天我们来聊聊Leetcode上剑指Offer系列的一道经典题目——"数字1的个数"。这道题编号为剑指Offer II 162,看似简单,实则暗藏玄机。题目要求我们计算从0到n的所有整数中,数字1出现的总次数。
举个例子,当n=13时,我们需要计算0,1,2,3,...,13这些数字中所有1的个数。具体来说:
- 1包含1个1
- 10包含1个1
- 11包含2个1
- 12包含1个1
- 13包含1个1
所以总共有6个1。
这道题之所以被选入剑指Offer,是因为它考察了面试者对数字规律的观察能力和数学思维。在实际面试中,面试官可能会先让你给出暴力解法,然后逐步引导你寻找更优解。
2. 暴力解法分析
2.1 直观思路
最直观的解法就是遍历从1到n的所有数字,对每个数字逐位检查是否为1,然后累加计数。这种方法简单直接,容易想到。
python复制def countDigitOne(n):
count = 0
for i in range(1, n+1):
num = i
while num > 0:
if num % 10 == 1:
count += 1
num = num // 10
return count
2.2 时间复杂度分析
这种方法的时间复杂度是O(n*log n),因为对于每个数字i,我们需要检查其所有位数(大约log i位)。当n很大时(比如10^9),这种解法会非常慢,无法在合理时间内完成。
2.3 暴力解法的局限性
虽然暴力解法在小规模数据上表现尚可,但在面试中仅给出这种解法通常是不够的。面试官期望看到更高效的解法,这就要用到数学规律了。
3. 数学规律解法
3.1 数字位分析
我们需要找到一个数学规律,能够直接计算出1在各个位上出现的次数,而不需要逐个数字检查。考虑数字的每一位(个位、十位、百位等),分别计算该位为1时有多少种可能。
以数字n=1234为例,我们来看百位上的1出现的次数:
- 百位数字是2,大于1
- 百位为1的数字范围是0100-0199,1100-1199
- 总共有2*100=200个1(因为更高位可以是0或1)
3.2 通用公式推导
对于任意数字n,我们可以按位计算1的个数。设当前位为digit,其权重为weight(个位=1,十位=10,百位=100等),则:
- 如果digit > 1:
该位1出现的次数 = (高位数字 + 1) * weight - 如果digit == 1:
该位1出现的次数 = 高位数字 * weight + 低位数字 + 1 - 如果digit == 0:
该位1出现的次数 = 高位数字 * weight
3.3 算法实现
基于上述规律,我们可以写出如下代码:
python复制def countDigitOne(n):
count = 0
weight = 1 # 当前位的权重,从个位开始
while weight <= n:
# 分解数字为高位、当前位、低位
high = n // (weight * 10)
current = (n // weight) % 10
low = n % weight
if current > 1:
count += (high + 1) * weight
elif current == 1:
count += high * weight + low + 1
else:
count += high * weight
weight *= 10 # 移动到下一位
return count
4. 算法正确性验证
4.1 小规模测试
让我们用n=13来验证算法:
- weight=1(个位):
high=1, current=3, low=0
current>1 → count += (1+1)*1=2 - weight=10(十位):
high=0, current=1, low=3
current==1 → count += 0*10 + 3 + 1=4
总计:2+4=6,与暴力解法结果一致。
4.2 边界情况
- n=0:结果为0
- n=1:结果为1
- n=10:个位1出现1次(1),十位1出现1次(10),总计2次
这些边界情况都能正确处理。
5. 复杂度分析与优化
5.1 时间复杂度
数学解法的时间复杂度是O(log n),因为只需要遍历数字的每一位。对于32位整数最多只需32次循环,效率极高。
5.2 空间复杂度
仅使用了常数个变量,空间复杂度为O(1)。
5.3 可能的优化
虽然当前算法已经很高效,但可以考虑以下几点:
- 提前终止:当high为0且current为0时可以提前结束循环
- 位运算:某些情况下可以用位运算替代除法,但现代编译器通常会自动优化
6. 实际应用与扩展
6.1 类似问题
这种按位分析的思路可以推广到其他数字计数问题,比如:
- 计算数字0的个数
- 计算数字2-9的个数
- 计算数字"13"出现的次数等
6.2 实际应用场景
这类问题在实际中有多种应用:
- 数字统计分析:如统计某个数字在大量数据中出现的频率
- 页码计数:如打印书籍时计算需要多少个数字"1"
- 性能优化:某些算法中需要快速计算数字特征
6.3 扩展思考
如果将问题改为计算二进制中1的个数,就是著名的"汉明重量"问题,可以使用位操作技巧高效解决。这展示了不同进制下类似问题的不同解法。
7. 常见错误与调试技巧
7.1 常见错误
- 边界处理不当:忘记处理n=0或n=1的情况
- 权重更新错误:在循环中忘记更新weight或更新顺序不对
- 数字分解错误:高位、当前位、低位的计算有误
7.2 调试技巧
- 打印中间变量:在循环中打印high、current、low的值
- 小规模测试:先用小数字验证算法正确性
- 对比暴力解法:对于中等规模的n,用暴力解法验证结果
8. 面试技巧与回答策略
8.1 面试官期望
面试官通常希望看到:
- 先给出暴力解法,展示基本编程能力
- 分析暴力解法的问题,展示问题意识
- 逐步推导更优解,展示数学思维
- 处理边界条件,展示严谨性
8.2 回答策略
- 先明确问题:确认输入输出要求
- 提出暴力解法并分析复杂度
- 寻找规律,提出数学解法
- 验证算法正确性
- 讨论可能的优化和扩展
9. 代码实现细节
9.1 Python实现优化
Python的整数除法有两种:
- //:向下取整
- /:浮点除法
在本题中必须使用//,否则会导致错误。
9.2 其他语言实现
在C++/Java中需要注意整数溢出问题,特别是当n接近INT_MAX时。Python没有这个问题,因为它的整数大小只受内存限制。
9.3 测试用例
完整的测试应该包括:
- 小数字(0-20)
- 中等数字(100-1000)
- 大数字(10^8-10^9)
- 特殊数字(10^k, 10^k-1)
10. 总结与个人心得
这道题看似简单,但想要高效解决需要深入的数学洞察力。在实际编程中,我总结了以下几点经验:
- 数字问题往往可以分解到位来处理,这种分而治之的思想很重要。
- 数学规律有时比暴力计算更高效,但需要仔细验证。
- 边界条件处理是算法正确性的关键,不能忽视。
- 在面试中,沟通思考过程比直接给出答案更重要。
最后,这道题的变种很多,比如计算其他数字的出现次数,或者计算数字序列而不是单个数字。掌握这种按位分析的思路,可以解决一大类数字计数问题。
