ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

整数反转的溢出判断与边界处理:力扣第7题深度解析

整数反转的溢出判断与边界处理:力扣第7题深度解析

第一次刷到这道题的时候,我心想:不就是倒着输出数字吗?取模、除法、循环,三下五除二就能过。结果提交以后红了一片,一看报错才明白——真正的坑不在反转,在溢出。

力扣第7题“整数反转”,是一道典型看着简单、实则暗藏杀机的题。它表面上考的是拆位和重组,实际上考的是你对32位有符号整数边界条件的理解。今天把这题从头到尾拆一遍,包括推导过程、C++代码、我踩过的边界坑,以及面试时尽量避开的一个隐藏雷区,希望能帮你一次过。

1. 题目其实只考了三件事

1.1 原题到底说了什么

题目本身非常短:给你一个32位的有符号整数 x,把它的数字部分反转。如果反转后的结果超出了32位有符号整数的范围,也就是 [-2147483648, 2147483647],就返回 0。

示例就这么几个:

  • 输入 123,输出 321;
  • 输入 -123,输出 -321;
  • 输入 120,输出 21;
  • 输入 0,输出 0。

注意题目通常还会加一句:假设环境不允许存储64位整数。这句话很多人直接忽略,但它其实是在明示——你别想用 long long 绕过去,老老实实做溢出判断。

1.2 三个考点逐条拆开看

这题真正要你掌握的东西有三层:

第一层是拆位。能不能把一个整数的每一位单独取出来,这是基本功。取个位用x % 10,去掉个位用x / 10,两个操作交替进行,就能从低位到高位把数字抠出来。

第二层是重组。把拆出来的数字按相反顺序拼回去,核心公式就是ans = ans * 10 + pop。这里很多人一上来就能写出来,但未必想清楚这个公式为什么成立。后面我会专门推导。

第三层是溢出判断。这是区分“刷过”和“真懂”的分水岭。反转的过程中,ans * 10会越来越大,一不小心就超出 INT_MAX 或者跌破 INT_MIN。题目要求这时候返回 0,那你必须在乘法发生之前提前发现危险,而不是等溢出了再用一个更大的类型去接。

这三层对应了三种不同的能力:手熟程度、对数学公式的理解、对边界条件的敏感度。网上很多人说这题是简单题,我觉得它配得上“简单”的标签,但绝不是一个能闭着眼睛秒掉的题。

2. 核心解法:拆位与重组的数学原理

2.1 拆位的第一步:取模和整除怎么配合

先看拆位。以 123 为例:

int x = 123; int pop = x % 10; // 3,拿到个位 x = x / 10; // 12,去掉个位

再来一轮,12 % 10 = 2,12 / 10 = 1;最后一轮,1 % 10 = 1,1 / 10 = 0。整个过程把 123 拆成了 3、2、1,正好是反转后需要的顺序。

这里有个很多新手没注意到的点:C++ 里负数取模也是能直接用的。(-123) % 10的结果是 -3,(-123) / 10的结果是 -12。也就是说,负数的拆位过程和正数完全一致,符号会自动保留在每一位上。你不需要单独判断正负,也不需要先把负号拎出来、转成正数再处理。

2.2 重组公式:ans = ans * 10 + pop 的由来

拆出来之后要拼回去。你可能会想:第一次拿到 3,那就是 3;第二次拿到 2,希望得到 32;第三次拿到 1,希望得到 321。

观察一下规律:32 是怎么从 3 变过来的?是3 * 10 + 2。321 又是怎么来的?是32 * 10 + 1。所以通项公式就出来了:

ans = ans * 10 + pop

初始时ans = 0,第一次循环变ans = 0 * 10 + 3 = 3,第二次ans = 3 * 10 + 2 = 32,第三次ans = 32 * 10 + 1 = 321。

这一步的数学本质是:ans每乘以一次 10,就是在原有的结果后面多腾出一位,再把新拆下来的数字塞进个位。顺序是反的,正好满足反转要求。而负数同理,(-3) * 10 + (-2) = -32,符号一直跟着走。

2.3 第一版能跑通的代码

把拆位和重组拼在一起,先写一个不考虑溢出的版本:

class Solution { public: int reverse(int x) { int ans = 0; while (x != 0) { int pop = x % 10; x = x / 10; ans = ans * 10 + pop; } return ans; } };

这个代码跑示例 123、-123、120 都能得到正确结果。但如果你直接提交,会有一部分用例超界返回错误答案,比如反转 1534236469 这类 10 位数,结果会溢出成一个怪异数字。

所以,控制住ans = ans * 10 + pop这一步,才是真正需要花功夫的地方。

3. 溢出判断是这题真正的分水岭

3.1 溢出发生的精确时机

先想清楚一个问题:溢出到底发生在哪一步?

看ans = ans * 10 + pop。如果ans已经很大了,乘以 10 之后结果可能直接超过 int 的表示范围。问题在于,C++ 中 int 溢出是未定义行为,程序会得到一个随机结果,而不是自动帮你截断或者抛异常。

举个例子,如果ans是 214748365,乘 10 就是 2147483650,已经大于 INT_MAX 的 2147483647。此时结果根本存不进 int 里。这就是为什么很多人“本地跑没问题,一提交就错了”——因为你本地测试的小数字碰不到这个阈值。

所以要做的,是在ans * 10真正发生之前,用判断挡住它。数学上,ans * 10 + pop不溢出的等价条件是:

ans <= (INT_MAX - pop) / 10

这个式子没问题,但每次都要做减法和除法,不够直观。更常用的写法是直接用INT_MAX / 10作为阈值,分成两种情况讨论。

3.2 两行判断条件的逐句推导

先看正数方向。INT_MAX = 2147483647,INT_MAX / 10 = 214748364。

第一种情况:ans > 214748364。这说明ans至少是 214748365,那ans * 10至少是 2147483650,已经超过 2147483647,直接溢出,不用看 pop 是几。

第二种情况:ans == 214748364。那ans * 10 = 2147483640,还差一点才到上限,这时候能不能加 pop,取决于 pop 的大小。2147483640 距离 2147483647 只剩 7,所以如果pop > 7,加完就超了,溢出;pop <= 7,安全。

所以正数方向的条件是:

if (ans > INT_MAX / 10 || (ans == INT_MAX / 10 && pop > 7)) { return 0; }

负数方向完全对称,只是边界不同。INT_MIN = -2147483648,INT_MIN / 10 = -214748364,注意商是负的。

第一种情况:ans < -214748364,那ans * 10肯定跌破下限。

第二种情况:ans == -214748364,此时ans * 10 = -2147483640,距离下限 -2147483648 还差 8。因为 pop 是负数,如果pop < -8,加完就低于下限;pop >= -8安全。

所以负数方向的条件是:

if (ans < INT_MIN / 10 || (ans == INT_MIN / 10 && pop < -8)) { return 0; }

注意这里的正负不对称:上限末位是 7,下限末位是 8。原因很简单,32 位有符号整数的范围是 -2147483648 到 2147483647,负数的绝对值比正数大 1。这是补码表示决定的,也是在反转负数时最容易漏掉的细节。

完整版代码如下:

class Solution { public: int reverse(int x) { int ans = 0; while (x != 0) { int pop = x % 10; x = x / 10; if (ans > INT_MAX / 10 || (ans == INT_MAX / 10 && pop > 7)) { return 0; } if (ans < INT_MIN / 10 || (ans == INT_MIN / 10 && pop < -8)) { return 0; } ans = ans * 10 + pop; } return ans; } };

3.3 C++ 里 INT_MAX 和 INT_MIN 怎么来

上面代码里直接用INT_MAX、INT_MIN,很多人看到会问:这几个宏哪来的?

它们是 C++ 标准库里的常量,定义在<climits>头文件中。在力扣的编译环境里,通常已经包含了必要的头文件,但你写本地工程或者面试手写代码时,最好自己显式包含,或者干脆写成字面量2147483647和-2147483648。

我个人更推荐写成字面量,原因有两个:一是面试时不用依赖头文件,白板上写代码极友好;二是字面量能把边界值直接摆出来,方便你和面试官一起推导。实际上你只要记住两句话:正数边界 2147483647,它除以 10 是 214748364;负数边界 -2147483648,它除以 10 是 -214748364。

4. 我实测踩过的四个边界坑

4.1 面对 INT_MIN 时,负数反转直接崩

我第一次提交用的就是第二节那个无溢出判断版本,报错用例是 x = -2147483648。

我当时很疑惑:这个数反转以后是 -8463847412,明显超界,按题目要求应该返回 0 才对,但代码返回了一堆莫名其妙的数字。原因就是上面说的,ans * 10在编译层面已经溢出,后续结果不可控。

后来我加上了负数边界判断,这个用例就过了。但要注意:INT_MIN反转时,第一步x % 10得到的是 -8,而不是 8;中间过程ans一直是负数。所以负数判断条件里的pop < -8,这个负号千万不要丢。很多人记住了INT_MAX的 7,却把INT_MIN的负 8 当成正 8,这就是典型的半桶水写法。

4.2 末尾带 0 的数字,反转后会丢 0 吗

输入 120,期望输出是 21,而不是 021。这个其实不用担心,题目要的是整数,021 写成整数就是 21,程序里ans = 0 * 10 + 0 = 0也已经把前导 0 消掉了。

但反过来,有人会担心:1200 反转后不是应该输出 21 吗?对,就是你猜的那样。这个用例本身没有问题。真正容易出问题的是你手动验证时,把 1200 反转后手动写成 0021,然后疑惑“程序怎么输出 21”——数字和字符串在概念上不同,这里不用纠结。

4.3 x = 0 以及正负不对称的细节

输入 0 直接返回 0,这个用例简单到不会有人错。但让我真正长记性的是正负不对称性。

我当时用x = 1463847412验证过,反转结果是 2147483641,正好比 INT_MAX 的 2147483647 小 6,所以能通过。我又想当然地认为,x = -1463847412的反转结果是 -2147483641,也应该没问题。结果发现负数侧更宽松——下限是 -2147483648,所以 -2147483641 确实合法,能通过,没问题。

但反过来,x = 1563847412反转后是 2147483651,超上限,返回 0;而x = -1563847412反转后是 -2147483651,也超下限,返回 0。正负两个方向都有各自的边界,必须分开判断,不能共用一个条件。这个“不对称”不是某个实现的问题,而是 int 类型本身的固有属性。

4.4 易错用例对照表

我把实际用來验证的典型用例整理成一张表,方便你直接抄去跑:

输入 x反转过程期望输出
1233 -> 32 -> 321321
-123-3 -> -32 -> -321-321
1200 -> 0 -> 2121
000
14638474122147483641,在最大值内2147483641
15638474122147483651,超最大值0
-1463847412-2147483641,在最小值内-2147483641
-1563847412-2147483651,超最小值0
-2147483648-8463847412,超最小值0

最后一次调试时我是把这张表的用例全部跑一遍才提交的。之后但凡遇到反转类、字符串反转类的题,我都习惯先列一张边界表,这个习惯后面帮我省了不少事。

5. 两种替代写法:能过,但我劝你别在面试里用

除了手写溢出判断,我还见过两种很常见的替代方案。它们都能通过平台测试,但更适合用来理解思路,而不是作为面试时的主答案。

5.1 long long 临时变量方案

第一种是开一个long long变量来装反转结果,最后判断是否超出 int 的边界,超出就返回 0,否则强转回 int。

class Solution { public: int reverse(int x) { long long ans = 0; while (x != 0) { ans = ans * 10 + x % 10; x = x / 10; } if (ans > INT_MAX || ans < INT_MIN) { return 0; } return (int)ans; } };

这个方案代码极短,逻辑也清晰,我在日常刷题时经常用它来快速验证想法。但是题目原题明确写了“假设环境不允许存储64位整数”,long long 方案等于变相破坏了这个假设,放在面试里会被面试官追问“如果环境真不支持64位怎么办”。所以它可以作为思路铺垫,但不要作为最终答案。

5.2 字符串反转方案

第二种是先转字符串,带负号反转,再用字符串转数字的函数拼回去:

class Solution { public: int reverse(int x) { string s = to_string(x); if (s[0] == '-') { reverse(s.begin() + 1, s.end()); long long ans = stoll(s); if (ans < INT_MIN) return 0; return (int)ans; } else { reverse(s.begin(), s.end()); long long ans = stoll(s); if (ans > INT_MAX) return 0; return (int)ans; } } };

这个方案能跑通,但性能是三个方案里最差的,因为涉及字符串分配、反转、解析,而且还依赖stoll能解析出 64 位整数。它唯一的价值是给你提供一种“用库函数解决问题”的思路,真用到工程里还要处理异常,得不偿失。

5.3 为什么面试官更想看你写手动判断

我个人建议在面试场合主写第三节的手动判断版本,因为它把你的溢出意识直接暴露给了面试官。

面试官想看的不是你会不会调库,而是你能不能在乘法发生之前预判危险。手动判断版本里那两行if,其实就是在展示你对 32 位整数边界的理解。就算写得啰嗦一点,也完全可以边写边解释:先解释INT_MAX / 10是阈值,再解释等号时为什么还要比较个位,最后解释正负不对称是因为下限绝对值比上限大 1。

这一套讲下来,比提交一个跑通的 long long 版本更有说服力。技术面试到最后拼的就是这种“能不能把边界讲明白”的功底。

6. 从第 7 题延伸出去的通用套路

6.1 不写一行多余额外逻辑,时间复杂度也是常数级

先看复杂度:每轮循环x会除以 10,32 位整数的位数最多是 10 位,所以最多循环 10 次。无论输入多大,循环次数都是常数,时间复杂度可以写成 O(log10 x),也可以直接说 O(1)。空间上只用了一个ans变量,常数级。

这也是为什么我不建议用字符串方案:同样是常数级复杂度,字符串方案凭空多出 O(1) 的临时空间,还要付出额外的常数时间。刷题阶段当然无所谓,但如果你是做嵌入式或者高性能计算相关的工作,这类习惯会影响你未来的代码品味。

6.2 同类题串联:回文数、大数拼接都能用这套思路

拆位加重组这个套路,在第 9 题“回文数”上可以直接复用。判断一个整数是不是回文数,最自然的做法就是反转一半,然后和另一半比较。

第 7 题是“完整反转 + 溢出判断”,第 9 题是“半反转 + 相等判断”。两者的核心公式都是ans = ans * 10 + pop,区别只是边界条件和结束时机。你只要把第 7 题理解透,第 9 题基本就是 10 分钟的事。

另外,反转的思路还可以延伸到更复杂的拼接题,比如两个数字拼接后判断是否溢出,或者把数字转成数组之后做区间处理。这些题本质上都是把“十进制表示的每一位”当作可操作的最小单位,而% 10和/ 10永远是拿到这个单位的钥匙。

6.3 一个让我少踩一半坑的小习惯

最后分享一个我刷完这题养成的小习惯:任何数字操作的题,先把边界值列出来再动手。

所谓边界值,就是 INT_MAX、INT_MIN、0、最高位附近的值、末尾带 0 的值。就第 7 题而言,我在提交前一定会把第四节那张表跑一遍。因为在大多数情况下,你的逻辑思路是对的,出问题的都是+1/-1、> 7/< -8这种差一点点的判断。先用极端值验证,能帮你把 80% 的边界 bug 提前杀掉。

这个习惯后来帮我在不少题上省了反复提交的尴尬。力扣第 7 题的价值不只在于让你学会反转一个整数,更在于它逼着你正视一个事实:整数的范围是一堵墙,所有操作都要在墙内完成。想清楚这一点,比记住这题的标准答案重要得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进