【剑指Offer刷题系列】数字 1 的个数



问题描述

给定一个整数 num,计算所有 小于等于 num非负整数 中数字 '1' 出现的个数(也被称为 数字 1 的个数)。

注意

  • 计算过程中,数字 '1' 可以出现在任意位上。
  • num 是一个非负整数,且满足 0 <= num < 10^9

原题链接


示例

示例 1:

输入

num = 0

输出

0

解释
数字 0 中没有数字 '1'


示例 2:

输入

num = 13

输出

6

解释
013 中,数字 '1' 出现的次数如下:

  • 1 → 一次
  • 10 → 一次
  • 11 → 两次
  • 12 → 一次
  • 13 → 一次

总计 1 + 1 + 2 + 1 + 1 = 6 次。


示例 3:

输入

num = 99

输出

20

解释
099 中,数字 '1' 出现的次数为 20 次。


思路解析

本题要求在一个范围内统计数字 '1' 出现的总次数。由于 num 的范围较大(0 <= num < 10^9),直接遍历每个数字并统计 '1' 的出现次数将导致时间效率过低。因此,需要采用 数学方法 来高效地解决问题。

方法一:按位计数(数学方法)

核心思路

对于每一位(个位、十位、百位等),计算数字 '1' 在该位上出现的次数。具体来说,对于一个数 num,我们可以按以下步骤计算每一位上 '1' 的出现次数:

  1. 确定当前位的位置

    • 设当前位为第 i 位(从 0 开始计数,0 为个位,1 为十位,依此类推)。
    • 计算 divider = 10^i
  2. 分割数字

    • num 分割为三部分:
      • 高位high = floor(num / (divider * 10))
      • 当前位current = floor((num / divider) % 10)
      • 低位low = num % divider
  3. 计算 '1' 的出现次数

    • 如果 current > 1,则 '1' 在当前位上出现的次数为 (high + 1) * divider
    • 如果 current == 1,则 '1' 在当前位上出现的次数为 high * divider + low + 1
    • 如果 current == 0,则 '1' 在当前位上出现的次数为 high * divider
  4. 累加结果

    • 将所有位上的 '1' 出现次数累加起来,即为最终结果。

数学证明

对于某一位 idivider = 10^i

  • 高位(high):高位数决定了当前位上 '1' 出现的完整周期数。
  • 当前位(current):当前位的值决定了当前周期中 '1' 出现的额外次数。
  • 低位(low):低位数影响了当前位为 '1' 时的具体次数。

通过这种方法,我们可以在 O(log n) 的时间复杂度内计算出所有位上 '1' 的总次数。

示例分析

num = 13 为例:

  1. 个位(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
  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
  3. 总计

    • 总次数为 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)
    • 算法只使用了常数级别的额外空间,如变量 counthighcurrentlow 和位位置 i,不依赖于输入规模。

结论

通过采用 按位计数 的数学方法,我们能够高效地计算从 0num 的所有非负整数中数字 '1' 出现的总次数。该方法利用了数字在各个位上的独立性,逐位分析 '1' 的出现频率,确保了时间复杂度为 O(log n) 和空间复杂度为 O(1) 的高效性。

这种方法不仅适用于本题,还可广泛应用于其他类似的数字位分析问题。掌握此类按位计数的技巧,对于提升算法设计和问题解决能力具有重要的指导意义,能够在实际编程和算法挑战中灵活应用,提高代码的性能和效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值