LeetCode 每日一题坚持到第 9 题,今天这道回文数,我打算认真写一次。这题在 LeetCode 上是 easy 难度,但真要说“透彻掌握”,大部分人其实没有。回文数的定义小学就学过,从左往右读和从右往左读一样,比如 121、1221、12321;可一旦把判断方式限定在“不能转字符串、不能开数组、只靠整数运算”,思路就完全不一样了。
这篇文章我会给你两套 Python 解法:完整反转法和反转后半部分法。前者好理解,适合新手把算法逻辑跑通;后者是 LeetCode 官方也认可的精简方案,能少算约一半位数,也是面试官更愿意看到的思路。不管你是刚开始刷题的新手,还是准备把基础题整理成笔记的老手,这篇都值得收藏。我会把两套代码的原理、运行过程、边界用例和本地自测方法全部拆开讲,你照着敲一遍就能直接提交。
先说一个我自己的观点:回文数这题,很多答案只是扔出一行 str(x) == str(x)[::-1],看起来很优雅,但这恰恰是刷题里最危险的“假会”。本文最后会解释为什么建议你不要停在字符串法上。
1. 回文数这题到底在考什么
1.1 题目回顾:先别急着写代码
LeetCode 第 9 题要求你写一个函数,判断一个整数 x 是否是回文数。它给的例子有三个:121 是,-121 不是,10 不是。
这三个例子已经把核心考点暴露得很清楚了。第一,负数为什么不是回文数?因为负号在数字最前面,如果从右往左读,负号会跑到末尾,比如 -121 反过来是 121-,这显然不相等。第二,为什么 10 不是?因为数字的末位是 0,两个方向读会出现“01”和“10”的差异。第三,题目没有说“禁止转换字符串”,这是一个隐藏考点,真正有经验的面试官会追问你能不能不用额外空间完成。
这道题虽然标着 easy,但它并没有那么简单。它考察的不是你能不能判断一个字符串是否对称,而是你能不能理解“十进制整数的位数分离”和“累加反转”这两个基础操作。这两个操作日后做整数反转、回文链表、最长回文子串时都会反复用到。
1.2 第一直觉:转字符串法为什么不够
顺着前面的疑问,先来说说那个一行字符串解法:
python复制def isPalindrome(self, x: int) -> bool:
return str(x) == str(x)[::-1]
这行代码能通过测试,但它有三个问题。第一,它把整数转成字符串,产生了一份额外的字符数组,空间复杂度是 O(n),而用纯数字判断可以做到 O(1)。第二,它没有训练到反转类算法的基本功。LeetCode 第 7 题“整数反转”和第 9 题往往是连着刷的,如果你只会转字符串,后面遇到不能转字符串的题目还是会卡住。第三,字符串反转的思维无法迁移。以后你遇到回文链表,没法把链表的每个值转成一个字符串再判断,你必须学会“找中点、反转后半段”这种对称操作的思路。
所以本文下面讲的两个数字解法,不是炫技,而是帮你建立一套后续能复用的思维模型。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 两种解法的整体设计思路
2.1 核心思维:把数字“倒过来”再比较
判断回文数最天然的思路是:把一个数字倒过来,看倒过来的结果是否和原来相等。121 倒过来还是 121,所以是回文数;123 倒过来是 321,不是回文数。
问题在于,怎样用纯数学方式“倒”一个整数。这里用到的技术,是所有整数操作题里最重要的一个小套路:每次取当前数字的末位,然后把它拼接到结果后面。
单独看一个例子,如果 x = 123,想要反转成 321,过程是这样:
- 第 1 步,取 123 的末位 3;
- 第 2 步,把 123 去掉末位变成 12;
- 第 3 步,再取 12 的末位 2;
- 第 4 步,把 12 变成 1;
- 第 5 步,取 1 的末位 1;
- 第 6 步,把 1 变成 0,循环结束。
把这个过程翻译成代码公式,就是下面两行的反复执行:
python复制reversed_num = reversed_num * 10 + x % 10 # 取出最后一位并追加到结果末尾
x //= 10 # 去掉已经处理过的最后一位
用个生活化类比:反转数字就像倒积木。你面前有一列积木,最上面那块最容易拿。每次拿掉最上面那块,放到另一个空盒子里;第二次拿到的积木放在第一次拿到的上面。等原来那列积木全部拿完,新盒子里积木的顺序就和原来完全相反了。x % 10 是“拿最上面那块”,x //= 10 是“把这块从原队列里移除”,reversed_num * 10 + 新块 是“把新块放到新盒子的最上面”。
2.2 完整反转法和反转后半部分法,差异在哪
有了上面的反转公式,题目自然出现两个分支。
第一种是完整反转法。把整个数字全部反转,得到一个新整数,再拿它和原始数字比。如果是回文数,反转后的数字会等于原始数字;如果不是,就不等。
第二种是反转后半部分法。它利用回文数的对称性:只要后半部分反转后和前半部分相等,就说明整体是回文。比如 1221,前半部分是 12,后半部分 21 反转后是 12,两边对上,是回文;比如 12321,前半部分是 12,中间位是 3,后半部分 21 反转后去掉中间位还是 12,也对上。
第二种方法在计算量上有天然优势。完整反转法要把数字的每一位都处理完,而反转后半部分只需要处理一半左右。举个具体数字,x = 12345678987654321,用完整反转法要循环 17 次;用反转后半部分法,循环到第 9 次左右就可以停下来了,因为你已经处理到对称轴的附近。虽然时间复杂度在 big-O 表示下都是 O(log n),因为数字位数就是 log 级别,但实际运行中后半反转法的常数要小得多。
更重要的是,完整反转法在 C++、Java 这类有固定整数位宽的语言里可能会溢出。Java 的 int 最大是 2147483647,如果反转一个超过这个范围的整数,结果就不可控。Python 的 int 是任意精度,不会溢出,所以很多人刷 Python 时会忽略这件事;但如果你是面试,面试官可能会问“如果换到 Java 你怎么办”,这时候你抛出“只反转一半”的方案,正好能绕开溢出。
3. 解法一:完整反转法,先把逻辑跑通
3.1 代码实现与逐行拆解
先给出可以在 LeetCode 上直接提交的版本:
python复制class Solution:
def isPalindrome(self, x: int) -> bool:
if x < 0:
return False
original = x
reversed_num = 0
while x > 0:
reversed_num = reversed_num * 10 + x % 10
x //= 10
return original == reversed_num
这段代码分四步走。第一步,如果 x 是负数,直接返回 False,因为负数不可能回文。第二步,把 x 的值先存到 original 里,因为后面的 while 循环会不断修改 x,把 x 一步步减到 0,如果不提前存,比较时就用不上原始数字了。第三步,进入循环,每次把 x 的末位“剥离”下来,拼接到 reversed_num 后面,然后让 x 丢掉这一位。第四步,循环结束后比较 original 和 reversed_num 是否相等。
这里有一个细节需要新手注意:while x > 0 这个条件,对 x = 0 的情况会直接跳过循环,此时 reversed_num 是 0,original 也是 0,最后返回 True,结果是对的。0 本身是回文数,因为它从左往右读和从右往左读都是 0。
3.2 用具体数字完整走一遍
以 x = 12321 为例,我给你列个表,这样看运行过程最直观:
| 当前 x | x % 10 | reversed_num 变化 | x //= 10 后 |
|---|---|---|---|
| 12321 | 1 | 0 * 10 + 1 = 1 | 1232 |
| 1232 | 2 | 1 * 10 + 2 = 12 | 123 |
| 123 | 3 | 12 * 10 + 3 = 123 | 12 |
| 12 | 2 | 123 * 10 + 2 = 1232 | 1 |
| 1 | 1 | 1232 * 10 + 1 = 12321 | 0 |
循环结束后,original 还是 12321,reversed_num 变成了 12321,两者相等,所以返回 True。
这个例子的好处是它恰好让你看到取余和整除操作如何把数字“倒”过来。x 不断变小,每位数字被依次放到新数的最高位、次高位……一直到最低位,新数就完成了整个数字的反转。
3.3 复盘一下:这种解法的短板在哪
完整反转法最容易理解,但我会在刷题笔记里把它标记为“过渡方案”,而不是最终提交方案。原因有两个。
第一是长度浪费。判断回文只需要看一半,你反转全部位数等于做了一半无用功。比如对于 10 位数的回文数,你其实只要比较前 5 位和后 5 位反转结果,没必要处理全部 10 位。
第二是其他语言的溢出风险。前面提过,Java 的 int 反转 2147447412 这类数字时就可能出现超过 Integer.MAX_VALUE 的结果。Python 因为大整数机制感觉不到,但你在面试时如果主动指出这个坑,面试官会认为你真的理解跨语言差异,而不只是会背题。
既然完整反转有上述短板,下面的反转后半部分法就可以登场了。
4. 解法二:反转后半部分法,少算一半还能绕开溢出
4.1 怎么确定“已经反转到一半了”
第二套解法,也是官方题解里的方案。核心思想是维护一个 reversed_half 变量,每次从原数字 x 的末尾取一个数字,接到 reversed_half 上,直到 reversed_half 大于等于剩余的 x,就说明我们已经处理到一半以上了。
代码里的循环条件非常关键:
python复制while x > reversed_half:
这个条件之所以成立,是因为原数字 x 在每次循环里都会减少一位,而 reversed_half 每次会增加一位。当 x 的剩余位数不足 reversed_half 的位数,或者两者位数相等但 x 更小时,说明“后半段已经翻完甚至超过了原数剩余部分”。
举例理解:原始数 1221,经过两轮后 x = 12,reversed_half = 12,两者位数相同,继续反转就会变成 x = 1,reversed_half = 122,这就过头了,所以在相等时停刚刚好。原始数 12321,经过三轮后 x = 12,reversed_half = 123,此时 reversed_half 已经比 x 多一位,说明中间位 3 已经被算进去了,停在这里正好。
4.2 边界条件必须单独处理
在进入循环之前,需要先把两种不可能为回文的情况排除:
x < 0,负数直接返回 False;x % 10 == 0 and x != 0,也就是末位是 0 且本身不是 0 的情况,比如 10、100、1020 这种,直接返回 False。
第二条存在的原因要稍微想一下。如果某个正整数的末位是 0,那么它的最高位必须也是 0 才是回文,但数字的最高位不可能为 0。因此,只有 0 本身能成为末位为 0 的回文数,其他以 0 结尾的非零整数可以直接否决。
还有一点要提醒大家:不要觉得第二条条件多余。如果输入 x = 10,不排除它,后面的循环会执行:x = 10 时取余得到 0,reversed_half = 0,x //= 10 后 x = 1,第二轮循环时 x = 1,reversed_half = 0,条件成立,然后 reversed_half 变成 1,x 变 0,最后判断 0 == 1 或 0 == 0,会误判成 True,这是错的。所以边界条件要先拦下。
4.3 反转后半部分法的完整代码
提交到 LeetCode 的版本如下:
python复制class Solution:
def isPalindrome(self, x: int) -> bool:
if x < 0 or (x % 10 == 0 and x != 0):
return False
reversed_half = 0
while x > reversed_half:
reversed_half = reversed_half * 10 + x % 10
x //= 10
return x == reversed_half or x == reversed_half // 10
最后一行是这套方法最精妙也最容易理解错的位置。它分两种情况:
- 如果原数是偶数位回文,比如 1221,反转完的
reversed_half和剩下来的x位数相同,此时直接用x == reversed_half判断; - 如果原数是奇数位回文,比如 12321,
reversed_half会比x多一位,多出来的那一位正好是原始数字的中间位,把它去掉再比较,也就是x == reversed_half // 10。
两个条件用 or 连接,无论原数是偶数位还是奇数位,都能覆盖到。
4.4 运行过程表:1221 和 12321 的对比
我建议你第一次学这个解法时,把 1221 和 12321 两个数字手抄一遍,分别走一次循环,很快就懂奇偶差异。
先看 1221:
| 轮次 | 进入循环前 x | reversed_half | 计算后 x | 计算后 reversed_half |
|---|---|---|---|---|
| 1 | 1221 | 0 | 122 | 1 |
| 2 | 122 | 1 | 12 | 12 |
第三轮判断时,x = 12,reversed_half = 12,x > reversed_half 为假,循环退出。此时 x == reversed_half,两个 12 相等,返回 True。
再看 12321:
| 轮次 | 进入循环前 x | reversed_half | 计算后 x | 计算后 reversed_half |
|---|---|---|---|---|
| 1 | 12321 | 0 | 1232 | 1 |
| 2 | 1232 | 1 | 123 | 12 |
| 3 | 123 | 12 | 12 | 123 |
第四轮判断时,x = 12,reversed_half = 123,x > reversed_half 为假,循环退出。此时 x = 12,reversed_half // 10 = 12,右边条件成立,返回 True。
如果你把这两个过程对比着看,会发现在循环停止的那一刻,两套数值自动区分了奇偶:偶数位时 x 和 reversed_half 等长,奇数位时 reversed_half 刚好多出一位。这就是为什么最后一行要写两个判断条件,而不是只写 x == reversed_half。
5. Python 细节与边界用例,最容易翻车的地方
5.1 Python 负数取模和整除的坑
写过 C++ 或 Java 再转 Python 的人,最容易在取模运算上翻车。同样是 -123 % 10,在 C 系语言里结果是 -3,在 Python 里结果却是 7。原因是 Python 的 % 运算结果总是和除数同号,或者说 Python 在计算时采用了向下取整的规则,而不是向零截断。
这会影响我们写反转逻辑吗?其实不会,因为上面两个解法里,负数都在最开头被 x < 0 拦住了,根本走不到 x % 10。但如果你自己扩展代码,比如想写一个“反转任意整数并判断回文”的通用函数,就一定要注意这个差异。
另外一个 Python 新手常忽略的点:// 也是向下取整。-123 // 10 在 Python 中是 -13,而不是 -12。如果你在某个项目里直接从其他语言的代码里搬反转逻辑,没注意负数行为,找到 bug 会花很长时间。我的经验是,只要涉及整数的取余和整除,优先想清楚每个变量的正负性,能提前排除负数就提前排除。
5.2 这组测试用例,建议直接保存下来
刷题时只跑题目给的三个例子远远不够。边界情况在 LeetCode 判题系统里占了大头,很多提交挂在 0、负数、10 的倍数这种细节上。
以下是我长期测试回文数问题时使用的用例:
| 输入 | 期望结果 | 说明 |
|---|---|---|
| 0 | True | 特殊边界,0 是回文 |
| 7 | True | 个位数默认是回文 |
| -7 | False | 负号无法对称 |
| 10 | False | 末位 0 且本身不是 0 |
| 100 | False | 三位数但是 00 开头不可能 |
| 121 | True | 基础奇数位回文 |
| 1221 | True | 基础偶数位回文 |
| 12321 | True | 带中间位的奇数位回文 |
| -121 | False | 负回文在题目定义下仍不是回文 |
| 2147447412 | True | 大整数回文 |
| 1000021 | False | 带 0 的不规则数字 |
把上面的用例放进本地测试脚本,两个解法都能验证。但凡实现上边界条件少写了一个,这些用例里至少有一两个会报错。
5.3 本地搭一个最小测试环境,不用反复提交
初学者最容易做的事是:每次改代码都去 LeetCode 网页点“提交”,失败了再看输出,再改,再提交。这样效率不高,也不利于培养测试意识。我建议你本地建一个测试脚本,用最标准的 Python 标准库就能跑,不需要任何第三方依赖。
如果你本地 Python 环境还没配置好,用命令行先创建虚拟环境是一个好习惯。在项目目录下执行:
bash复制python -m venv .venv
然后激活这个虚拟环境:
- macOS/Linux 使用
source .venv/bin/activate - Windows 使用
.venv\Scripts\activate
激活后把下面的代码保存为 test_palindrome.py 运行即可:
python复制def is_palindrome(x: int) -> bool:
if x < 0 or (x % 10 == 0 and x != 0):
return False
reversed_half = 0
while x > reversed_half:
reversed_half = reversed_half * 10 + x % 10
x //= 10
return x == reversed_half or x == reversed_half // 10
cases = [
(0, True),
(7, True),
(-7, False),
(10, False),
(100, False),
(121, True),
(1221, True),
(12321, True),
(-121, False),
(2147447412, True),
(1000021, False),
]
for number, expected in cases:
result = is_palindrome(number)
print(f"{number}: expected {expected}, got {result} -> {'PASS' if result == expected else 'FAIL'}")
这个脚本看起来很简单,但它是你做题时的安全网。每次改完算法先跑一遍本地脚本,再去提交,能省下大量等待判题的时间。如果你想切换实现,只需要把 is_palindrome 函数里的方法体换掉就行。
我在 VSCode 里刷题时,通常会把这个脚本和题解笔记放在同一个文件夹里,方便以后复习。环境配置只有第一次需要花点时间,后面就是打开即跑。
6. 从“会做一题”到“会做一类题”
6.1 同一道题准备双解,面试时会更稳
我见过很多人在面试里写回文数,第一反应就是字符串反转。面试官点点头,接着问:“如果不能用额外空间呢?”这个时候如果你能马上写出反转后半部分的解法,整个面试氛围会完全不一样。
准备双解不是做无用功。第一种完整反转法帮助你把整数反转的循环公式深深写进肌肉记忆;第二种反转后半部分法帮你在“回文 = 对称”这个本质上建立直觉。面试现场最怕的不是答不出来,而是只会一种方法然后被追问到卡壳。你提前把两种解法都准备好,一旦被追问,等于提前准备了备选答案。
如果你时间有限,只记一个版本,我建议你重点背反转后半部分法。因为它囊括了边界条件判断、循环停止条件、奇偶位处理这三个考点,含金量最高。
6.2 回文题在 LeetCode 里的延伸链路
回文数这题的正向价值,是帮你打通数字反转这个技能点。顺着这个技能点向后延伸,有几个题目可以连着刷:
- LeetCode 第 7 题“整数反转”:直接使用本文的
rev = rev * 10 + x % 10公式,只不过需要考虑 32 位整数溢出时的返回 0 逻辑; - LeetCode 第 234 题“回文链表”:已经不能靠反转整个数字解决,需要结合快慢指针找链表中间点,再反转后半段链表;
- LeetCode 第 5 题“最长回文子串”:难度升级到动态规划或中心扩展,但“回文对称轴”的思想仍然一致。
你会发现,从数字回文到链表回文再到字符串回文,核心永远在围绕“对称”做文章。今天这个 easy 题,其实是整条学习链的第一环。如果你做过回文链表后再回头看这题,会特别有感触,因为 234 题找中点用的快慢指针,和这里判断 x > reversed_half 来停止循环的思维模式很像:都是通过在空间上找到那条对称轴来减少无谓的比较。
6.3 面试追问时的防御姿势
围绕这题,面试官通常会在四个方向上做文章。
第一,“为什么负数直接排除?”你要能说明负号的位置不可能对称,而不是含混回答“题目说的”。
第二,“为什么排除 10 的倍数?”因为回文要求首位等于末位,首位不可能为 0,所以末位也不能是 0,除非整个数字就是 0。
第三,“while 条件为什么是 x > reversed_half,能不能改成 >=?”不能用大于等于,因为这样会多数一位,比如 11 在计算到 x = 1、reversed_half = 1 时还会再进一次循环,得出 x = 0、reversed_half = 11,最终判断会出错。小于的条件恰好让循环停在位数等长或仅差一位的状态。
第四,“如果输入是负数,但是去掉符号后是回文,比如 -121,那算不算?”在 LeetCode 这道题的定义里不算,因为题目说的是“整数”,负号本身是整数的一部分;只有当你自己重新定义“绝对值回文”时才可以去掉符号,但面试时要先和面试官确认定义,不能想当然。
把上面四个追问的答案想清楚,你才算真正掌握了这题。
7. 问题排查与经验速查
7.1 遇到提交失败,先按这个顺序排查
如果提交后报错,我建议按照下面的顺序定位问题,而不是盲目改代码。
第一步,先检查是不是负数没有处理。错误信息往往显示 -121 返回了 True,原因就是代码里没有在开头排除负数。第二步,检查 0 这个边界是否被误伤。如果你写成 if x % 10 == 0: return False,那么输入 0 时也会被排除,结果错误。第三步,手动走查一个奇数和偶数回文。拿 1221 和 12321 分别代入循环,看循环出口处 x 和 reversed_half 的关系是否和你想的一致。第四步,检查最后一行是否漏了 reversed_half // 10 的分支。如果只写 x == reversed_half,奇数位回文会全军覆没。
大多数错误都逃不出这四个原因。
7.2 高频问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| -121 返回 True | 缺少负数判断 | 在函数第一行加 if x < 0: return False |
| 0 返回 False | 末尾 0 排除逻辑把 0 也排除了 | 条件改成 x % 10 == 0 and x != 0 |
| 10 返回 True | 没有排除末尾为 0 的非零数字 | 在循环前直接返回 False |
| 12321 返回 False | 最后一行漏掉奇数位场景 | 使用 or x == reversed_half // 10 |
| 某一题在 C++/Java 里溢出 | 使用了完整反转法 | 换用反转后半部分法 |
| 本地运行结果和平台不一致 | 平台输入可能是大数或边界用例 | 用上一节测试脚本覆盖全部边界 |
这张表看起来短,但每条都是我实际刷题过程中看到过的问题。有些错误很隐蔽,比如 0 的情况,你多写了边界条件反而把它误伤,这种“做多错多”的事情在算法题里很常见。所以写完代码后用测试用例跑一遍,比肉眼检查更可靠。
7.3 关于这个题目,我自己的几点体会
最后分享一点个人的刷题感受。回文数这种 easy 题最容易被忽视,觉得“会做”就直接跳过,可实际上把边界条件、奇偶逻辑、双解差异说清楚,远没有想象中容易。我自己后来复盘时发现,能清楚解释 while x > reversed_half 为什么用严格小于的人,写回文链表时对快慢指针的边界处理也更有感觉。
对新手读者,我的建议很朴素:不要只看题解,把完整反转法和反转后半部分法分别手写一次,再跑一遍上面那组测试用例。等 1221 和 12321 的运行过程你都能不看代码推出来,这题就真正变成你的东西了。顺手的话,把这个测试脚本留在你的刷题项目里,以后做到整数反转、回文链表时还能回来复用。
