摘要:记录 LeetCode 9「回文数」的三种写法。第一反应是把整数转成字符串,再用双指针从两边向中间比较,这个方法比较直观。但题目的进阶要求是“不将整数转为字符串”,所以继续学习如何通过
% 10和// 10反转整数,以及为什么实际上只需要反转数字的一半。
题目链接:LeetCode 9. 回文数
LeetCode 9. 回文数 —— 字符串双指针 + 数学反转整数
这道题要求判断一个整数是不是回文数。
所谓回文,就是:
从左往右读
和
从右往左读
完全一样
例如:
121
正着:121
反着:121
→ 回文
而:
123
正着:123
反着:321
→ 不是回文
我最近刚好在刷双指针,所以看到这道题的第一反应就是:
先把整数变成字符串
↓
左右各放一个指针
↓
从两边向中间比较
这个方法确实可以解决。
但是题目下面还有一个进阶要求:
你能不将整数转为字符串来解决这个问题吗?
所以这道题除了双指针之外,真正想练习的其实还有:
怎么直接操作一个整数的各个位数?
方法一:转成字符串 + 双指针
这是我一开始想到的方法。
代码:
class Solution:
def isPalindrome(self, x: int) -> bool:
x = str(x)
i = 0
j = len(x) - 1
while i < j:
if x[i] != x[j]:
return False
i += 1
j -= 1
return True
思路就是:
x = 121
先转成字符串:
"121"
然后设置左右两个指针:
i j
↓ ↓
1 2 1
比较:
x[i] == x[j]
如果相同:
左指针向右走
右指针向左走
也就是:
i += 1
j -= 1
继续比较。
只要中途发现:
x[i] != x[j]
说明左右不对称,可以直接:
return False
如果一直比较到两个指针相遇,都没有出现不同,就说明它是回文数。
为什么负数不需要单独判断?
例如:
x = -121
转成字符串之后:
"-121"
左右第一次比较:
'-' != '1'
所以会直接返回:
False
因此这个字符串写法甚至不用额外判断:
if x < 0:
方法二: 如果不转字符串,要怎么处理整数?
前置知识
这道题真正要学的地方在这里。
字符串可以直接写:
x[i]
x[j]
所以很容易拿到左右两边的字符。
但是整数:
121
并不能直接写:
x[0]
所以需要换一种方式取得每一位数字。
这里有两个很常用的操作:
x % 10
和:
x // 10
1. % 10:取出最后一位
例如:
123 % 10
结果是:
3
因为:
123 ÷ 10
余数 = 3
所以:
x % 10
可以取得 整数的个位数。
例如:
123 % 10 = 3
45 % 10 = 5
7 % 10 = 7
2. // 10:删除最后一位
例如:
123 // 10
结果是:
12
也就是把个位数删掉了。
所以:
123
↓ % 10
取出 3
123
↓ // 10
剩下 12
这两个操作组合起来,就可以一位一位处理一个整数。
思路
知道怎么取得最后一位以后,就可以考虑:
能不能把整个数字倒过来,然后和原数字比较?
1. 怎么把 123 变成 321?
准备一个变量:
reversed_num = 0
原数字:
x = 123
第一次 先取个位:
digit = x % 10
得到:
digit = 3
然后把这个 3 放进 reversed_num:
reversed_num = reversed_num * 10 + digit
这个公式后面会解释, 在这知道它的作用就是 把相对应的数字放在相对应的位置 就行
也就是:
0 × 10 + 3
= 3
然后删掉 x 的最后一位:
x //= 10
于是:
x = 12
现在:
x reversed_num
12 3
第二轮:
digit = 12 % 10
得到:
2
继续:
reversed_num = 3 * 10 + 2
得到:
32
然后:
x //= 10
得到:
1
现在:
x reversed_num
1 32
第三轮:
digit = 1 % 10
得到:
1
所以:
reversed_num = 32 * 10 + 1
得到:
321
最终:
123
↓
321
这样就完成了整个整数的反转。
2. 为什么是 reversed_num * 10 + digit?
这一句一开始可能比较抽象:
reversed_num = reversed_num * 10 + digit
可以理解成:
把一个新的数字接到原数字的最后面。
例如现在:
reversed_num = 12
digit = 3
想变成:
123
首先:
12 × 10 = 120
相当于把原来的数字整体向左移动一位:
12
↓
120
然后再:
120 + 3
得到:
123
所以:
reversed_num * 10 + digit
就是:
给 reversed_num 的最后面接上一位 digit
完整代码
因为反转过程中会不断修改 x:
x //= 10
所以需要提前保存原来的数字, 用于后面和反转后的数字对比
class Solution:
def isPalindrome(self, x: int) -> bool:
if x < 0:
return False
# 保存原始数字
original = x
reversed_num = 0
# x 还有数字没有处理完,就继续循环
# 每轮用 // 10 去掉最后一位,直到 x 变成 0
while x > 0:
# 1. 取最后一位
digit = x % 10
# 2. 把这一位接到反转数字后面
reversed_num = reversed_num * 10 + digit
# 3. 从原数字中删掉这一位
x //= 10
return original == reversed_num
为什么负数可以直接返回 False?
无论是什么负数, 前面都会带个负号, " - "
直接反转过去的话, 负号也会一起反转, 肯定会不回文就是了。
例如:
-121
从左往右:
-121
从右往左:
121-
显然不同。
所以, 直接针对这个特殊情况下定义:
if x < 0:
return False
负数一定不是回文数。
方法三:其实不用把整个数字都反转
上一个方法是:
121
↓
全部反转
↓
121
↓
和原数字比较
但是仔细想一下,判断回文其实只是在比较:
左半边
和
右半边
例如:
1221
可以看成:
12 | 21
我们只需要把右边的:
21
反过来变成:
12
然后比较左右两边即可。
所以其实没有必要把整个数字都反转:
只把右半边取出来并反转就够了。
不过数字可能是偶数位,也可能是奇数位。
这两种情况处理到中间时会有一点不同,所以分别来看。
1. 偶数位:以 1221 为例
先准备:
x = 1221
reversed_num = 0
我们的目标是:
1221
↓
12 | 21
↓
12 | 12
也就是不断从 x 的右边取数字,放进 reversed_num。
第一次
取出最后一位:
1221 % 10 = 1
所以:
reversed_num = 1
再把 x 的最后一位删掉:
1221 // 10 = 122
现在:
x reversed_num
122 1
可以理解成:
122 | 1
右边才取出一位,还没有到中间,所以继续。
第二次
从:
122
取出最后一位:
122 % 10 = 2
接到原来的 1 后面:
reversed_num = 1 * 10 + 2
得到:
12
同时:
122 // 10 = 12
所以现在:
x reversed_num
12 12
也就是:
12 | 12
对于偶数位来说,到中间时左右两部分正好一样长。
这时候:
x == reversed_num
说明两边已经碰到一起,不需要再继续从 x 中取数字了。
整个过程就是:
x reversed_num
1221 0
122 1
12 12
可以先记住偶数位的情况:
处理到中间
↓
x 和 reversed_num 正好相遇
2. 奇数位:以 12321 为例
再看:
12321
它可以分成:
12 | 3 | 21
和偶数位不同的是,中间多了一个:
3
但是这个中间位不需要和任何数字配对。
我们还是按照同样的方法,从右往左取数字。
开始:
x = 12321
reversed_num = 0
第一次
12321 % 10 = 1
所以:
reversed_num = 1
x = 1232
现在:
1232 | 1
第二次
取出:
1232 % 10 = 2
接到 1 后面:
reversed_num = 12
同时:
x = 123
现在:
123 | 12
还没有走到中间,所以继续。
第三次
从:
123
取出最后一位:
3
所以:
reversed_num = 123
而:
x = 12
现在变成:
x reversed_num
12 123
也就是说,两边已经从:
123 | 12
走到了:
12 | 123
已经越过中间了。
所以奇数位和偶数位稍微不同:
偶数位:
处理到中间时,两边正好相遇
奇数位:
处理到中间时,两边会交叉
而 reversed_num 多出来的最后一位:
123
↑
3
正好就是原数字中间的:
12 | 3 | 21
↑
中间这一位不影响左右是否对称,所以之后 在比较时 把它去掉即可:
reversed_num // 10
得到:
123 // 10 = 12
于是左右真正需要比较的还是:
12 和 12
3. 两种情况怎么合成一个停止条件?
现在不用再重新走一遍两个例子,只看刚才得到的规律:
偶数位:
x 和 reversed_num 相遇
奇数位:
x 和 reversed_num 交叉
也就是说,我们真正想要的是:
在两边还没有相遇或交叉之前,一直继续取数字。
处理过程中:
x
→ 每轮删掉最后一位
→ 越来越小
reversed_num
→ 每轮接上一位
→ 越来越大
所以只要:
x > reversed_num
说明两边还没有相遇或交叉,需要继续处理。
而一旦:
x <= reversed_num
说明已经走到中间附近,可以停止。
因此循环条件就是:
while x > reversed_num:
reversed_num = reversed_num * 10 + x % 10
x //= 10
这里的:
x == reversed_num
对应两边正好相遇;
而:
x < reversed_num
对应两边已经交叉。
所以不需要提前计算数字有多少位,也不需要提前判断它到底是奇数位还是偶数位。
让两边自己不断靠近,直到相遇或者交叉就可以了。
4. 最后怎么判断是不是回文?
停止之后,也正好对应前面的两种情况。
如果原数字是偶数位,例如:
1221
最后:
12 | 12
直接比较:
x == reversed_num
即可。
如果原数字是奇数位,例如:
12321
最后:
x = 12
reversed_num = 123
reversed_num 多出来的 3 是中间位。
把它删掉:
reversed_num // 10
再比较:
x == reversed_num // 10
即可。
所以最后只需要覆盖这两种情况:
return x == reversed_num or x == reversed_num // 10
它只是把前面已经得到的两种情况合在了一起:
偶数位:
左右直接相等
奇数位:
去掉中间位以后相等
完整代码
class Solution:
def isPalindrome(self, x: int) -> bool:
# 负数一定不是回文
if x < 0:
return False
# 除了 0 以外,末尾为 0 的数一定不是回文
if x % 10 == 0 and x != 0:
return False
reversed_num = 0
# 只反转数字的一半
while x > reversed_num:
# 取出 x 的最后一位,并接到 reversed_num 后面
reversed_num = reversed_num * 10 + x % 10
# 删除 x 的最后一位
x //= 10
# 偶数位:x == reversed_num
# 奇数位:去掉 reversed_num 中间那一位
return x == reversed_num or x == reversed_num // 10
除了负数,还有一种可以直接排除的情况:
末尾是
0的非零整数,一定不是回文数。
例如:
10
如果它是回文,开头也应该是 0,也就是:
01
但整数不会保留前导 0,所以 10 不可能是回文。
唯一例外是:
0
0 本身是回文数。
复杂度
假设整数一共有 n 位。
方法一:字符串 + 双指针
需要把整数转换成字符串,并进行左右比较。
时间复杂度:
O(n)
空间复杂度:
O(n)
因为创建了新的字符串。
方法二:反转整个整数
每一轮都会删除一位数字:
x //= 10
总共处理 n 位。
所以时间复杂度:
O(n)
只使用几个整数变量,因此额外空间复杂度:
O(1)
方法三:只反转一半
只需要处理大约一半的数字。时间会快点。
虽然实际循环次数只有:
n / 2
但是在复杂度里:
O(n / 2)
仍然写成:
O(n)
空间复杂度:
O(1)

962

被折叠的 条评论
为什么被折叠?



