C++整数反转:从力扣第7题吃透int溢出与边界判断
发布时间:2026/10/22 15:44:43 锦皓数字建站

好久没写基础题笔记了。前几天帮一位同学调力扣第7题C的提交代码在他本地跑得好好的一提交就返回错误结果。他一脸纳闷题目不就是把数字倒过来吗怎么会错我让他把输入改成2147483647试一下他瞬间明白了——反转之后的7463847412根本装不进int。这道题在力扣里标记为medium但我一直觉得它实质上是“语言边界题”里最经典的一课题目本身不绕难的是你对32位整数、溢出行为和C取模规则的理解能不能一次到位。第7题到底考什么一句话概括给定一个32位有符号整数x返回数字反转后的结果如果反转结果超出[-2^31, 2^31-1]返回0。很多刚开始刷题的人把它当成普通数学题实际上它是最适合用来建立“边界条件思维”的题目之一。不管你是准备机试、面试手撕代码还是单纯想巩固C语言细节这道题都值得抽出半小时认真做一遍、复盘一遍。1. 题干拆解与第一反应里的坑1.1 题面到底说了什么题目给的信息很短核心就三点输入是32位有符号整数数字部分要反转反转后超出范围就返回0。这里有几个容易被忽略的细节第一“32位有符号整数”指的是int的常见形态。在力扣的C评测环境里int就是4字节、范围-2147483648到2147483647。这道题的所有隐藏用例基本就是在边界值附近反复试探。第二“反转数字部分”意味着符号不参与反转。负数反转后还是负数比如-123反转成-321。这一点大多数人不会搞错但很容易在处理绝对值时出问题后面我会专门分析INT_MIN这个最阴的用例。第三反转后“超过范围”要看整个结果而不是过程中的某一位。比如原数2147483647看似在范围内但它反转后是7463847412明显溢出再比如原数-2147483412反转后是-2143847412虽然在范围内可以输出但很多人在算到这一步时不确定该不该判溢出。这个时候最好的办法是先准备好两个精确的端点值然后用数学式判断不要凭感觉。1.2 多数人的第一版代码错在哪如果你把题目丢给一个还没踩过坑的新手他大概率会写这么一段int reverse(int x) { int ans 0; while (x ! 0) { ans ans * 10 x % 10; x / 10; } return ans; }这段代码对123、-123这种常规输入没问题但提交后会有一批用例失败。原因很简单ans ans * 10 x % 10在极端情况下会让ans先溢出而C的规定是signed整数溢出属于未定义行为。所谓未定义行为意思是编译器可以“合理地”做任何事有的机器上你得到的是截断后的怪值有的编译器优化后连判断逻辑都会变得不可预测。这就是本地运行正常、在线评测出错的最常见原因。所以第7题的本质不是在循环体里多写一行判断而是要理解溢出不是算完之后才能发现的意外而是每次拼接前就必须提前拦截的状态。这也是面试官最希望从你嘴里听到的理解。1.3 一个容易被忽略的大前提题目要求返回值是int。这意味着你不能用long long接住结果再判断long long是否超出int范围然后返回int。虽然实际提交时用long long也能AC很多题解就是这么写的但这属于“逃课”解法。严格来说题目考的是“在int范围内完成运算并正确识别溢出”如果你拿long long中转就绕开了这个考点。面试时一旦被追问“如果不允许用long long呢”你就得现场换思路很容易慌。基于上面几点我们在学这道题时目标应该定为全程只用int在每次拼接前完成溢出判断写出一版不依赖任何辅助类型、不依赖未定义行为的正统解答。2. 三种主流解法的横向对比2.1 数学法逐位拆解与拼接数学法的核心是两行操作x % 10取出当前最低位x / 10去掉最低位然后ans ans * 10 digit把取出的位拼接到结果的末尾。整个过程就像手工列竖式一样从个位开始一位一位往前翻。这个方法的好处是空间复杂度为O(1)不依赖任何容器完全贴合题目隐含的“不使用辅助空间”的要求。缺点是对溢出判断的要求很高因为ans会逼近int边界你必须把边界条件想清楚。很多教材喜欢把溢出判断写成“if (ans INT_MAX / 10) return 0;”这个式子的意思很直观如果ans已经大于214748364那么下一步不管digit是多少ans * 10 digit都必然超过2147483647。反过来如果ans小于-214748364也必然低于-2147483648。只有在ans恰好等于214748364或-214748364时才需要看digit的具体大小做二次判断。这个逻辑就是第7题真正的考点。2.2 字符串法能过但没必要字符串法也很容易想到把整数转成字符串去掉负号反转字符串再用parse函数转回整数最后判断范围。C11之后标准库提供了std::to_string和std::stoll写起来大概十几行string s to_string(x); if (s[0] -) reverse(s.begin() 1, s.end()); else reverse(s.begin(), s.end()); long long val stoll(s); if (val INT_MAX || val INT_MIN) return 0; return (int)val;这段代码能过力扣但有两个明显问题第一它引入了字符串辅助空间时间复杂度里多了一个O(n)的字符串操作常数空间复杂度也变成了O(n)第二它依赖stoll这个64位解析函数如果题目进一步限定“不准使用64位类型”这段代码就得推翻重写。所以字符串法适合作为思路补充不适合作为这道题的标准答案。面试官如果看到你用字符串大概率会追问一句“能不能在O(1)空间下完成”到那时再回头写数学法时间会非常紧张。2.3 64位类型法省心但不保险用long long中转是最省心的写法long long ans 0; while (x ! 0) { ans ans * 10 x % 10; x / 10; } if (ans INT_MAX || ans INT_MIN) return 0; return (int)ans;这段代码在力扣上也能AC因为64位范围足够大即使x是INT_MAX反转后的7463847412也在long long范围内。但就像前面说的它绕开了“32位运算时如何识别溢出”这一核心问题。在真实工程里这种“先扩大容器再判断”的思路也很常见——比如金融计算里用更大的十进制类型来避免精度问题。但面试刷题场景下题目暗示的考点是“int自反不能依赖更大类型”所以强烈建议掌握纯int写法。2.4 为什么最终选择数学法作为主线把三种方法放在一起对比数学法的优势在于一是空间最优符合题目精神二是写出溢出判断后你能真正理解int的存储边界三是一旦理解面试追问“改成long long输入怎么办”“改成任意位数怎么办”都能举一反三。字符串法和64位法是很好的辅助思路但主线一定要选数学法。方案时间复杂度空间复杂度是否依赖64位类型面试友好度数学法O(log10(x))O(1)否高字符串法O(n)O(n)依赖stoll低64位类型法O(log10(x))O(1)是中3. 溢出检测的原理与C代码实现3.1 为什么阈值是INT_MAX / 10很多人背下了“判断ans INT_MAX / 10”但不知道这个数是怎么来的。其实特别简单int能表示的最大值是2147483647它除以10等于214748364余数是7。如果我们正在拼接一个新数字那么下一步执行的运算是ans ans * 10 digit。为了让结果不超过2147483647必须满足ans * 10 digit小于等于2147483647。当ans等于214748364时ans * 10等于2147483640这时digit如果取8结果就是2147483648超过了int的上界所以必须进一步限制digit不能超过7。这个7就是INT_MAX % 10。负数的道理完全对称int的下界是-2147483648INT_MIN / 10等于-214748364INT_MIN % 10等于-8。当ans等于-214748364时digit如果取-9结果-2147483649低于下界溢出。所以我们的判断条件是digit不小于-8。用INT_MAX % 10和INT_MIN % 10代替魔法值7和-8代码的可移植性更强。如果哪天int位数变了或者你想把这个逻辑套用到long long类型上只需要替换宏逻辑本身一行都不用改。3.2 C取模运算的符号规则C里x % 10的符号和被除数x一致。也就是说-123 % 10的结果是-3而不是3。很多从数学习惯走出来的同学第一次会不适应以为余数一定是正数。实际上C11标准规定对于整数除法结果向零截断余数的符号与被除数相同。这意味着我们提取负数的每一位时取得的是负的digit。这反而是好事因为负数反转后还是负数直接用digit拼接不需要额外处理符号。举几个实际例子(-123) % 10 -3(-123) / 10 -12(-12) % 10 -2(-12) / 10 -1(-1) % 10 -1(-1) / 10 0整个循环里x从-123一路变成-3、0ans则由0拼出-3、-32、-321完美符合预期。所以数学法天然兼容负数不需要对x求绝对值。3.3 完整代码实现下面是推荐的标准解答记得引入头文件 class Solution { public: int reverse(int x) { int ans 0; while (x ! 0) { int digit x % 10; // 正数溢出检测 if (ans INT_MAX / 10 || (ans INT_MAX / 10 digit INT_MAX % 10)) { return 0; } // 负数溢出检测 if (ans INT_MIN / 10 || (ans INT_MIN / 10 digit INT_MIN % 10)) { return 0; } ans ans * 10 digit; x / 10; } return ans; } };现在逐行解释。digit x % 10取出当前最低位两个if是溢出防线ans ans * 10 digit执行拼接x / 10消掉已经处理的那一位。循环到x为0结束。有人会问为什么正数和负数要分开判断能不能统一本质上可以但因为C中整数除法向零取整负数边界的最小值绝对值比最大值大1导致INT_MIN / 10等于-214748364而INT_MAX / 10等于214748364二者数值上不一样所以分两行判断更清晰。面试时能写出分段判断本身就是对边界理解到位的体现。3.4 复杂度分析与一版保底写法时间复杂度是O(log10(x))也就是x的十进制位数。x最多10位因为int范围不超2147483647所以循环最多跑10次。空间复杂度O(1)只用了ans和digit两个int变量。如果考试时实在紧张可以先用long long版本快速拿到分也就是前面2.3节那段代码。但我依然建议日常练习老老实实写纯int版本把它练成肌肉记忆。一旦纯int版本写熟long long版本就是注释掉两个if的区别反过来可不行。4. 边界测试用例与常见翻车现场4.1 必测的边界用例清单刷这道题不能只看示例。示例只有123、-123、120这么几个覆盖不了边界。我每次讲这道题都会列一张测试清单输入输出原因说明00特殊情况循环不进ans为0123321常规正数-123-321常规负数12021末尾0反转后消失21474836470反转后7463847412溢出-21474836480反转后-8463847412溢出21474836411463847412反转后不溢出正常输出-2147483412-2143847412接近边界的合法负数反转15342364690隐藏用例反转后9646324351溢出10000000030反转后3000000001溢出最后几个用例特别值得留意。2147483641反转后是1463847412它刚好在int范围内而-2147483412反转后是-2143847412也刚好在范围之内。这两个用例是用来验证你的判断没有“一刀切”地把边界附近的值全部判成溢出。很多人写完溢出判断后用前几个边界用例测完就提交结果被这种隐藏用例击中就是因为他只验证了“必然溢出”的情况没有验证“临界不溢出”的情况。4.2 经典错误先乘后判前面提过的先乘后判这里展开讲。初版代码最常见的是if (ans * 10 digit INT_MAX) return 0;表面看是在拼接前检查结果但问题在于表达式求值时ans * 10 digit已经以int类型完成了运算。如果它溢出结果是未定义行为。在栈上调试时可能侥幸得到截断值但启用了优化的评测环境下编译器可能基于“int永远不会溢出”的假设进行优化结果你连判断逻辑都找不到。这就是“本地能跑、线上报错”的根源之一。正确做法是移动不等式不直接算ans * 10 digit而是比较ans和INT_MAX / 10。这相当于把乘法转化为除法把“乘积的结果是否越界”转化为“乘积的因子是否越界”。在编程中这叫“用等价变换规避中间溢出”是一个值得记住的通用技巧。4.3 INT_MIN的出场机会另一个高频翻车点是INT_MIN。有人为了统一处理正负数先int sign x 0 ? -1 : 1再用int n abs(x)。但INT_MIN的绝对值2147483648超过int范围abs(INT_MIN)在C里同样属于未定义行为。实际上很多库返回的仍是INT_MIN于是后面循环里n先减成了负数整个拼接逻辑全乱。处理负数的正确方式就是前文说的不取绝对值直接利用C余数与被除数同号的性质让digit保持负数参与拼接。这也是这道题对语言了解程度的一种考察。如果你非要用取绝对值的方法那至少要把n的类型改成long longlong long n llabs((long long)x)。这能解决问题但同样绕开了语言细节。4.4 调试时的一些实测心得本地调试时我通常会在循环开头打印ans、digit、x三个变量观察拼接过程。打印一轮后你会发现溢出判断发生的时机往往在最后一次拼接前而不是拼接后。这个观察有助于建立“提前判断”的直觉。另外建议写一个小脚本把上面表格里的用例一次性跑完。不要只测一两个用例就提交。刷算法题时测试用例设计的完备性本身也是一项能力第7题是练习这个能力的好素材。5. 面试追问与扩展思考5.1 不允许用乘除法怎么办这是一个比较常见的追问变体。题目会变成“不使用乘法和除法实现整数反转”。思路可以换成用循环做累加每次拼接时用digit加10次来替换乘以10再把x除以10换成循环减10来求商。虽然效率低了但展示了你对乘除法本质的理解。如果面试只是为了考语言基础通常不会真的要求你写这种低效版本更多是看你能不能想到替代方案。5.2 输入从int扩展到long long如果输入是64位有符号整数思路完全一样只是把INT_MAX和INT_MIN换成LLONG_MAX和LLONG_MIN。代码里的判断阈值变成LLONG_MAX / 10和LLONG_MIN / 10。这个迁移证明了数学法的通用性。反过来如果用字符串法改成long long之后也只是替换解析函数并不复杂但数学法改动更少逻辑更完整。5.3 与回文数、atoi的关联第7题的溢出判断思路在回文数这类题目中也有体现——判断回文时可以只反转后半段再和前半段比较核心仍然是数字拆解与拼接。而在“字符串转换整数(atoi)”这类题目中溢出判断同样要用到与INT_MAX / 10比较的技巧。也就是说你今天在第7题里练熟的这个阈值判断后面会在至少两三道题里继续见到。这也是为什么我一直建议初学者不要背题解而要理解推导。5.4 一些长期有用的编码习惯写第7题时我还会顺带练习几个通用习惯第一使用INT_MAX、INT_MIN而非硬编码2147483647因为宏定义的含义更清楚也方便移植第二把溢出判断写成注释块增强可读性第三在提交前用测试清单验证“临界可溢出”和“临界不可溢出”两类用例。这些习惯看起来很小但在实际代码评审中恰恰是别人愿意跟你合作的理由。最后再分享一个我自己的小习惯。每次刷完这类边界题我会故意把代码重写成最糟糕的版本比如先乘后判、用abs处理负数再手动走一遍边界用例看它怎么崩溃。这个过程看起来浪费时间但能帮我把“为什么会溢出”“为什么提前判断”这些点记得非常牢。第7题值得你反复写三遍第一遍按直觉写第二遍按正确思路写第三遍什么都不看在白板上默写并讲清每一行的理由。等你做到第三遍面试官再问起整数反转你就不是背答案而是真的懂了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。