LeetCode 上一道非常经典的基础题——67. 二进制求和。这道题虽然难度是简单,但却是面试中常考的基础题型,核心考察对二进制运算规则的理解,以及字符串、进位的处理技巧,非常适合新手入门练习。
先来看下题目要求:给你两个二进制字符串 a 和 b ,以二进制字符串的形式返回它们的和。这里要注意,二进制字符串仅由 '0' 和 '1' 组成,而且可能存在长度不一致的情况(比如 a 是 "11",b 是 "101"),同时不能直接将二进制字符串转成数字再相加(因为当字符串过长时,会超出数字类型的范围,导致溢出)。
先上最终 AC 代码,后面逐行拆解逻辑,保证新手也能看懂:
function addBinary(a: string, b: string): string {
// 步骤1:将两个二进制字符串反转,方便从低位到高位依次相加
const aArr = a.split('').reverse().join('');
const bArr = b.split('').reverse().join('');
// 步骤2:确定循环的最大长度(取两个字符串长度的最大值)
const n = Math.max(a.length, b.length);
let carry: number = 0; // 进位标识,初始为0
const ans = []; // 存储相加后的每一位结果
// 步骤3:从低位到高位依次遍历,计算每一位的和
for (let i = 0; i < n; i++) {
// 取出当前位的数字,若超出字符串长度则视为0
carry += (i < a.length ? parseInt(aArr[i]) : 0);
carry += i < b.length ? parseInt(bArr[i]) : 0;
// 当前位的结果 = (当前和)% 2(二进制逢2进1,余数就是当前位值)
ans.push((carry % 2).toString());
// 更新进位:取当前和的整数部分(逢2进1,商就是进位值)
carry = Math.floor(carry / 2);
}
// 步骤4:循环结束后,若还有进位,需在最高位补1
if (carry) {
ans.push('1');
}
// 步骤5:将结果数组反转,拼接成字符串返回
return ans.reverse().join('');
};
一、核心思路解析
二进制求和的核心规则和十进制求和类似,都是「从低位到高位依次相加,逢进制数进1」,区别在于十进制逢10进1,二进制逢2进1。
这道题的关键难点的是:字符串长度不一致 + 进位处理 + 避免数字溢出。
我们的解决方案是:将字符串反转,从索引0开始(即原字符串的最低位)依次相加,用一个变量记录进位,遍历结束后判断是否还有剩余进位,最后将结果反转回来,就是最终的二进制和。
二、代码逐行拆解
1. 字符串反转处理
const aArr = a.split('').reverse().join('');
const bArr = b.split('').reverse().join('');
这一步是解题的关键技巧。比如 a = "110"(二进制对应6),反转后就是 "011";b = "1011"(二进制对应11),反转后就是 "1101"。
为什么要反转?因为二进制求和需要从最低位开始计算,而字符串的索引是从左到右(对应二进制的高位到低位),反转后,索引0就对应原字符串的最低位,方便我们用循环从左到右遍历,依次计算每一位的和。
2. 初始化变量
const n = Math.max(a.length, b.length);
let carry: number = 0;
const ans = [];
-
n:取两个字符串长度的最大值,确保循环能覆盖所有位(比如 a 长度2,b 长度3,循环3次,就能遍历完所有位)。
-
carry:进位标识,初始值为0。比如某一位相加得2(1+1),则进位为1,当前位为0;若相加得3(1+1+1,包含上一位的进位),则进位为1,当前位为1。
-
ans:数组用于存储每一位计算后的结果,最后拼接成字符串返回,比直接字符串拼接效率更高。
3. 核心循环:逐位相加
for (let i = 0; i < n; i++) {
carry += (i < a.length ? parseInt(aArr[i]) : 0);
carry += i < b.length ? parseInt(bArr[i]) : 0;
ans.push((carry % 2).toString());
carry = Math.floor(carry / 2);
}
这一段是整个代码的核心,我们逐行拆解循环内的逻辑:
-
carry += (i < a.length ? parseInt(aArr[i]) : 0):判断当前索引 i 是否在 a 反转后的字符串长度范围内,如果在,就将当前位的字符转成数字(0或1)加到 carry 上;如果不在(说明 a 的长度比 b 短,当前位没有数字),就加0。 -
carry += i < b.length ? parseInt(bArr[i]) : 0:和上一步同理,处理 b 反转后的当前位。 -
ans.push((carry % 2).toString()):计算当前位的结果。因为二进制逢2进1,所以当前位的值就是 carry 除以2的余数(比如 carry=2,余数0;carry=3,余数1;carry=1,余数1),转成字符串后存入 ans 数组。 -
carry = Math.floor(carry / 2):更新进位。carry 除以2的整数部分,就是下一位需要加的进位(比如 carry=2,整数部分1;carry=3,整数部分1;carry=1,整数部分0)。
举个例子帮助理解:假设 a="11"(反转后"11"),b="1"(反转后"1"),循环执行2次(n=2):
-
i=0:carry = 0 + 1(aArr[0]) + 1(bArr[0])=2 → 当前位 2%2=0,ans.push("0") → carry=2/2=1。
-
i=1:carry = 1 + 1(aArr[1]) + 0(bArr[1]不存在)=2 → 当前位 2%2=0,ans.push("0") → carry=2/2=1。
4. 处理剩余进位
if (carry) {
ans.push('1');
}
循环结束后,可能还存在进位(比如上面的例子,循环结束后 carry=1),这时候需要在结果的最高位补1。比如上面的例子,ans 此时是 ["0","0"],push("1") 后变成 ["0","0","1"]。
5. 结果反转并返回
return ans.reverse().join('');
因为我们之前将字符串反转后计算,ans 数组中存储的是从低位到高位的结果,所以需要再反转一次,才能得到从高位到低位的正确二进制字符串。比如上面的例子,ans 反转后是 ["1","0","0"],join('') 后就是 "100",也就是 11 + 1 = 100(二进制),结果正确。
三、总结与优化思路
这道题的核心逻辑就是「模拟二进制手工求和」,通过反转字符串简化低位遍历,用变量记录进位,处理好边界情况(长度不一致、剩余进位),就能轻松AC。
关于优化:
-
当前代码的时间复杂度是 O(n)(n为两个字符串长度的最大值),空间复杂度是 O(n)(存储结果的数组),已经是最优解,因为必须遍历所有位才能计算出结果。
-
如果不想用反转字符串的方式,也可以用指针从两个字符串的末尾(低位)开始遍历,逻辑类似,只是需要处理指针越界的情况,代码会稍微繁琐一点。