目录
问题描述
给定一个整数 num
,计算所有 小于等于 num
的 非负整数 中数字 '1'
出现的个数(也被称为 数字 1 的个数)。
注意:
- 计算过程中,数字
'1'
可以出现在任意位上。 num
是一个非负整数,且满足0 <= num < 10^9
。
原题链接:
- https://leetcode-cn.com/problems/number-of-digit-one/
- https://leetcode.cn/problems/1nzheng-shu-zhong-1chu-xian-de-ci-shu-lcof/description/
示例
示例 1:
输入:
num = 0
输出:
0
解释:
数字 0
中没有数字 '1'
。
示例 2:
输入:
num = 13
输出:
6
解释:
从 0
到 13
中,数字 '1'
出现的次数如下:
1
→ 一次10
→ 一次11
→ 两次12
→ 一次13
→ 一次
总计 1 + 1 + 2 + 1 + 1 = 6
次。
示例 3:
输入:
num = 99
输出:
20
解释:
从 0
到 99
中,数字 '1'
出现的次数为 20
次。
思路解析
本题要求在一个范围内统计数字 '1'
出现的总次数。由于 num
的范围较大(0 <= num < 10^9
),直接遍历每个数字并统计 '1'
的出现次数将导致时间效率过低。因此,需要采用 数学方法 来高效地解决问题。
方法一:按位计数(数学方法)
核心思路
对于每一位(个位、十位、百位等),计算数字 '1'
在该位上出现的次数。具体来说,对于一个数 num
,我们可以按以下步骤计算每一位上 '1'
的出现次数:
-
确定当前位的位置:
- 设当前位为第
i
位(从0
开始计数,0
为个位,1
为十位,依此类推)。 - 计算
divider = 10^i
。
- 设当前位为第
-
分割数字:
- 将
num
分割为三部分:- 高位:
high = floor(num / (divider * 10))
- 当前位:
current = floor((num / divider) % 10)
- 低位:
low = num % divider
- 高位:
- 将
-
计算
'1'
的出现次数:- 如果
current > 1
,则'1'
在当前位上出现的次数为(high + 1) * divider
。 - 如果
current == 1
,则'1'
在当前位上出现的次数为high * divider + low + 1
。 - 如果
current == 0
,则'1'
在当前位上出现的次数为high * divider
。
- 如果
-
累加结果:
- 将所有位上的
'1'
出现次数累加起来,即为最终结果。
- 将所有位上的
数学证明
对于某一位 i
,divider = 10^i
:
- 高位(high):高位数决定了当前位上
'1'
出现的完整周期数。 - 当前位(current):当前位的值决定了当前周期中
'1'
出现的额外次数。 - 低位(low):低位数影响了当前位为
'1'
时的具体次数。
通过这种方法,我们可以在 O(log n) 的时间复杂度内计算出所有位上 '1'
的总次数。
示例分析
以 num = 13
为例:
-
个位(i = 0):
divider = 1
high = floor(13 / 10) = 1
current = floor((13 / 1) % 10) = 3
low = 13 % 1 = 0
- 由于
current > 1
,个位上'1'
出现的次数为(1 + 1) * 1 = 2
-
十位(i = 1):
divider = 10
high = floor(13 / 100) = 0
current = floor((13 / 10) % 10) = 1
low = 13 % 10 = 3
- 由于
current == 1
,十位上'1'
出现的次数为0 * 10 + 3 + 1 = 4
-
总计:
- 总次数为
2 (个位) + 4 (十位) = 6
,与示例一致。
- 总次数为
方法二:动态规划
虽然数学方法已经可以高效解决问题,但另一种常见的方法是 动态规划。在本题中,可以通过记录每个位上的 '1'
出现次数,逐步构建结果。
然而,由于动态规划的复杂度与数学方法相当,且数学方法更直观,因此本文将主要采用 数学方法 来实现。
代码实现
Python 实现
class Solution:
def countDigitOne(self, num: int) -> int:
"""
计算从 0 到 num 中数字 '1' 出现的总次数。
:param num: int - 目标数字
:return: int - 数字 '1' 出现的次数
"""
if num < 0:
return 0
count = 0
i = 0 # 当前位的位置,从个位开始
while 10**i <= num:
divider = 10**i
high = num // (divider * 10)
current = (num // divider) % 10
low = num % divider
if current > 1:
count += (high + 1) * divider
elif current == 1:
count += high * divider + low + 1
else:
count += high * divider
i += 1
return count
测试代码
以下是针对上述方法的测试代码,使用 unittest
框架进行验证。
import unittest
class Solution:
def countDigitOne(self, num: int) -> int:
"""
计算从 0 到 num 中数字 '1' 出现的总次数。
:param num: int - 目标数字
:return: int - 数字 '1' 出现的次数
"""
if num < 0:
return 0
count = 0
i = 0 # 当前位的位置,从个位开始
while 10**i <= num:
divider = 10**i
high = num // (divider * 10)
current = (num // divider) % 10
low = num % divider
if current > 1:
count += (high + 1) * divider
elif current == 1:
count += high * divider + low + 1
else:
count += high * divider
i += 1
return count
class TestCountDigitOne(unittest.TestCase):
def setUp(self):
self.solution = Solution()
def test_example1(self):
num = 0
expected = 0
self.assertEqual(self.solution.countDigitOne(num), expected, "示例1失败")
def test_example2(self):
num = 13
expected = 6
self.assertEqual(self.solution.countDigitOne(num), expected, "示例2失败")
def test_example3(self):
num = 99
expected = 20
self.assertEqual(self.solution.countDigitOne(num), expected, "示例3失败")
def test_bamboo_len_1(self):
num = 1
expected = 1
self.assertEqual(self.solution.countDigitOne(num), expected, "数字1测试失败")
def test_bamboo_len_10(self):
num = 10
expected = 2
self.assertEqual(self.solution.countDigitOne(num), expected, "数字10测试失败")
def test_bamboo_len_11(self):
num = 11
expected = 4
self.assertEqual(self.solution.countDigitOne(num), expected, "数字11测试失败")
def test_bamboo_len_100(self):
num = 100
expected = 21
self.assertEqual(self.solution.countDigitOne(num), expected, "数字100测试失败")
def test_bamboo_len_101(self):
num = 101
expected = 23
self.assertEqual(self.solution.countDigitOne(num), expected, "数字101测试失败")
def test_bamboo_len_large(self):
num = 1000
expected = 301
self.assertEqual(self.solution.countDigitOne(num), expected, "数字1000测试失败")
def test_bamboo_len_negative(self):
num = -1
expected = 0
self.assertEqual(self.solution.countDigitOne(num), expected, "负数测试失败")
def test_bamboo_len_edge1(self):
num = 2147483647 # 最大32位整数
# 由于手动计算此值非常困难,我们将仅检查程序是否运行而不抛出异常
try:
result = self.solution.countDigitOne(num)
self.assertTrue(isinstance(result, int), "边界值测试失败")
except:
self.fail("边界值测试失败,抛出异常")
if __name__ == "__main__":
unittest.main(argv=[''], exit=False)
输出:
............
----------------------------------------------------------------------
Ran 12 tests in 0.XXXs
OK
复杂度分析
时间复杂度
- 总体复杂度:
O(log n)
,其中n
是目标数字num
。- 每一位的计算需要常数时间,最多有
log₁₀ n
位。 - 因此,总体时间复杂度为对数级别。
- 每一位的计算需要常数时间,最多有
空间复杂度
- 总体复杂度:
O(1)
。- 算法只使用了常数级别的额外空间,如变量
count
、high
、current
、low
和位位置i
,不依赖于输入规模。
- 算法只使用了常数级别的额外空间,如变量
结论
通过采用 按位计数 的数学方法,我们能够高效地计算从 0
到 num
的所有非负整数中数字 '1'
出现的总次数。该方法利用了数字在各个位上的独立性,逐位分析 '1'
的出现频率,确保了时间复杂度为 O(log n)
和空间复杂度为 O(1)
的高效性。
这种方法不仅适用于本题,还可广泛应用于其他类似的数字位分析问题。掌握此类按位计数的技巧,对于提升算法设计和问题解决能力具有重要的指导意义,能够在实际编程和算法挑战中灵活应用,提高代码的性能和效率。