1. 问题背景与理解
第一次看到LeetCode 401这道"二进制手表"的题目时,我以为是考察硬件编程或者嵌入式开发。实际上这是一道伪装成硬件问题的经典算法题,考察的是组合数学和位运算的应用。题目要求我们列出所有可能的时间表示,其中小时(0-11)和分钟(0-59)的二进制形式中1的个数之和等于给定的亮灯数。
这个题目巧妙地将时间表示与二进制结合起来,我特别喜欢这种将现实场景抽象为算法问题的设计。它不像某些纯数学题那样枯燥,而是让我们在解决实际问题的过程中掌握核心算法思想。
2. 解题思路分析
2.1 暴力枚举法
最直观的解法就是枚举所有可能的时间组合,然后检查哪些组合满足亮灯数的要求。具体来说:
- 遍历所有可能的小时(0-11)
- 对于每个小时,遍历所有可能的分钟(0-59)
- 计算当前小时和分钟的二进制表示中1的个数总和
- 如果总和等于给定的亮灯数,就将这个时间加入结果列表
这种方法的时间复杂度是O(12*60)=O(720),在LeetCode的约束条件下完全够用。虽然看起来有点"暴力",但对于这个问题规模来说是最简单可靠的解法。
2.2 位运算优化
我们可以用位运算来优化1的个数的计算。在C++中可以使用__builtin_popcount函数,Java中可以使用Integer.bitCount,Python中可以使用bin(x).count('1')。
例如,计算5的二进制表示中1的个数:
- 5的二进制是101
- bin(5).count('1') = 2
这种优化虽然在大O表示法上没有改变时间复杂度,但实际运行时会更快,因为CPU有专门的指令来处理位运算。
2.3 组合数学方法
更高级的解法是使用组合数学。我们可以:
- 遍历所有可能的亮灯数分配方式(小时亮灯数k,分钟亮灯数num-k)
- 对于每种分配,找出所有二进制表示有k个1的小时
- 找出所有二进制表示有(num-k)个1的分钟
- 将这些组合起来形成有效时间
这种方法避免了无效的枚举,理论上更高效,但实现起来稍微复杂一些。
3. 代码实现与解析
3.1 Python实现
def readBinaryWatch(num): def count_bits(n): return bin(n).count('1') result = [] for h in range(12): for m in range(60): if count_bits(h) + count_bits(m) == num: result.append(f"{h}:{m:02d}") return result这个实现非常直观:
- 定义了一个辅助函数count_bits来计算数字的二进制表示中1的个数
- 双重循环枚举所有可能的小时和分钟
- 检查亮灯数是否匹配
- 格式化时间字符串(注意分钟要补零)
3.2 Java实现
import java.util.*; public class Solution { public List<String> readBinaryWatch(int num) { List<String> result = new ArrayList<>(); for (int h = 0; h < 12; h++) { for (int m = 0; m < 60; m++) { if (Integer.bitCount(h) + Integer.bitCount(m) == num) { result.add(String.format("%d:%02d", h, m)); } } } return result; } }Java实现使用了Integer.bitCount这个内置方法,它是用高效的位操作实现的,性能很好。
3.3 C++实现
#include <vector> #include <string> #include <bitset> using namespace std; vector<string> readBinaryWatch(int num) { vector<string> result; for (int h = 0; h < 12; ++h) { for (int m = 0; m < 60; ++m) { if (bitset<10>(h << 6 | m).count() == num) { result.push_back(to_string(h) + ":" + (m < 10 ? "0" : "") + to_string(m)); } } } return result; }C++版本使用了bitset来高效计算1的个数。这里有个技巧:将小时和分钟合并成一个10位数(小时占高4位,分钟占低6位),然后用bitset一次性计算总亮灯数。
4. 复杂度分析与优化
4.1 时间复杂度
暴力枚举法的时间复杂度是O(12*60)=O(720),这是一个常数时间复杂度,因为无论输入是什么,我们都要遍历所有可能的时间组合。
4.2 空间复杂度
空间复杂度是O(1)的额外空间(不考虑输出结果),因为我们只需要常数级别的额外空间来存储临时变量。
4.3 可能的优化
虽然暴力法已经足够好,但我们还可以做一些优化:
- 预计算所有数字的亮灯数,存储在一个数组中,避免重复计算
- 使用组合数学方法提前生成有效的小时和分钟组合
- 对于无效的亮灯数(如>8)可以直接返回空列表
5. 边界条件与测试用例
5.1 特殊输入处理
- num=0:只有"0:00"一个解
- num>8:无解,因为最大亮灯数是小时(3亮灯) + 分钟(5亮灯) = 8
- num<0:无效输入,应该返回空列表
5.2 典型测试用例
测试用例1:num=1 期望输出:["0:01","0:02","0:04","0:08","0:16","0:32","1:00","2:00","4:00","8:00"] 测试用例2:num=2 期望输出应包含:"0:03","0:05","0:06","0:09","0:10","0:12","0:17","0:18","0:20","0:24","0:33","0:34","0:36","0:40","0:48","1:01","1:02","1:04","1:08","1:16","1:32","2:01","2:02","2:04","2:08","2:16","2:32","3:00","4:01","4:02","4:04","4:08","4:16","4:32","5:00","6:00","8:01","8:02","8:04","8:08","8:16","8:32","9:00","10:00"] 测试用例3:num=8 期望输出:["7:31","7:47","7:55","7:59","11:31","11:47","11:55","11:59"]6. 常见错误与调试技巧
6.1 常见错误
- 分钟格式化错误:忘记补零,如"1:2"而不是"1:02"
- 亮灯数计算错误:特别是使用位运算时可能出错
- 边界条件处理不当:如num=0或num>8的情况
- 无效时间包含:如小时>11或分钟>59
6.2 调试技巧
- 打印中间结果:在循环中打印当前小时、分钟和亮灯数
- 单元测试:为count_bits函数单独编写测试
- 小规模测试:先用num=1这样的小输入验证基本逻辑
- 对比输出:与预期输出对比,找出差异点
7. 算法扩展与变种
7.1 类似问题
- 二进制手表II:给定亮灯数,返回所有可能的时间(就是本题)
- 二进制手表I:给定时间,返回亮灯数(更简单)
- 数字的二进制表示中1的个数(191. Number of 1 Bits)
7.2 进阶思考
- 如果LED可以显示24小时制,如何修改算法?
- 如果手表有更多LED(如秒数),如何扩展解法?
- 如何高效生成所有满足条件的组合,而不是暴力枚举?
8. 实际应用场景
虽然这个问题看起来有点抽象,但它实际上体现了几个重要的编程概念:
- 位运算的应用:如何高效计算二进制表示中1的个数
- 组合问题的解法:如何系统地枚举所有可能组合
- 问题抽象能力:将现实问题转化为算法问题
这些技能在嵌入式开发、密码学、数据压缩等领域都有实际应用。例如,在嵌入式系统中,我们经常需要检查或设置特定的位;在密码学中,计算汉明重量(1的个数)是一个基本操作。
9. 个人心得与建议
在解决这个问题时,我有几点体会:
- 不要被题目描述迷惑:看似硬件问题,实则是算法题
- 从简单方法开始:暴力法虽然"笨",但往往是最可靠的起点
- 善用语言特性:不同语言提供的位运算工具可以简化代码
- 注意输出格式:特别是时间格式化这种细节容易出错
对于刷题建议,我认为这道题适合用来练习:
- 位运算基础
- 组合问题的枚举方法
- 边界条件处理
- 代码的清晰性和正确性
最后提醒一点:在面试中遇到这类问题时,一定要先和面试官确认输入输出的具体要求,比如时间格式、异常处理等,这能展现你的沟通能力和严谨性。