LeetCode 第 9 题,回文数(Palindrome Number)。这题我每次带人入门刷题都会拎出来讲一遍,因为它太适合用来理解“整数处理”和“边界条件”了。很多新手拿到题目第一反应是转字符串再反转,这当然能过,但今天想聊的是更贴合题目本意的两种数字解法:完整反转法和反转后半部分法。如果你希望以后做整数类题目时不只靠拼 API,或者面试时被问到“能不能不用字符串解决”,这篇文章应该能帮你把思路彻底理清楚。
题目本身不难,难的是把每个细节都处理干净。比如负数算不算回文?末尾是 0 的数要注意什么?反转一半的循环到底什么时候停?这些点一旦想明白,以后遇到 LeetCode 7(整数反转)、LeetCode 234(回文链表)这类题目,你都会觉得顺畅很多。下面我按自己实际做题时的思考过程来拆解。
1. 题目复盘:回文数到底在考什么
1.1 题目描述和样例信号
题目要求很简单:给你一个整数 x,如果 x 是一个回文整数,返回 true,否则返回 false。回文整数指的是正序(从左向右)和倒序(从右向左)读都是一样的整数,比如 121、1221、12321 都是回文数。
样例通常给这么几个:
- 输入 121,输出 true
- 输入 -121,输出 false,因为从左到右是 -121,从右到左是 121-
- 输入 10,输出 false,因为从右到左是 01,和原数不一致
这几个样例其实已经把最关键的两个边界信号放出来了。负号是一个很明显的坑,它提醒你回文判定涉及的是十进制数字序列,符号会破坏对称性。而 10 这个样例则更隐晦:它末尾带 0,反转后首位会变成 0,这在十进制整数表示里是不可能存在的,所以除了 0 本身以外,所有末尾为 0 的整数都不可能是回文数。
我在实际做题时习惯先把这些隐藏信息抽出来,再想解法。因为 LeetCode 的样例往往不止是让你验证算法正确性,还在暗示你哪些边界情况会被判 false,哪些测试用例隐藏在后台等着你漏掉。
1.2 为什么说这题本质是数学题而非字符串题
如果允许转字符串,这题一行就能写完:return str(x) == str(x)[::-1]。这个写法在 LeetCode 上能通过,代码也最简单。但你可以留意一下:很多公司面试官并不反感你用字符串法,可如果你想展示自己对整数底层运算的熟悉程度,就最好能给出不用字符串的解法。
原因在于,整数的十进制表示本身就是数字不断除以 10 取余数的过程。要判断回文,本质上是在对比“最高位数字”和“最低位数字”是否一致,然后逐层往里收缩。这种对比如果用取模 % 和整除 // 来实现,你就是在用数学运算直接操作数字,整个过程不依赖额外空间去复制字符串,也不存在字符串转换带来的隐式开销。
另外,转字符串的方案虽然简单,但在其他语言里可能会隐藏一些细节。比如负数转字符串后带负号,你需要额外判断;或者在某些语言里整数转字符串后反转会产生前导零问题。用纯数字解法,反而可以把这些语言相关的坑全部避开,逻辑是通用的。这也是我在本地用 Python 练习时始终坚持手写数字逻辑的原因。
1.3 拿到题先想清楚三类边界
做这道题我建议先理出三类边界,再开始写代码:
- 负数:只要 x 小于 0,直接 false,不需要进入后续逻辑。因为负号永远不会对称。
- 末尾为 0 且 x 不为 0:比如 10、100、1230。这类数的最高位不可能是 0,但反转后最低位会变成 0,所以不可能回文。
- 0 本身:0 是个特殊情况,它既满足负数检查,也不落入“末尾 0”的坑里,应该返回 true。
当你把这三类边界在纸上写清楚之后,整个解法的主干其实就已经清晰了,后面就是选一种反转策略去实现。很多同学一上来就写主逻辑,结果边界漏了,反复提交反复红,其实先花半分钟列边界,比多提交十次更省时间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解法一:完整反转法,最直观的“把数字倒过来”
2.1 核心思路与第一版代码
完整反转法的思路非常直白:既然回文数正着读和倒着读一样,那我就把整个输入整数反转成一个新整数,然后比较两者是否相等。比如 x = 12321,反转后得到 12321,相等,返回 true;x = 12345,反转后得到 54321,不相等,返回 false。
要在 Python 里反转一个整数,不需要字符串,我们只需要循环取末位数字,然后不断累加。具体操作是:每次用 x % 10 取出当前数字的最后一位,把它追加到反转结果后面,再通过 x //= 10 把原数字的最后一位去掉。重复这个过程,直到原数字变成 0。
python复制def isPalindrome_v1(x: int) -> bool:
if x < 0 or (x % 10 == 0 and x != 0):
return False
reverted = 0
temp = x
while temp > 0:
reverted = reverted * 10 + temp % 10
temp //= 10
return reverted == x
这里有一个非常重要的细节:我定义了一个 temp 变量来保存 x 的副本,而不是直接修改 x。因为循环结束后还需要拿反转结果和原始 x 做比较,如果直接在 while x > 0 里修改 x,循环结束 x 会变成 0,那就没法比了。这个坑我见过很多初学 Python 的朋友踩,他们写完循环发现返回值永远是 False,问题就出在原始数据被自己改掉了。
2.2 为什么 Python 不需要担心溢出问题
在 Java 或 C++ 里写这个解法,一个绕不开的问题是整数溢出。比如 C++ 的 int 只有 32 位,反转一个接近 INT_MAX 的数字,可能中间结果直接越界。LeetCode 第 7 题“整数反转”就是专门考这个的,需要在循环里加一句判断:如果 reverted > INT_MAX / 10 就直接返回 0。
但 Python 的整数是任意精度的,理论上你想加多少位就加多少位,不会因为超出 32 位或 64 位范围而报错。这既是 Python 写算法题舒服的地方,也带来一个小隐患:如果你把完整反转法搬到 C++ 或 Java 里,就一定要考虑溢出保护。
我在实际面试中通常先写 Python 版本,然后主动和面试官提一句“Python 整数不溢出,所以这里不需要额外判断,但如果换到 C++/Java,我会在循环里加一个溢出检查”。这样既展示了语言敏感度,也体现了工程意识。算法题不只要 AC,更要让看代码的人觉得你考虑周全。
2.3 完整反转法的时间和空间复杂度
完整反转法的时间复杂度是 O(log₁₀ n)。为什么不是 O(n)?因为输入整数 n 的位数约等于 log₁₀ n,循环次数和位数成正比,也就是和数字的十进制位数成正比。对于 32 位整数来说,最多循环 10 次左右,非常快。
空间复杂度方面,额外只用了 reverted 和 temp 两个变量,不随输入规模增长,所以是 O(1)。这个复杂度在 LeetCode 上表现已经很好了,但整体反转仍然有一个隐藏浪费:我们其实只需要反转一半就能判断回文,后半段的计算是冗余的。
这就像判断一个字符串是不是回文,你可以从头遍历到尾,也可以左右各放一个指针往中间走,走到中间相遇就停下来。显然双指针省了一半的遍历。反转后半部分法的优化思路正是如此。
3. 解法二:反转后半部分法,把循环次数砍半
3.1 为什么只反转一半就够
想一下回文数的结构:1221,前半部分是 12,后半部分是 21;但后半部分反转过来正好是 12,和前半部分相等。所以判断回文不一定要把整个数字反转完,只要把数字的后半部分反转出来,然后和前半部分比较即可。
难点在于:“后半部分”的边界在哪?我们不知道这个数字一共有多少位,没法预先知道什么时候算“处理到一半”。但我们可以换一个判断标准:在循环中不断把原数字的最后一位取出、加到 reverted 变量上,同时把原数字缩短一位。当原数字已经小于等于 reverted 时,说明我们已经处理到数字的中间位置甚至超过了中间位置。
用一个具体例子走一遍会更清楚。x = 1221:
- 初始:x = 1221,reverted = 0
- 第一步:1221 > 0,取出末位 1,reverted = 0 * 10 + 1 = 1,x = 122
- 第二步:122 > 1,取出末位 2,reverted = 1 * 10 + 2 = 12,x = 12
- 第三步:12 > 12 不成立,循环停止
此时 reverted = 12,x = 12,正好相等,说明是回文数。整个过程只循环了两次,而完整反转法需要循环四次。
3.2 奇偶位数分开处理的原因
如果数字位数是偶数,比如 1221,循环结束时 x 和 reverted 长度相同,直接比较 x == reverted 就能得到答案。
但如果是奇数位,比如 12321,处理过程会不一样:
- 初始:x = 12321,reverted = 0
- 第一步:x = 1232,reverted = 1
- 第二步:x = 123,reverted = 12
- 第三步:x = 12,reverted = 123
- 第四步:12 > 123 不成立,循环停止
此时 reverted = 123,x = 12。由于原数字位数是奇数,中间那位数字 3 被放到了 reverted 的末尾。判断时需要把 reverted 的末位去掉再和 x 比较,也就是 reverted // 10 == x。12 == 12,返回 true。
所以最终判断条件是:
python复制return x == reverted or x == reverted // 10
第一次写的时候容易把奇数情况漏掉,只写成 x == reverted,结果 12321 这种测试用例就过不了。我自己的习惯是每次写完都拿一个奇位数样例和一个偶位数样例分别跑一遍,比如 12321 和 1221。
3.3 完整代码与边界处理
python复制def isPalindrome_v2(x: int) -> bool:
# 负数和末尾为0但自身不为0的数,直接排除
if x < 0 or (x % 10 == 0 and x != 0):
return False
reverted = 0
while x > reverted:
reverted = reverted * 10 + x % 10
x //= 10
return x == reverted or x == reverted // 10
这里有个和完整反转法不同的细节:我们是在循环里直接修改 x,而且不需要暂存副本。因为最后就要拿处理到一半的 x 和 reverted 比较,x 被改掉反而是我们想要的。这个设计很巧妙,很多第一次接触这个解法的人会不太适应,担心 x 被破坏,但只要理解“前半部分留在 x 里,后半部分被翻转到 reverted 里”,就不会觉得奇怪了。
另外,末尾为 0 的排除条件在这里格外重要。假设 x = 10,如果不排除,循环会怎么走?
- 初始 x = 10,reverted = 0
- 10 > 0,取出末位 0,reverted = 0,x = 1
- 1 > 0,取出末位 1,reverted = 1,x = 0
- 循环停止,此时 x = 0,reverted = 1,判断
0 == 1 or 0 == 0,竟然返回 true
这显然是错的。10 不是回文数,却因为末位是 0 导致了错误判断。原因就是 x 变成 0 之后触发了 x == reverted // 10 这个条件。所以在循环之前必须先排除所有“末位为 0 且非 0 的数”,这一行条件虽然简短,却是整个解法的安全阀。
3.4 拿生活例子理解“反转一半”
我经常拿切卷心菜来打比方:判断一整棵卷心菜里面坏没坏,你不需要切片切到底,切到中间发现两面结构对称,基本就能判断了。反转一半的解法就是这样,它利用回文数的对称性,把循环次数减少了一半。
这种“双指针在中间相遇”的思想在算法里非常常见。数组题里的左右指针往中间走,链表题里的快慢指针找中点,字符串题里的前后对比,本质上都在干同一件事——利用对称性减少无用遍历。所以刷这道题不只是学一个 isPalindrome 的实现,更是在积累一种通用的优化意识。
4. 本地调试与测试用例设计
4.1 用 assert 快速验证
每次写完算法,我会习惯性地在本地跑一遍测试用例,而不是直接贴到 LeetCode 上等判题。Python 里最简单的方式就是用 assert 写断言,测不过会直接抛异常,测过则什么都不输出,非常干净。
python复制# 测试普通用例
assert isPalindrome_v1(121) is True
assert isPalindrome_v1(1221) is True
assert isPalindrome_v1(12321) is True
assert isPalindrome_v1(-121) is False
assert isPalindrome_v1(10) is False
assert isPalindrome_v2(121) is True
assert isPalindrome_v2(1221) is True
assert isPalindrome_v2(12321) is True
assert isPalindrome_v2(-121) is False
assert isPalindrome_v2(10) is False
print("all test cases passed!")
跑完这些用例后,我会再加几组相对刁钻的输入,比如 1001、100001、0、1、9、-101、2147447412(这个是 int 范围内的回文数)。这些用例覆盖了偶数位、奇数位、单位数、负数和末位 0 等场景。测试用例设计得足够全,代码通过率会高很多,也省去了在网页上反复提交的等待时间。
4.2 本地 Python 环境下的执行细节
在用 VS Code 或其他本地编辑器跑代码时,有几个小地方需要注意。由于我们写的是函数,记得在文件末尾加上函数调用或断言,否则运行起来看不到任何输出,容易误以为代码有问题。刚才那种 assert 写法就很好,它会明确告诉你哪一行断言没通过。
另外,变量命名不要用 list、str 这种内置关键字,也不要在一个函数里混用 temp 和 x 导致理解混乱。代码是写给人看的,LeetCode 不会因为你命名规范加分,但面试官会。我平时建议的命名规则是:原始输入用 x,反转结果用 reverted,临时副本用 temp,含义清楚,读代码的人不需要猜。
4.3 用 pow 构造大回文数做压力测试
有些同学会担心,反转一半法在大数情况下会不会出错。其实可以用一个很大的回文数来验证,比如 123456789987654321。这个数字位数很多,但 Python 处理起来没有压力。你可以把它作为断言测试放进去,如果两种解法都返回 true,说明逻辑在大数场景下依然正确。
构造这个测试用例本身也是一个小练习:123456789 然后接它的逆序 987654321,这正好是一个回文数。理解了回文数的构造方式,你对题目的理解会更立体。我自己刷题时喜欢用类似的方法生成测试数据,而不是只依赖 LeetCode 隐藏的后台用例。
5. 两种解法对比与面试表达技巧
5.1 完整反转法与反转后半部分法的核心差异
面试时经常会被问“这两种解法有什么区别”,所以我把它们的关键差异整理成一个表,方便一目了然。
| 对比维度 | 完整反转法 | 反转后半部分法 |
|---|---|---|
| 基本思想 | 反转整个整数后和原数比较 | 只反转后半部分,和前半部分比较 |
| 循环次数 | 和数字位数相同 | 约为数字位数的一半 |
| 是否担心溢出 | Python 不需要,C++/Java 需要 | 天然避开了反转后半段可能出现的溢出风险 |
| 对原数据的处理 | 需要临时变量暂存原数 | 可以直接修改原数,保留前半部分 |
| 边界判断 | 需要处理负数和末位 0 | 同样需要处理负数和末位 0,且末位 0 的判断更为关键 |
| 代码可读性 | 更直观,新手容易理解 | 略绕,需要理解“一半”的判断条件 |
从时间复杂度看,两者都是 O(log₁₀ n),但常数因子不同。从空间复杂度看,两者都是 O(1)。所以实际运行差异在 LeetCode 这种规模下并不明显。区别更多体现在思维的进阶程度上:完整反转法是“我能想到的解法”,反转一半则是“我理解对称性后的最优解法”。
5.2 面试时怎么把反转一半讲清楚
如果面试官让你现场讲思路,我的建议是不要一上来就贴代码,而是按下面这个顺序口述:
- 先排除负数和末尾为 0 的非零数,这两类直接 false。
- 用一个 reverted 变量保存“从后往前抠出来的数字”,每抠一位,就把 reverted 乘 10 加上这一位。
- 同时把 x 除以 10,相当于不断砍掉末尾。
- 当 x 不再大于 reverted 时,说明已经处理到中间了。
- 此时如果 x 和 reverted 相等,说明位数是偶数;如果 x 等于 reverted 整除 10,说明位数是奇数,中间那位被多放到 reverted 末尾,去掉再比较。
讲完之后再举 1221 和 12321 两个例子带面试官走一遍循环,逻辑会非常清晰。我见过很多候选人代码写得对,但讲不出为什么循环条件要写 x > reverted,结果面试官追问两句就露馅了。所以算法题刷完以后,一定要能解释每一步选择的原因,而不只是记得怎么写。
5.3 做完这题可以顺手刷的关联题
刷题最忌讳孤立地背答案,所以我每做完一道题,都会顺手找两三道思路相近的题目巩固一下。回文数这道题做完之后,我建议你立刻去做两个延伸:
第一是 LeetCode 第 7 题“整数反转”。这道题和回文数共用一套“取末位、累加、砍末位”的操作模板,但额外增加了溢出判断,能帮你把整数反转的通用技巧打磨得更扎实。
第二是 LeetCode 第 234 题“回文链表”。这道题要求判断链表是否回文,思路从“反转一半”升级成了“快慢指针找中点 + 反转后半段链表”,是同一个优化思想在链表结构上的延伸。做完你会明显感受到,题目在变,但底层的对称性思维没变。
如果在日常刷题时遇到与“反转”相关的题目,比如字符串回文、数组回文,也都可以迁移这套思路:能反转的就反转比较,不能整体反转的就想办法只处理一半,关键是用最少的操作完成对称性验证。
5.4 我的实操体会:最优解不一定是第一选择
最后说点我自己的感受。完整反转法虽然效率不如反转一半,但它的代码非常直观,适合用来“先 AC 拿到一个正确答案”。如果你是在面试高压状态下,一时间想不出最优解,先写出完整反转法并用清晰的逻辑讲出来,远好过卡在原地五分钟一句话不说。代码能跑通、逻辑能讲明白,本身就已经是合格的解法。
反转后半部分法更像是对这道题理解透彻后的产物。它不是背出来的,而是在理解了“回文 = 对称”之后自然浮现的方案。所以我建议刷题顺序是:先用自己能想到的方法解出来,再去看最优解为什么优,再看最优解对应的边界为什么这么设计。这样一轮下来,题目才会真正变成你自己的东西。
我个人现在每次重新刷这道题,都会在本地把两种解法各写一遍,跑通所有断言再进下一条。写熟练之后,代码不是最重要的,里面的边界意识和对“为什么反转一半就够”的理解,才是能带走并且复用到其他题目上的能力。
