1. 问题重述与理解
LeetCode 1404题要求我们计算将一个二进制数通过特定操作减少到1所需的步骤数。具体规则如下:
- 如果当前数字是偶数,则将其除以2
- 如果当前数字是奇数,则将其加1
题目保证输入总是可以按上述规则变为1。输入是一个由'0'和'1'组成的字符串,长度在1到500之间,且第一个字符总是'1'。
1.1 操作规则的本质分析
这个问题的核心在于理解二进制数的奇偶性判断和加减操作在二进制层面的表现:
- 奇偶判断:二进制数的最低位决定奇偶性。最低位为0是偶数,为1是奇数
- 除以2操作:二进制数右移一位,相当于去掉最右边的0
- 加1操作:从最低位开始,连续的1会变为0,直到遇到第一个0变为1
例如:
- 二进制"1101"(13)是奇数,加1得到"1110"(14)
- 二进制"10"(2)是偶数,除以2得到"1"(1)
2. 基础解法:直接模拟
2.1 模拟思路
最直观的解法是模拟题目描述的操作过程:
- 将二进制字符串转换为十进制数
- 根据当前数值的奇偶性执行相应操作
- 记录操作步骤直到数值变为1
然而,这种方法对于大数(如500位二进制数)效率极低,因为:
- 500位二进制数转换为十进制可能超过普通数据类型的表示范围
- 每次操作都需要完整的数值转换
2.2 二进制层面的模拟优化
更高效的方法是在二进制层面直接模拟操作过程:
python复制def numSteps(s: str) -> int:
steps = 0
while s != '1':
if s[-1] == '0': # 偶数,除以2
s = s[:-1]
else: # 奇数,加1
s = addOne(s)
steps += 1
return steps
def addOne(s: str) -> str:
s = list(s)
i = len(s) - 1
while i >= 0 and s[i] == '1':
s[i] = '0'
i -= 1
if i >= 0:
s[i] = '1'
else:
s.insert(0, '1')
return ''.join(s)
这种方法避免了数值转换,直接在二进制字符串上操作。但最坏情况下(如全1的字符串),加1操作需要O(n)时间,整体复杂度为O(n^2)。
3. 高效解法:进位分析与数学推导
3.1 进位传播规律
观察加1操作后的进位传播特性:
- 加1操作从最低位开始
- 连续的1会变为0,直到遇到第一个0变为1
- 如果所有位都是1,会在最前面添加一个1
关键发现:一旦产生进位,后续每一位都会产生进位。这意味着:
- 进位会一直传播到最高位
- 进位过程中遇到的0会变成1,需要额外的加1操作
3.2 优化算法设计
基于上述观察,可以设计O(n)时间复杂度的算法:
- 初始化步骤数为n-1(每个非最高位至少需要一次除以2操作)
- 从右向左遍历字符串,记录进位
- 遇到1且有进位时,需要额外的加1操作
- 最后处理最高位的进位
python复制def numSteps(s: str) -> int:
steps = 0
carry = 0
n = len(s)
for i in range(n-1, 0, -1):
digit = int(s[i]) + carry
if digit % 2 == 1: # 需要加1操作
steps += 1
carry = 1
steps += 1 # 除以2操作
return steps + carry
3.3 算法正确性验证
以"1101"(13)为例:
- 初始:steps=0, carry=0
- 处理第3位'1':digit=1 → 加1 → steps=1, carry=1
- 处理第2位'1':digit=2 → 不加1 → steps=3 (前面加1和除以2各算一步)
- 处理第1位'0':digit=1 → 加1 → steps=4, carry=1
- 最后处理最高位:steps=6
与示例1一致,验证了算法的正确性。
4. 数学公式解法
4.1 关键观察
进一步分析可以发现:
- 步骤数主要由最右边的1的位置决定
- 最右边的1右边的0不需要额外操作
- 最右边的1左边的0每个都需要一次加1操作
4.2 公式推导
设最右边的1的位置为i(从0开始):
- 如果i=0(即s="1"),直接返回0
- 否则:
- 基础步骤:n-1(每个非最高位至少一次除以2)
- 加1步骤:[1,i-1]区间内的0的数量 + 2(最右边的1和最高位的进位)
python复制def numSteps(s: str) -> int:
n = len(s)
last_one = s.rfind('1')
if last_one == 0:
return n - 1
zeros = s[1:last_one].count('0')
return (n - 1) + zeros + 2
4.3 复杂度分析
- 时间复杂度:O(n),只需遍历字符串常数次
- 空间复杂度:O(1),只使用常数额外空间
5. 边界情况与测试用例
5.1 典型测试用例
| 输入 | 十进制 | 步骤 | 输出 |
|---|---|---|---|
| "1" | 1 | 0 | 0 |
| "10" | 2 | 2→1 | 1 |
| "1101" | 13 | 13→14→7→8→4→2→1 | 6 |
| "111101111" | 495 | 多步 | 13 |
5.2 特殊边界情况
- 最小输入:"1"(直接返回0)
- 全1字符串:"111...1"(需要特别注意进位)
- 交替字符串:"1010...10"(测试0和1交替时的处理)
6. 实际编码注意事项
6.1 语言实现差异
不同语言中字符串处理的效率差异:
- Python:字符串不可变,操作可能产生新对象
- Java/C++:可以转换为字符数组进行原地修改
- Go:可以使用bytes.Buffer高效处理
6.2 性能优化技巧
- 避免不必要的字符串拷贝
- 使用位运算替代部分算术运算
- 提前终止条件判断(如已经为1时立即返回)
6.3 常见错误
- 忽略最高位的进位处理
- 错误计算连续进位的影响
- 边界条件处理不完整(如长度为1的情况)
7. 算法选择建议
根据实际场景选择合适解法:
- 面试场景:推荐数学公式解法,展示问题分析能力
- 竞赛场景:选择最高效的O(n)模拟解法
- 学习场景:先实现直接模拟,再逐步优化
8. 扩展思考
8.1 类似问题
- 计算将一个数变为1的最少操作次数(允许更多操作类型)
- 二进制数的加法、乘法模拟
- 其他进制下的类似问题
8.2 数学性质深入
这个问题实际上与Collatz猜想有关,题目保证可以变为1的特性正是该猜想的内容。虽然Collatz猜想尚未被证明,但在题目限制下总是成立。
8.3 实际应用
这种位操作技巧在以下场景有实际应用:
- 大数运算实现
- 加密算法中的位操作
- 硬件设计中的状态转换
在实际工程中,理解二进制操作的本质可以帮助我们设计更高效的算法和处理大数运算。这道题目虽然看似简单,但蕴含了丰富的计算机科学基础知识,值得深入理解和掌握。