在 JavaScript 中,最优雅且易于面试官理解的解法是直接遍历法。JavaScript 提供了非常方便的 toString(2) 方法来处理二进制转换。
以下是具体的代码实现和核心技巧:
1. 直接遍历法 (推荐)
这种方法通过遍历所有可能的 720 分钟(12 小时 × 60 分钟),检查二进制中 1 的个数是否符合要求。
1/**
2 * @param {number} turnedOn
3 * @return {string[]}
4 */
5var readBinaryWatch = function(turnedOn) {
6 const res = [];
7
8 for (let h = 0; h < 12; h++) {
9 for (let m = 0; m < 60; m++) {
10 // 将小时和分钟转为二进制字符串,并计算 '1' 的个数
11 const hCount = h.toString(2).split('1').length - 1;
12 const mCount = m.toString(2).split('1').length - 1;
13
14 if (hCount + mCount === turnedOn) {
15 // 使用模板字符串进行格式化,分钟不足 10 位补 0
16 res.push(`${h}:${m < 10 ? '0' + m : m}`);
17 }
18 }
19 }
20
21 return res;
22};为什么 JavaScript 这样写最快?
toString(2): 这是 JS 内置的进制转换方法,非常直接。.split('1').length - 1: 这是一个非常巧妙的 JS 技巧,用来统计字符串中某个字符出现的次数。它比正则匹配或循环遍历字符串要简洁得多。
2. 回溯法 (进阶版)
如果你希望展示对算法深度(DFS/Backtracking)的理解,可以使用这种方式。我们把手表的 10 盏灯看作一个数组:[8, 4, 2, 1, 32, 16, 8, 4, 2, 1]。
1var readBinaryWatch = function(turnedOn) {
2 const result = [];
3 // 前4个是小时,后6个是分钟
4 const bits = [8, 4, 2, 1, 32, 16, 8, 4, 2, 1];
5
6 function backtrack(count, index, h, m) {
7 // 剪枝:小时不能超过 11,分钟不能超过 59
8 if (h > 11 || m > 59) return;
9
10 // 终止条件:选够了灯的数量
11 if (count === turnedOn) {
12 result.push(`${h}:${m < 10 ? '0' + m : m}`);
13 return;
14 }
15
16 // 遍历剩余的灯
17 for (let i = index; i < 10; i++) {
18 if (i < 4) {
19 // 点亮小时灯
20 backtrack(count + 1, i + 1, h + bits[i], m);
21 } else {
22 // 点亮分钟灯
23 backtrack(count + 1, i + 1, h, m + bits[i]);
24 }
25 }
26 }
27
28 backtrack(0, 0, 0, 0);
29 return result;
30};面试小贴士
- 性能:这道题的规模非常小(常量级),两种方法在 LeetCode 上的执行时间差异几乎可以忽略不计。
- 代码简洁度:在实际面试中,方法一更不容易写错,且代码量极少,通常是首选。
- ES6+ 语法:使用
String.prototype.padStart()也可以处理分钟补零的问题:${m}.padStart(2, '0')。
如果你对位运算的具体原理(比如为什么 能去 )感兴趣,我可以详细解释一下?
好的,我们来重点拆解一下**位运算(Bit Manipulation)**在这个问题里的核心逻辑。
在 LeetCode 的二进制手表问题中,理解“灯”与“数值”的关系是关键。
1. 核心原理:二进制计数
手表上的灯其实就是二进制的各个位(Bit)。
- 小时 (Hours): 4 盏灯,对应二进制位 (即 )。最大值是 ,但题目限制小时不能超过 。
- 分钟 (Minutes): 6 盏灯,对应二进制位 (即 )。最大值是 ,但题目限制分钟不能超过 。
当你点亮 turnedOn 盏灯时,本质上是在寻找所有“二进制表示中 1 的个数之和”等于 turnedOn 的时间组合。
2. 什么是 n & (n - 1)?
在面试中,如果你不使用 toString(2).split('1').length - 1 这种 JS 特有技巧,面试官通常会考你如何通过位运算计算 1 的个数。
这是一个非常经典的算法(Brian Kernighan 算法):
逻辑推导:
假设 ,其二进制为 1100。
- (
1100) - (
1011) - 做与运算 ():
你会发现,最右边的
11100 (12) 2& 1011 (11) 3---------- 4 1000 (8)1消失了!
为什么有效?
因为 会把 二进制中最右边的 1 变成 0,并把它右边所有的 0 变成 1。两者相与,必然会消除掉那个最右边的 1。
我们可以写一个通用的计数函数:
1function countOnes(n) {
2 let count = 0;
3 while (n > 0) {
4 n = n & (n - 1); // 每次消除一个 1
5 count++;
6 }
7 return count;
8}3. 为什么“直接遍历”是可行的?
很多同学在做算法题时,第一反应是写复杂的递归(回溯)。但在处理这种取值范围极其有限的问题时,直接遍历往往更优。
| 维度 | 取值范围 | 计算量 |
|---|---|---|
| 小时 | (12种) | 次循环 |
| 分钟 | (60种) | 720 次计算在计算机看来是微秒级的 |
在这种情况下,代码的可读性和稳定性(不容易写出死循环或逻辑错误)远比算法的“高级感”更重要。
4. JS 格式化的细节
在代码中,m < 10 ? '0' + m : m 或者 ${m}.padStart(2, '0') 是为了处理时间格式的规范:
- 如果分钟是
8,手表显示必须是1:08。 - 如果小时是
1,则不需要补零,直接显示1:08。
这个逻辑你清楚了吗?需要我帮你写一个结合了位运算 n & (n-1) 的完整 JS 版本吗?
既然你对位运算的原理感兴趣,我们最后来看一个不依赖 JS 内置函数(不使用 toString 和 split),纯靠位运算实现的完整版本。
在面试中,这种写法通常能展现你对底层原理的掌握。
纯位运算版本 (JavaScript)
这个版本的核心是实现一个 countBits 函数,利用我们刚才讨论的 n & (n - 1) 技巧。
1/**
2 * @param {number} turnedOn
3 * @return {string[]}
4 */
5var readBinaryWatch = function(turnedOn) {
6 const res = [];
7
8 // 计算一个整数二进制中 1 的个数
9 const countBits = (n) => {
10 let count = 0;
11 while (n > 0) {
12 n = n & (n - 1); // 抹去最右边的 1
13 count++;
14 }
15 return count;
16 };
17
18 for (let h = 0; h < 12; h++) {
19 for (let m = 0; m < 60; m++) {
20 // 直接计算小时和分钟里 1 的总数
21 if (countBits(h) + countBits(m) === turnedOn) {
22 // 格式化输出:分钟不足 10 位补 '0'
23 res.push(`${h}:${m < 10 ? '0' + m : m}`);
24 }
25 }
26 }
27
28 return res;
29};为什么这种写法在面试中“加分”?
- Brian Kernighan 算法:
n & (n - 1)的时间复杂度只与1的个数有关,比逐位移位检查要快。 - 空间意识:这种写法避免了生成大量的临时字符串(
toString会产生新字符串),对内存更友好。 - 通用性:这段逻辑在 C++、Java、Go 等任何语言中几乎是一模一样的,体现了你对算法通用性的理解。
总结一下二进制手表的解题套路:
- 看到“组合”或“状态枚举”:先看数据范围。如果像这题只有 720 种情况,直接遍历是最稳的。
- 看到“二进制计数”:立刻联想到
n & (n - 1)或者语言内置的位计数函数。 - 注意细节:小时和分钟的边界( 和 ),以及分钟的补零格式。