bigywater.tech
算法与数据结构 · LeetCode 7

LeetCode 7. 整数反转

给定一个 32 位有符号整数 x,返回把它的数字部分反转后的结果;如果反转后超出 32 位有符号整数的范围 [-2^31, 2^31 - 1],返回 0。题目还规定环境不允许存储 64 位整数,所以不能先用更宽的类型算出结果再比较。这道题没有值得做成动画的动态过程,难点全在溢出检查和各语言对整数运算的细节差异,所以本文给出 Python、Java、Go、JavaScript、C++ 五种语言的实现,每一种都真实编译运行,并与一个独立的暴力解逐组对比。

20,000
组随机对拍,与暴力解的结果全部一致
5 种语言
各自真实编译运行,输出与暴力解逐组一致

题意与示例

完整题目见官方页面(链接在文末)。前几组基准用例及暴力解算出的结果(暴力解是把整数转成字符串翻转,再用 Python 的大整数直接判断范围,与下面的解法思路完全不同);一共 15 组,覆盖零、个位数、末尾带 0、负数、32 位上下界,以及翻转后刚好在边界内外的数:

真实实测 · 本机执行输出
输入 [123]
输出 321
输入 [-123]
输出 -321
输入 [120]
输出 21
输入 [0]
输出 0
输入 [-10]
输出 -1
输入 [7]
输出 7

陷阱:不检查溢出会得到什么结果

按位取出个位、拼到结果末尾,这个循环谁都会写:rev = rev * 10 + x % 10。问题出在 rev * 10 可能已经超出 32 位。下面是不做检查的 Java 写法在两个会溢出的输入上的真实输出,以及 Java 里 Math.abs 对最小值的结果:

真实实测 · 本机执行输出
$ javac N.java && java N   # 不做溢出检查的写法
naive(1534236469) = 1056389759
naive(-1563847412) = 2147483645
Math.abs(Integer.MIN_VALUE) = -2147483648

1534236469 翻转后应该是 9646324351,已经超出 32 位范围,正确答案是 0,但不检查的写法返回了 1056389759,这是 9646324351 按 32 位回绕后的值,看上去像一个正常的答案,不会报任何错。-1563847412 翻转后是 -2147483651,比下界还小一点,无检查的写法返回的却是正数 2147483645,连符号都变了。第三行说明另一个坑:Math.abs(Integer.MIN_VALUE) 的结果仍然是 -2147483648,因为 2147483648 装不进 32 位,所以在 Java、C++ 这类固定宽度语言里,"先取绝对值再处理"对 -2147483648 这个输入是不可靠的。

Python 没有这两个问题,因为整数不会溢出。同样的输入在 Python 里翻转的结果是:

真实实测 · 本机执行输出
$ python3 -c "print(int(str(1534236469)[::-1]))"
9646324351

9646324351 大于 2147483647,所以最终答案是 0。这也是暴力解的思路:不检查过程,直接用大整数算完再判断范围。但题目要求的做法是过程中始终不超出 32 位,所以各种语言的实现都要在"乘 10 加个位"之前就判断会不会溢出。

五种语言的实现

思路:每次取出末位 d 和去掉末位的 x,在执行 rev * 10 + d 之前,判断它会不会越界。rev 越界的条件是:rev 已经大于 INT_MAX / 10,或者等于 INT_MAX / 10(即 214748364)且 d 大于 7;下界同理,d 小于 -8。Java、Go、C++、JavaScript 里 % 的结果符号跟随被除数,/ 向 0 取整,所以负数不需要特殊处理,直接对 x 做上面的循环;Python 的 % 和 // 向负无穷取整,行为不同,所以 Python 版本先记下符号,对绝对值做循环(Python 里取绝对值不会溢出)。JavaScript 的数字是双精度浮点数,没有 / 向 0 取整的整数除法,要显式用 Math.trunc,不能用 Math.floor。

Python · 解法代码def solve(x): INT_MAX = 2**31 - 1 sign = -1 if x < 0 else 1 x = abs(x) rev = 0 while x: x, d = divmod(x, 10) # 乘 10 之前先检查,rev 始终留在 32 位范围内,不依赖 Python 的大整数 if rev > INT_MAX // 10 or (rev == INT_MAX // 10 and d > (7 if sign > 0 else 8)): return 0 rev = rev * 10 + d return sign * rev
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[123]  →  321
[-123]  →  -321
[120]  →  21
[0]  →  0
[-10]  →  -1
[7]  →  7
Java · 解法代码class Solution { public int reverse(int x) { int rev = 0; while (x != 0) { int d = x % 10; // 余数的符号跟随被除数,负数得到负的个位 x /= 10; if (rev > Integer.MAX_VALUE / 10 || (rev == Integer.MAX_VALUE / 10 && d > 7)) return 0; if (rev < Integer.MIN_VALUE / 10 || (rev == Integer.MIN_VALUE / 10 && d < -8)) return 0; rev = rev * 10 + d; } return rev; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[123]  →  321
[-123]  →  -321
[120]  →  21
[0]  →  0
[-10]  →  -1
[7]  →  7
Go · 解法代码func reverse(x int) int { const maxInt32, minInt32 = 1<<31 - 1, -(1 << 31) rev := 0 for x != 0 { d := x % 10 // 余数的符号跟随被除数 x /= 10 if rev > maxInt32/10 || (rev == maxInt32/10 && d > 7) { return 0 } if rev < minInt32/10 || (rev == minInt32/10 && d < -8) { return 0 } rev = rev*10 + d } return rev }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[123]  →  321
[-123]  →  -321
[120]  →  21
[0]  →  0
[-10]  →  -1
[7]  →  7
JavaScript · 解法代码function reverse(x) { const MAX = 2 ** 31 - 1, MIN = -(2 ** 31); const maxQ = Math.trunc(MAX / 10), minQ = Math.trunc(MIN / 10); let rev = 0; while (x !== 0) { const d = x % 10; // 余数的符号跟随被除数 x = Math.trunc(x / 10); // 向 0 取整,不能用 Math.floor if (rev > maxQ || (rev === maxQ && d > 7)) return 0; if (rev < minQ || (rev === minQ && d < -8)) return 0; rev = rev * 10 + d; } return rev; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[123]  →  321
[-123]  →  -321
[120]  →  21
[0]  →  0
[-10]  →  -1
[7]  →  7
C++ · 解法代码#include <climits> class Solution { public: int reverse(int x) { int rev = 0; while (x != 0) { int d = x % 10; // C++11 起,商向 0 取整,余数符号跟随被除数 x /= 10; if (rev > INT_MAX / 10 || (rev == INT_MAX / 10 && d > 7)) return 0; if (rev < INT_MIN / 10 || (rev == INT_MIN / 10 && d < -8)) return 0; rev = rev * 10 + d; } return rev; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[123]  →  321
[-123]  →  -321
[120]  →  21
[0]  →  0
[-10]  →  -1
[7]  →  7

正确性验证:与暴力解逐组对比

五种语言都通过了同一批 15 组用例,并且每一组的输出都与暴力解的结果完全一致:

真实实测 · 本机执行输出
共 15 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 15/15
  Java        通过 15/15
  Go          通过 15/15
  JavaScript  通过 15/15
  C++         通过 15/15

Python 版本另外做了大规模随机对拍:

真实实测 · 本机执行输出
随机对拍 20000 组(一半是 32 位范围内均匀随机的整数,另一半是首位为 1 或 2 的 10 位数(正负各半),专门逼近翻转后溢出的边界),solve 与 brute 的结果全部一致。

另外四种语言只在这 15 组固定用例上验证过,没有做大规模随机对拍;15 组用例里包含了 32 位上下界和翻转后刚好在边界内外的输入,是针对上面溢出判断挑选的。

参考与说明

  • 官方题目页:LeetCode 7. 整数反转。本文只用自己的话概括了题意,完整约束以官方页面为准。
  • 五种语言的代码都是真实编译、运行得到的输出,编译和运行命令写在每个标签页的输出块里。运行时由一个统一的测试入口(本文没有展示,只做读取用例、调用解法、打印结果)逐行读取用例,所以标签页里展示的只有解法本身。
  • 中间我在给 C++ 搭测试入口时,先用了在这台 macOS 的 clang 上不存在的头文件 bits/stdc++.h,编译报错,改成显式包含标准头文件后才通过,这不影响解法代码。
  • 没有覆盖:把运算改用 64 位整数再判断范围的做法(题目声明不允许,本文不展开);x = 0 之外输入的负零处理在 JavaScript 里表现为 0(-10 的用例里已经覆盖,结果与其他语言一致)。
京ICP备2026067502号-1京公网安备11010502063487号