DAY7: LeetCode 9. 回文数

摘要:记录 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)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值