如何反转一个 32 位整数并处理溢出?(LeetCode 7)
简化版
把一个 32 位有符号整数的数字部分反转(123 → 321,-123 → -321,120 → 21)。若反转后超出 int 范围 [-2³¹, 2³¹-1] 就返回 0。做法是不断 % 10 取末位、/ 10 去末位,把末位累加到结果 res = res * 10 + digit。核心难点是溢出判断:必须在 res * 10 + digit 之前判断会不会越界(因为溢出后就没法判了),拿 res 和 Integer.MAX_VALUE / 10 比较。
详细版
int reverse(int x) {
int res = 0;
while (x != 0) {
int digit = x % 10; // 取末位(对负数,Java 的 % 保留符号,如 -123%10=-3)
x /= 10; // 去末位
// 溢出判断:累加前先判(正负边界都要考虑)
if (res > Integer.MAX_VALUE / 10 || (res == Integer.MAX_VALUE / 10 && digit > 7))
return 0;
if (res < Integer.MIN_VALUE / 10 || (res == Integer.MIN_VALUE / 10 && digit < -8))
return 0;
res = res * 10 + digit;
}
return res;
}
- 取末位累加:
res = res * 10 + (x % 10),逐位把 x 的末尾搬到 res 的高位。 - 负数处理:Java 的
%对负数保留符号(-123 % 10 = -3),所以负数无需特殊转换,累加天然带符号。 - 溢出判断在累加前:
res接近MAX/10(214748364)时,再看下一位是否让它越过2147483647/-2147483648。 - 复杂度:O(log x)(位数)。
完整版教学
一、基本反转:取末位、搬高位
反转整数的核心操作循环三步:
- 取末位:
digit = x % 10。 - 去末位:
x /= 10。 - 累加到结果:
res = res * 10 + digit——res每次先 ×10 腾出个位,把 x 的当前末位接上去。
这样 x 的末位第一个被取出、经过多次 ×10 被推到 res 的最高位;x 的最高位最后取出、留在 res 的个位——恰好反转。120 反转:末位 0→res=0,2→res=2,1→res=21,自然去掉了前导零。
二、负数为什么不用特殊处理
Java(和 C++)的取模 % 对负数保留被除数的符号:-123 % 10 = -3(不是 7)。所以对 -123:
-123 % 10 = -3,res = -3;-12 % 10 = -2,res = -32;-1 % 10 = -1,res = -321。
结果 -321 自带负号,不需要先取绝对值再补符号。这是 Java/C++ 取模符号规则带来的便利(Python 的 % 结果符号跟除数,需注意差异)。
三、溢出判断:必须在累加前(核心难点)
反转后的值可能超出 int 范围(如 1534236469 反转成 9646324351 超过 2³¹-1)。而 res = res * 10 + digit 一旦溢出,res 就变成错误值,事后无法判断。所以必须在乘加之前预判:
- 正溢出:
Integer.MAX_VALUE = 2147483647,MAX/10 = 214748364。若res > 214748364,则res*10必超;若res == 214748364且digit > 7(MAX 末位是 7),也超。→ 返回 0。 - 负溢出:
Integer.MIN_VALUE = -2147483648,MIN/10 = -214748364。若res < -214748364,或res == -214748364且digit < -8(MIN 末位是 -8),也越界。→ 返回 0。
这个「和 MAX/10、MIN/10 比较,边界再看末位」的技巧是所有「整数累加防溢出」题的通用模板(atoi 也是)。
四、用 long 简化(若允许)
如果语言/题目允许用更大的类型,可以用 long 累加,最后判断是否超出 int 范围:
long res = 0;
while (x != 0) { res = res * 10 + x % 10; x /= 10; }
return (res < Integer.MIN_VALUE || res > Integer.MAX_VALUE) ? 0 : (int) res;
简单很多。但本题经典要求「只用 32 位」(不借助 long),那就得用上一节的「提前判断」写法。面试常追问「不用 long 怎么做」,所以两种都要会。
五、易错点
- 事后判溢出:最经典错误。溢出后 res 已错,判断无意义,必须提前判。
- 忘记负溢出边界:MAX 末位 7、MIN 末位 8(绝对值),两个边界的末位不同,别只判正的。
- 前导零:反转
100得1(res累加天然去零),不用特殊处理。 Integer.MIN_VALUE本身:-2147483648取绝对值会溢出,所以别用「先取绝对值」的思路,直接用带符号的%累加最稳。
六、在临界值上验证预判公式
预判把不等式 MIN <= res*10+digit <= MAX 改写为对 res 和最后一位的比较,避免危险乘法先发生。正边界前缀是 214748364,只有 digit≤7 安全;负边界前缀是 -214748364,只有 digit≥-8 安全。
x=1463847412 反转目标 2147483641
到最后一步前 res=214748364, digit=1
res==MAX/10 且 1<=7 -> 安全
若 digit=8,则 2147483648>MAX -> 返回 0
负侧 res=-214748364, digit=-8 仍等于 MIN
digit=-9 则小于 MIN -> 返回 0
判断必须发生在乘 10 之前
| 校验维度 | 本题必须保持的结论 |
|---|---|
| 循环/递推不变量 | 每轮累加前 res 在 int 范围内;通过预判后本轮乘加仍在 int 范围内。 |
| 边界条件 | Integer.MIN_VALUE 不能取绝对值;直接使用带符号 %10 可统一处理正负。 |
| 复杂度与代价 | 每轮去掉一个十进制位,O(log |
| 自检方式 | 用最小输入、典型输入和触及边界的输入分别手推,结果应与定义一致。 |
这组推演的作用不是替代证明,而是把抽象规则落到每一步状态变化上。面试写完代码后,应主动指出不变量如何保证不漏解,以及边界为何不会越界或死循环。若手推结果与定义不一致,应先修正状态语义,而不是继续给代码打补丁。
七、面试现场如何验证这道题
数学题不能只凭样例相信公式,必须同时核对定义域、推导条件和定宽整数边界。本题应先复述这条不变量:每轮累加前 res 在 int 范围内;通过预判后本轮乘加仍在 int 范围内。只有代码的初始化、更新顺序和终止条件都维持它,样例通过才具有证明力。
- 典型路径从“x=1463847412 反转目标 2147483641”开始手推,最后应得到“判断必须发生在乘 10 之前”。
- 边界复核:
Integer.MIN_VALUE不能取绝对值;直接使用带符号%10可统一处理正负。 - 代价复核:每轮去掉一个十进制位,O(log |x|) 时间 O(1) 空间。
- 用 0、1、最小合法值和最大合法值检查公式的定义域。
- 乘法、取绝对值或取负前先判断是否可能触及
Integer.MIN_VALUE等不对称边界。 - 若算法依赖单调性、抵消或整除关系,要明确题设保证何时成立、何时必须额外验证。
最后要主动构造一个“少写一次循环、改变一个不等号或交换两行更新就会失败”的反例。能说清反例破坏了哪条不变量,比只报出复杂度更能证明真正掌握了算法。
记忆钩子:本题的代码可以压缩,但“每轮累加前 res 在 int 范围内;通过预判后本轮乘加仍在 int 范围内。”这条正确性主线不能省。
八、常见误区与追问
- 误区:溢出后判断 res 是否越界仍有效。 int 已环绕成另一个值,原始越界信息丢失,必须事前判断。
- 误区:正负边界的末位阈值相同。 MAX 末位是 7,MIN 的绝对值末位是 8,范围不对称。
- 误区:先对 x 取绝对值最省事。
abs(Integer.MIN_VALUE)仍溢出,带符号逐位处理更安全。 - 追问:为什么 Java 负数取模能直接使用? Java 余数与被除数同号,所以
-123%10=-3,累加自然保持负号。 - 追问:120 为什么返回 21? 首先取出的 0 不改变 res,反转后的前导零在整数表示中自然消失。
- 追问:若允许 long 是否仍需预判? 可先用 long 累加再比较 int 范围,但面试常追问不借助更宽类型的写法。
九、加强记忆
整数反转 = res = res * 10 + x % 10,逐位搬末位到高位。Java/C++ 的 % 对负数保留符号(-123%10=-3),所以负数无需特殊处理、结果自带负号。核心难点溢出必须在累加前预判:res > MAX/10(214748364)或 == 且 digit>7 → 正溢出;res < MIN/10 或 == 且 digit<-8 → 负溢出,都返回 0。别事后判、别对 MIN_VALUE 取绝对值。若允许用 long 则累加后判范围更简单。核心:边搬边判,MAX/10 是警戒线。