8-字符串转换整数(atoi)

题目

请你来实现一个myAtoi(string s)函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。

函数myAtoi(string s) 的算法如下:

读入字符串并丢弃无用的前导空格
检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。
读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。
将前面步骤读入的这些数字转换为整数(即,”123” -> 123, “0032” -> 32)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。
如果整数数超过 32 位有符号整数范围 [−2^31, 2^31− 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −2^31 的整数应该被固定为 −2^31 ,大于 2^31− 1 的整数应该被固定为 2^31− 1 。
返回整数作为最终结果。

注意:
本题中的空白字符只包括空格字符 ‘ ‘ 。
除前导空格或数字后的其余字符串外,请勿忽略 任何其他字符。

使用parseInt解决

(投机取巧了),没有按照人家官方说的那种方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @param {string} s
* @return {number}
*/
var myAtoi = function(s) {
let num = parseInt(s, 10);
if(isNaN(num)){
return 0;
} else if(num < Math.pow(-2, 31) || num > (Math.pow(2, 31)-1)){
return num < Math.pow(-2, 31) ? Math.pow(-2, 31) : (Math.pow(2, 31)-1);
} else {
return num;
}
};

自动机

  • 自动机
    我们的程序在每个时刻有一个状态s,每次从序列中输入一个字符c,并根据字符c 转移到下一个状态s’。这样,我们只需要建立一个覆盖所有情况的从s与c映射到s’的表格即可解决题目中的问题。

状态分析

首先,从题意中,我们很轻易地可以知道,字符串str中的每个字符,都有可能是以下的四种类型中的一种:

  • 空格字符’ ‘(Space)
  • 正负号+和-(Sign)
  • 字符串型的数值(Number)
  • 除以上三种情况外的任何情况(Other)

阶段分析

如果想要将字符串转换为整数,那么必然会经历这四个有序的阶段:

1、开始转换(start)
2、判断正负(signed)
3、生成数值(in_number)
4、结束转换(end)

生成自动机

这步是最为关键的一步,它将状态和阶段巧妙地结合了起来。

话不多说,让我们先来看一个表格:

‘’(Space) +/-(Sign) Number Other
start start signed in_number end
signed end end in_number end
in_number end end in_number end
end end end end

现在来说明下这个表格的含义。

不同的行象征不同执行阶段:

  • 第0行:开始转换阶段
  • 第1行:判断正负阶段
  • 第2行:生成数值阶段
  • 第3行:结束转换阶段

不同的列象征不同的字符类型:

  • 第0列:字符为空格
  • 第1列:字符为正、负号
  • 第2列:字符为字符型数值
  • 第3列:字符为其他形式
    由行、列确定的坐标,象征着下一个字符所处的执行阶段。

例如,官方的str例子:” -42”,就会进行如下的转换流程(请结合表格认真阅读,这对你理解“自动机”的概念很有帮助):

  1. 一开始,必然是开始阶段,所以从第0行,即[0, ?]
  2. 第一个字符是空格,找到空格所在的列,即[?, 0]
  3. 结合行、列,即[0, 0],发现将为下一个字符执行start阶段
  4. 所以第二个字符还是从第0行开始,即[0, ?]
  5. 第二个字符是空格,空格的列数是[?, 0]
  6. 所以第三个字符的还是执行start阶段([0, 0])
  7. …(空格的分析不再赘述)
  8. 发现字符是负号-,而此时是在第0行(之前空格的原因),所以坐标是[0, 1],
  9. 那么可以下一个字符的执行阶段是signed,即第1列([1, ?])
  10. 接下来的字符是字符型的4,则列数是[?, 2]
  11. 所以坐标确定为[1, 2],则下一个字符的执行阶段是in_number,即[2, ?]
  12. 这次的字符还是字符型(2),则依旧定位到[?, 2],则下一个字符执行in_number阶段
  13. 没有字符了,遍历结束
  14. 依据负号和数值,得出转换结果为-42

现在你理解了如何由状态和阶段构建自动机了吧?
以上的这些步骤描述,其实就是LeetCode官方图例所表达的意思。

代码实现

话不多说,看代码吧~

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/**
* @param {string} str
* @return {number}
*/
var myAtoi = function(str) {
// 自动机类
class Automaton{
constructor() {
// 执行阶段,默认处于开始执行阶段
this.state = 'start';
// 正负符号,默认是正数
this.sign = 1;
// 数值,默认是0
this.answer = 0;
/*
关键点:
状态和执行阶段的对应表
含义如下:
[执行阶段, [空格, 正负, 数值, 其他]]
*/
this.map = new Map([
['start', ['start', 'signed', 'in_number', 'end']],
['signed', ['end', 'end', 'in_number', 'end']],
['in_number', ['end', 'end', 'in_number', 'end']],
['end', ['end', 'end', 'end', 'end']]
])
}

// 获取状态的索引
getIndex(char) {
if (char === ' ') {
// 空格判断
return 0;
} else if (char === '-' || char === '+') {
// 正负判断
return 1;
} else if (typeof Number(char) === 'number' && !isNaN(char)) {
// 数值判断
return 2;
} else {
// 其他情况
return 3;
}
}

/*
关键点:
字符转换执行函数
*/
get(char) {
/*
易错点:
每次传入字符时,都要变更自动机的执行阶段
*/
this.state = this.map.get(this.state)[this.getIndex(char)];

if(this.state === 'in_number') {
/*
小技巧:
在JS中,对字符串类型进行减法操作,可以将得到一个数值型(Number)的值

易错点:
本处需要利用括号来提高四则运算的优先级
*/
this.answer = this.answer * 10 + (char - 0);

/*
易错点:
在进行负数比较时,需要将INT_MIN变为正数
*/
this.answer = this.sign === 1 ? Math.min(this.answer, Math.pow(2, 31) - 1) : Math.min(this.answer, -Math.pow(-2, 31));
} else if (this.state === 'signed') {
/*
优化点:
对于一个整数来说,非正即负,
所以正负号的判断,只需要一次。
故,可以降低其判断的优先级
*/
this.sign = char === '+' ? 1 : -1;
}
}
}

// 生成自动机实例
let automaton = new Automaton();

// 遍历每个字符
for(let char of str) {
// 依次进行转换
automaton.get(char);
}

// 返回值,整数 = 正负 * 数值
return automaton.sign * automaton.answer;
};