返回
创建于
状态
公开

在 JavaScript 中,最优雅且易于面试官理解的解法是直接遍历法。JavaScript 提供了非常方便的 toString(2) 方法来处理二进制转换。

以下是具体的代码实现和核心技巧:


1. 直接遍历法 (推荐)

这种方法通过遍历所有可能的 720 分钟(12 小时 × 60 分钟),检查二进制中 1 的个数是否符合要求。

javascript
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]

javascript
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')。

如果你对位运算的具体原理(比如为什么 n&(n1)n \& (n-1) 能去 11)感兴趣,我可以详细解释一下?

好的,我们来重点拆解一下**位运算(Bit Manipulation)**在这个问题里的核心逻辑。

在 LeetCode 的二进制手表问题中,理解“灯”与“数值”的关系是关键。


1. 核心原理:二进制计数

手表上的灯其实就是二进制的各个位(Bit)。

  • 小时 (Hours): 4 盏灯,对应二进制位 23,22,21,202^3, 2^2, 2^1, 2^0(即 8,4,2,18, 4, 2, 1)。最大值是 11112=151111_2 = 15,但题目限制小时不能超过 1111
  • 分钟 (Minutes): 6 盏灯,对应二进制位 25,24,23,22,21,202^5, 2^4, 2^3, 2^2, 2^1, 2^0(即 32,16,8,4,2,132, 16, 8, 4, 2, 1)。最大值是 1111112=63111111_2 = 63,但题目限制分钟不能超过 5959

当你点亮 turnedOn 盏灯时,本质上是在寻找所有“二进制表示中 1 的个数之和”等于 turnedOn 的时间组合。


2. 什么是 n & (n - 1)

在面试中,如果你不使用 toString(2).split('1').length - 1 这种 JS 特有技巧,面试官通常会考你如何通过位运算计算 1 的个数。

这是一个非常经典的算法(Brian Kernighan 算法):

逻辑推导:

假设 n=12n = 12,其二进制为 1100

  1. n=12n = 12 (1100)
  2. n1=11n - 1 = 11 (1011)
  3. 做与运算 (n&(n1)n \& (n-1)):
    text
    11100  (12)
    2& 1011  (11)
    3----------
    4  1000  (8)
    你会发现,最右边的 1 消失了!

为什么有效?

因为 n1n-1 会把 nn 二进制中最右边的 1 变成 0,并把它右边所有的 0 变成 1。两者相与,必然会消除掉那个最右边的 1

我们可以写一个通用的计数函数:

javascript
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. 为什么“直接遍历”是可行的?

很多同学在做算法题时,第一反应是写复杂的递归(回溯)。但在处理这种取值范围极其有限的问题时,直接遍历往往更优。

维度取值范围计算量
小时0110 \sim 11 (12种)12×60=72012 \times 60 = 720 次循环
分钟0590 \sim 59 (60种)720 次计算在计算机看来是微秒级的

在这种情况下,代码的可读性稳定性(不容易写出死循环或逻辑错误)远比算法的“高级感”更重要。


4. JS 格式化的细节

在代码中,m < 10 ? '0' + m : m 或者 ${m}.padStart(2, '0') 是为了处理时间格式的规范:

  • 如果分钟是 8,手表显示必须是 1:08
  • 如果小时是 1,则不需要补零,直接显示 1:08

这个逻辑你清楚了吗?需要我帮你写一个结合了位运算 n & (n-1) 的完整 JS 版本吗?

既然你对位运算的原理感兴趣,我们最后来看一个不依赖 JS 内置函数(不使用 toStringsplit),纯靠位运算实现的完整版本。

在面试中,这种写法通常能展现你对底层原理的掌握。


纯位运算版本 (JavaScript)

这个版本的核心是实现一个 countBits 函数,利用我们刚才讨论的 n & (n - 1) 技巧。

javascript
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};

为什么这种写法在面试中“加分”?

  1. Brian Kernighan 算法n & (n - 1) 的时间复杂度只与 1 的个数有关,比逐位移位检查要快。
  2. 空间意识:这种写法避免了生成大量的临时字符串(toString 会产生新字符串),对内存更友好。
  3. 通用性:这段逻辑在 C++、Java、Go 等任何语言中几乎是一模一样的,体现了你对算法通用性的理解。

总结一下二进制手表的解题套路:

  • 看到“组合”或“状态枚举”:先看数据范围。如果像这题只有 720 种情况,直接遍历是最稳的。
  • 看到“二进制计数”:立刻联想到 n & (n - 1) 或者语言内置的位计数函数。
  • 注意细节:小时和分钟的边界(12126060),以及分钟的补零格式。