给定两个用字符串表示的二进制数,返回它们的和(同样是二进制字符串)。做法就是竖式加法:从两个字符串的最低位(最右边)往左,逐位相加,每一位的结果是 (a位 + b位 + 进位) % 2,新的进位是 (a位 + b位 + 进位) / 2;两个数都走完之后如果还有进位,还要再写一位。这道题的重点和上一题、字符串相乘一样:输入可以有一万位,不能转成语言内置的整数。本文用真实运行展示 Java 的 long 和 JavaScript 的 parseInt 各自怎么失败,给出五种语言的实现,并把逐位相加做成分步演示。
完整题目见官方页面(链接在文末)。两个数都不含前导 0(除非就是 "0")。基准解用 Python 的 int(a, 2) 解析成大整数,相加,再用 bin 转回二进制,是与逐位相加完全不同的写法。一共 12 组用例,覆盖:官方示例、0 + 0、1 + 1、两个数长度差别很大、连续进位(全 1 加 1)、64 位和 70 位的大数:
输入 ["11","1"] 输出 "100" 输入 ["1010","1011"] 输出 "10101" 输入 ["0","0"] 输出 "0" 输入 ["1","1"] 输出 "10" 输入 ["1","0"] 输出 "1" 输入 ["0","1"] 输出 "1"
如果想偷懒,用语言内置的函数把字符串解析成整数,相加,再转回二进制字符串,输入变长时就会出问题:Java 的 long 最多 63 位,更长的字符串会抛异常;JavaScript 的 Number 是双精度浮点,超过 53 位就会丢失末位。下面是真实运行的结果,JavaScript 那一组用的是 60 个 1 加 "10"(真实的和是 2^60 + 1):
直接用语言内置整数做二进制加法,为什么不行:
Java: Long.parseLong(70 个 1, 2) 抛出 NumberFormatException: For input string: "1111111111111111111111111111111111111111111111111111111111111111111111" under radix 2
JS parseInt 相加 : 1000000000000000000000000000000000000000000000000000000000000 (末位 0)
JS BigInt 相加 : 1000000000000000000000000000000000000000000000000000000000001 (末位 1)
两者相同? false
本文 solve('1' * 60, '10') = 1000000000000000000000000000000000000000000000000000000000001,与 Python 大整数结果一致:TrueJava 里 Long.parseLong 解析 70 个 1 时直接抛出 NumberFormatException。JavaScript 里 parseInt 相加的结果末位是 0,而用 BigInt 得到的正确结果末位是 1,两者不同:2^60 + 1 超出了双精度浮点能精确表示的范围,被舍入成了 2^60。本文的逐位相加不受位数限制,结果与 Python 大整数的结果一致。
两个指针 i、j 分别指向 a、b 的最后一位,carry 是进位。只要 i 或 j 还没走完,或者还有进位,就继续:把当前两位(走完的那个数按 0 计)和进位相加得 s,这一位写 s % 2,进位变成 s / 2。结果是从低位到高位生成的,最后要反转。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。
Python · 解法代码def solve(a, b): i, j, carry = len(a) - 1, len(b) - 1, 0 res = [] while i >= 0 or j >= 0 or carry: # 两个数都走完了,还要看最后的进位 s = carry if i >= 0: s += int(a[i]) i -= 1 if j >= 0: s += int(b[j]) j -= 1 res.append(str(s % 2)) # 这一位的结果 carry = s // 2 # 进位 return "".join(reversed(res))
$ python3 solution.py # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON ["11","1"] → "100" ["1010","1011"] → "10101" ["0","0"] → "0" ["1","1"] → "10" ["1","0"] → "1" ["0","1"] → "1"
Java · 解法代码class Solution { public String addBinary(String a, String b) { int i = a.length() - 1, j = b.length() - 1, carry = 0; StringBuilder sb = new StringBuilder(); while (i >= 0 || j >= 0 || carry > 0) { // 两个数都走完了,还要看最后的进位 int s = carry; if (i >= 0) s += a.charAt(i--) - '0'; if (j >= 0) s += b.charAt(j--) - '0'; sb.append(s % 2); // 这一位的结果 carry = s / 2; // 进位 } return sb.reverse().toString(); } }
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl ["11","1"] → "100" ["1010","1011"] → "10101" ["0","0"] → "0" ["1","1"] → "10" ["1","0"] → "1" ["0","1"] → "1"
Go · 解法代码func addBinary(a string, b string) string { i, j, carry := len(a)-1, len(b)-1, 0 res := []byte{} for i >= 0 || j >= 0 || carry > 0 { // 两个数都走完了,还要看最后的进位 s := carry if i >= 0 { s += int(a[i] - '0') i-- } if j >= 0 { s += int(b[j] - '0') j-- } res = append(res, byte('0'+s%2)) // 这一位的结果 carry = s / 2 // 进位 } for l, r := 0, len(res)-1; l < r; l, r = l+1, r-1 { res[l], res[r] = res[r], res[l] } return string(res) }
$ go build -o bin . && ./bin < cases.jsonl ["11","1"] → "100" ["1010","1011"] → "10101" ["0","0"] → "0" ["1","1"] → "10" ["1","0"] → "1" ["0","1"] → "1"
JavaScript · 解法代码function addBinary(a, b) { let i = a.length - 1, j = b.length - 1, carry = 0; const res = []; while (i >= 0 || j >= 0 || carry > 0) { // 两个数都走完了,还要看最后的进位 let s = carry; if (i >= 0) s += Number(a[i--]); if (j >= 0) s += Number(b[j--]); res.push(s % 2); // 这一位的结果 carry = Math.floor(s / 2); // 进位 } return res.reverse().join(""); }
$ node run.js < cases.jsonl ["11","1"] → "100" ["1010","1011"] → "10101" ["0","0"] → "0" ["1","1"] → "10" ["1","0"] → "1" ["0","1"] → "1"
C++ · 解法代码#include <algorithm> #include <string> using namespace std; class Solution { public: string addBinary(string a, string b) { int i = (int)a.size() - 1, j = (int)b.size() - 1, carry = 0; string res; while (i >= 0 || j >= 0 || carry > 0) { // 两个数都走完了,还要看最后的进位 int s = carry; if (i >= 0) s += a[i--] - '0'; if (j >= 0) s += b[j--] - '0'; res.push_back('0' + s % 2); // 这一位的结果 carry = s / 2; // 进位 } reverse(res.begin(), res.end()); return res; } };
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl ["11","1"] → "100" ["1010","1011"] → "10101" ["0","0"] → "0" ["1","1"] → "10" ["1","0"] → "1" ["0","1"] → "1"
共 12 组用例(含边界),以暴力解的结果为基准逐组比对: Python 通过 12/12 Java 通过 12/12 Go 通过 12/12 JavaScript 通过 12/12 C++ 通过 12/12
Python 版本另外和 int(a, 2) 做了随机对拍:两个数各 1 到 20 位,一半是随机的,一半是全 1(制造连续进位):
随机对拍 20000 组(两个各 1 到 20 位的二进制串(一半是随机的,一半是全 1,制造连续进位);首位是 1(按题目保证没有前导 0),偶尔取 "0"),solve 与 brute 的结果全部一致。
另外四种语言只在上面 12 组固定用例上验证过,没有做大规模随机对拍。这道题只扫描两个字符串各一遍,是线性的,所以没有做规模对比实验。
下面的演示用 1011(十进制 11)加 110(十进制 6),和应该是 10001(十进制 17)。最低位 1 + 0 = 1,写 1;下一位 1 + 1 = 2,写 0 进 1;再下一位 1(进位)+ 0 + 1 = 2,写 0 进 1;最高位 1(进位)+ 1 = 2,写 0 进 1;两个数都走完了,还剩一个进位 1,再写一位。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。