LeetCode 8. 字符串转换整数 (atoi)(中)& 剑指offer 面试题67. 把字符串转换成整数(中)

本题最难点在于如何用Python模拟解决 Int32 溢出问题。

题目

请你来实现一个 atoi 函数,使其能将字符串转换成整数。 首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。 当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。 注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。 在任何情况下,若函数不能进行有效的转换时,请返回 0。

说明: 假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [\(-2^{31}\), \(2^{31}-1\)]。如果数值超过这个范围,请返回 INT_MAX (\(2^{31}-1\)) 或 INT_MIN (\(-2^{31}\))。

示例

示例 1:

1
2
输入: "42"
输出: 42

示例 2:

1
2
输入: "   -42"
输出: -42

解释: 第一个非空白字符为 '-', 它是一个负号。 我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。

示例 3:

1
2
输入: "4193 with words"
输出: 4193

解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。

示例 4:

1
2
输入: "words and 987"
输出: 0

解释: 第一个非空字符是 'w', 但它不是数字或正、负号。 因此无法执行有效的转换。

示例 5:

1
2
输入: "-91283472332"
输出: -2147483648

解释: 数字 "-91283472332" 超过 32 位有符号整数范围。 因此返回 INT_MIN (\(−2^{31}\)) 。

测试用例

功能测试(输入的字符串表示正数、负数和0)。 边界值测试(最大的正整数;最小的负整数)。 特殊输入测试(输入的字符串为 nullptr 指针;输入的字符串为空字符串;输入的字符串中有非数字字符等)。

考察知识点

字符串处理、正则项匹配、条件判定逻辑

核心思想

使用正则表达式解决该问题

r'^[\+\-]?\d+'
解释:
^:匹配字符串开头
[+-]:代表一个+字符或-字符
?:前面一个字符可有可无
一个数字
+:前面一个字符的一个或多个
:一个非数字字符
*:前面一个字符的0个或多个

为什么可以使用正则表达式?如果整数过大溢出怎么办?用该方法来防止结果越界

1
max(min(result, 2**31 - 1), -2**31) 
题目中描述: 假设我们的环境只能存储 32 位大小的有符号整数

首先,这个假设对于 Python 不成立,Python 不存在 32 位的 int 类型。其次,即使搜索到的字符串转32位整数可能导致溢出,我们也可以直接通过字符串判断是否存在溢出的情况(比如 try 函数 或 判断字符串长度 + 字符串比较)。

参考连接

通过字符串遍历来解决该问题

这道题也可以通过直接遍历字符串来处理,做好特殊情况处理即可。

本题难点 如何检测 非空格 第一个字符 检测到之后,如何避免重复检测

解决方案 设置变量:FirstCharCheck = True

  • if FirstCharCheck
    • 遇到 空格‘ ’, continue(直接跳过,不会改变flag的值)
    • 遇到 + or -,更新 sign 符号,并检测 i+1 位置是否为数字,决定是否提前 return+ 的ASCII码是43 ,- 的ASCII码是45 所以 sign = 44-ord('+/-')
    • 遇到 ‘数字’,ans = ans*10 + ord(str[i]) - ord('0')
    • FirstCharCheck = False ,循环到下次时 FirstCharCheck = False,避免了重复检测。
  • else
    • 每次遇到digital,添加到ans上 ans = ans*10 + ord(str[i]) - ord('0')
    • 遇到非digital,跳出循环
  • 最终返回时,对于溢出的数字要做越界判定:
1
return max(min(ans, (1<<31)-1), -(1<<31))
  • 实际上,正常来说不应该在最后返回时才做溢出判断,因为数字的大小压根儿就不能超过 [-2147483648, 2147483647] 这个范围。所以,应该在计算过程中做溢出判断。
1
2
3
4
5
tmp = (ord(item) - ord_0) * (10**count)
if (not flag and res < 147483648 and tmp <= 2000000000) or (flag and res < 147483647 and tmp <= 2000000000):
pass # 满足条件,不做处理。
else:
is_valid = False

参考连接

Python版本

一个使用正则表达式的版本

简洁,但是完成难度大,可读性低,依赖正则模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import re
class Solution:
def myAtoi(self, s: str) -> int:
return max(
min(
int(*re.findall( # 由于返回的是个列表需要加*,解包并且转换成整数。
r'^[\+\-]?\d+', s.lstrip())
# lstrip()去掉字符串开头的空格
# ^:匹配字符串开头
# [\+\-]:代表一个+字符或-字符
# ?:前面一个字符可有可无
# \d:一个数字
# +:前面一个字符的一个或多个
# 总结起来就是,在s.lstrip中寻找以+或者-开头或者不以开头的多个数字
),
2**31 - 1
), # 在int函数结果和 2**31-1(理论最大值之间选择较小的那一个)
-2**31 # 在 min函数结果和 -2**31(理论最小值之间选择较大的那个一个)
)

# 可读性更高的版本
import re
class Solution:
def myAtoi(self, str: str) -> int:
INT_MAX = 2147483647
INT_MIN = -2147483648
str = str.lstrip() #清除左边多余的空格
num_re = re.compile(r'^[\+\-]?\d+') #设置正则规则设置正则规则 匹配一个 以+或者-开头的多个数字组成的字符串 / 匹配一个仅由多个数字组成的字符串
num = num_re.findall(str) #查找匹配的内容
num = int(*num) # re.findall返回的是一个字符串列表,int()数据类型转换不支持列表,用*对列表解包得到字符串。 需要注意的是,* 和 ** 只在“传参”时才有用。直接调用num = *num会报错 SyntaxError: can't use starred expression here
return max(min(num,INT_MAX),INT_MIN) #返回值

遍历字符串 进行判断的方法

  • 在最终返回时才做溢出判断(仅适用于Python所以不推荐)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 可读性更高的版本
class Solution:
def strToInt(self, _str: str) -> int:
sign = 1 # 整数符号 +1 or -1
ans = 0 # 累计结果
FirstCharCheck = True # bool 用于检测第一个非空格字符,默认为True,当检测到第一个字符后设为False
for i in range(len(_str)):
# FirstCharCheck为True时 进行第一个字符检测
if FirstCharCheck:
if _str[i] == ' ': # 遇到空格跳过,直到遇到第一个非空格字符 不会修改FirstCharCheck
continue
elif _str[i] == '-' or _str[i] == '+': # 如果第一个字符为+/-号, 更新sign,
sign = 44-ord(_str[i]) # Return the Unicode code point for a one-character string. ‘+’的ASCII码是43 ,‘-’的ASCII码是45 所以 sign = 44-ord(‘+/-’)
if i+1 < len(_str) and not _str[i+1].isdigit(): # 判定+/-后字符是否为数字
return 0
elif _str[i].isdigit(): # 如果第一个字符为数字,更新ans
ans = ans*10 + (ord(_str[i])-ord('0'))
else: # 第一个字符为其他符号,return
return 0
FirstCharCheck = False # 判定第一个字符后,更新FirstCharCheck,避免重复检测
# 后续遇到数字字符,累加到ans
elif _str[i].isdigit():
ans = ans * 10 + (ord(_str[i]) - ord('0'))
# 后续遇到非数字字符,直接break,计算结果
else:
break

ans *= sign # 先乘以正负

# 利用max和min进行越界判定
return max(min(ans, (1<<31)-1), -(1<<31))
  • 在计算过程中就实时进行溢出判断(适用于所有类型的语言,推荐!!!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
 # 可读性更高的版本
class Solution:
def __init__(self):
self.count = 0
self.ans = 0 # 累计结果

def autoAdd(self, char, sign):
tmp = (ord(char) - ord('0'))
# 在计算过程中就实时进行溢出判断
if self.ans > 214748365:
return False
elif self.ans == 214748364 and ((sign == -1 and tmp > 8) or (sign == 1 and tmp > 7)):
# 不满足条件,已经溢出了。
return False
else:
# 满足条件,新增。
self.ans = self.ans*10 + tmp
self.count += 1
return True

def strToInt(self, _str: str) -> int:
sign = 1 # 整数符号 +1 or -1
FirstCharCheck = True # bool 用于检测第一个非空格字符,默认为True,当检测到第一个字符后设为False
for i in range(len(_str)):
# FirstCharCheck为True时 进行第一个字符检测
if FirstCharCheck:
if _str[i] == ' ': # 遇到空格跳过,直到遇到第一个非空格字符 不会修改FirstCharCheck
continue
elif _str[i] == '-' or _str[i] == '+': # 如果第一个字符为+/-号, 更新sign,
sign = 44-ord(_str[i]) # Return the Unicode code point for a one-character string. ‘+’的ASCII码是43 ,‘-’的ASCII码是45 所以 sign = 44-ord(‘+/-’)
if i+1 < len(_str) and not _str[i+1].isdigit(): # 判定+/-后字符是否为数字
return 0
elif _str[i].isdigit(): # 如果第一个字符为数字,更新ans
if not self.autoAdd(_str[i], sign):
return ((1<<31)-1) if sign == 1 else -(1<<31)
else: # 第一个字符为其他符号,return
return 0
FirstCharCheck = False # 判定第一个字符后,更新FirstCharCheck,避免重复检测
# 后续遇到数字字符,累加到self.ans。
elif _str[i].isdigit():
if not self.autoAdd(_str[i], sign):
return ((1<<31)-1) if sign == 1 else -(1<<31)
# ans = ans * 10 + (ord(_str[i]) - ord('0'))
else: # 后续遇到非数字字符,直接break,计算结果
break

self.ans *= sign # 先乘以+1/-1恢复正负号
return self.ans

剑指offer版本

LeetCode的题目相较于《剑指offer》的原题做了改动,这里放上符合《剑指offer》题目要求的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def string2int32(_str: str) -> int:
"""convert str to int
:param _str: str
:return int:
"""
# 输入类型检查
if not isinstance(_str, str): raise ValueError
# 长度特判
if len(_str) == 0: raise ValueError
flag = True # 默认为正数
ord_0 = ord('0')
ord_9 = ord('9')

# 输入数据检测
if _str[0] == '-':
_str = _str[1:]
flag = False
for _char in _str:
tmp = ord(_char)
if tmp < ord_0 or tmp > ord_9:
raise ValueError

# string to int
res = 0
count = 0
is_valid = True
# 注意:这里是从后往前遍历的
for item in _str[::-1]:
is_valid = True
# 做溢出判断 -2147483648 < int32 < 2147483647
# 负数溢出判定 or 正数溢出判定
tmp = (ord(item) - ord_0) * (10**count)
if (not flag and res < 147483648 and tmp <= 2000000000) or (flag and res < 147483647 and tmp <= 2000000000):
pass # 满足条件,不做处理。
else:
is_valid = False
if is_valid:
res += tmp
count += 1
else:
raise ValueError

return res if flag else (0-res)

有效语法糖

如何约束返回值的范围

通过使用Python内建的max和min函数可以用简洁的方法约束结果的范围

1
2
3
4
5
6
7
8
9
10
# result的结果只能在(MIN, MAX)之间,大于MAX就返回MAX,小于MIN就返回MIN
return max(min(result, MAX), MIN)
# 等效于
if result<MAX:
if result>MIN:
return result # MIN<result<MAX
else:
return MIN # result<=MIN
else:
return MAX # result>=MAX

Python自带内建函数去除字符串开头、结尾的空格

1
2
3
4
5
6
7
>>> a = '   112dgsd 232   sdfsaf   123666     '
>>> a.lstrip() # Return a copy of the string with leading whitespace removed.
'112dgsd 232 sdfsaf 123666 '
>>> a.rstrip() # Return a copy of the string with tailing whitespace removed.
' 112dgsd 232 sdfsaf 123666'
>>> a.strip() # Return a copy of the string with leading and trailing whitespace removed.
'112dgsd 232 sdfsaf 123666'

Python解包骚操作

将可迭代对象分解出来的一种方法。参考连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 1、用于基本的解包
>>> a, b, *c = [1,2,3,4]
>>> c
[3, 4]
# * 和 ** 只在“传参”时才有用。直接调用num = *num会报错 SyntaxError: can't use starred expression here
>>> num = ['123']
>>> num = *num
"SyntaxError: can't use starred expression here"
>>> num = int(*num)
>>> num
123

# 2、用于list连接
>>> list1 = [1,2,3]
>>> list2 = range(3,6)
>>> [*list1, *list2]
[1, 2, 3, 3, 4, 5]

# 3、用于参数传递
>>> def func(a, b, c):
print(a+b+c)


>>> parameter = {'a':1, 'b':2, 'c':3}
>>> func(**parameter) # ** 符号作用的对象是字典对象,它会自动解包成关键字参数 key=value 的格式
6
>>> func(a=1, b=2, c=3) # **parameter和a=1, b=2, c=3完全等效
6

不使用str()将单个int转成str

使用ord()函数,不调用int方法将单个数字char转化成int,将‘+’、‘-’转化成1、-1。ord() 函数是 chr() 函数(对于8位的ASCII字符串)或 unichr() 函数(对于Unicode对象)的配对函数,它以一个字符(长度为1的字符串)作为参数,返回对应的 ASCII 数值,或者 Unicode 数值,如果所给的 Unicode 字符超出了你的 Python 定义范围,则会引发一个 TypeError 的异常。

1
2
3
4
5
6
7
8
9
>>> a = '6'
>>> ord(a) - ord('0')
6
>>> sgin = '+'
>>> 44-ord(sgin) # ‘+’的ASCII码是43 ,‘-’的ASCII码是45 所以 sign = 44-ord(‘+/-’)
1
>>> sgin = '-'
>>> 44-ord(sgin)
-1

使用内建函数判断字符是否是数字

1
2
3
4
5
6
>>> a = 'a'
>>> a.isdigit()
False
>>> a = '1'
>>> a.isdigit()
True