这是我参与更文挑战的第 23 天,活动详情查看 更文挑战
这个系列没啥花头,就是纯 leetcode 题目拆解分析,不求用骚气的一行或者小众取巧解法,而是用清晰的代码和足够简单的思路帮你理清题意。让你在面试中再也不怕算法笔试。
134. 字符串转换整数 (atoi) (string-to-integer-atoi)
标签
- 字符串
- 自动机
- 中等
题目
这里不贴题了,leetcode打开就行,题目大意:
请你来实现一个 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
。
注意:
- 本题中的空白字符只包括空格字符 ' ' 。
- 除前导空格或数字后的其余字符串外,请勿忽略 任何其他字符
示例 1:
输入:s = "42"
输出:42
解释:插入符号是当前读取的字符。
第 1 步:"42"(当前没有读入字符,因为没有前导空格)
^
第 2 步:"42"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
^
第 3 步:"42"(读入 "42")
^
解析得到整数 42 。
由于 "42" 在范围 [-231, 231 - 1] 内,最终结果为 42 。
示例 2:
输入:s = " -42"
输出:-42
解释:
第 1 步:" -42"(读入前导空格,但忽视掉)
^
第 2 步:" -42"(读入 '-' 字符,所以结果应该是负数)
^
第 3 步:" -42"(读入 "42")
^
解析得到整数 -42 。
由于 "-42" 在范围 [-231, 231 - 1] 内,最终结果为 -42 。
示例 3:
输入:s = "4193 with words"
输出:4193
解释:
第 1 步:"4193 with words"(当前没有读入字符,因为没有前导空格)
^
第 2 步:"4193 with words"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
^
第 3 步:"4193 with words"(读入 "4193";由于下一个字符不是一个数字,所以读入停止)
^
解析得到整数 4193 。
由于 "4193" 在范围 [-231, 231 - 1] 内,最终结果为 4193 。
示例 4:
输入:s = "words and 987"
输出:0
解释:
第 1 步:"words and 987"(当前没有读入字符,因为没有前导空格)
^
第 2 步:"words and 987"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
^
第 3 步:"words and 987"(由于当前字符 'w' 不是一个数字,所以读入停止)
^
解析得到整数 0 ,因为没有读入任何数字。
由于 0 在范围 [-231, 231 - 1] 内,最终结果为 0 。
示例 5:
输入:s = "-91283472332"
输出:-2147483648
解释:
第 1 步:"-91283472332"(当前没有读入字符,因为没有前导空格)
^
第 2 步:"-91283472332"(读入 '-' 字符,所以结果应该是负数)
^
第 3 步:"-91283472332"(读入 "91283472332")
^
解析得到整数 -91283472332 。
由于 -91283472332 小于范围 [-231, 231 - 1] 的下界,最终结果被截断为 -231 = -2147483648 。
基本思路
看到这种题,不要慌,而是感到高兴,一般长篇大论的题,说明限制多,反而会简单,只要按照题意做就行,主要是文字理解要准确,还有加上细心。
这题有一些简单的做法,但是我们本次目的是讲解下自动机,简单做法就直接看注释吧。
确定有限状态自动机
这里所说的 自动机 一般都指 确定有限状态自动机
首先理解一下自动机是用来干什么的:
自动机是一个对信号序列进行判定的数学模型。
- “信号序列”是指一连串有顺序的信号,例如字符串从前到后的每一个字符、数组从 1 到 n 的每一个数、数从高到低的每一位等。
- “判定”是指针对某一个命题给出或真或假的回答。有时我们需要对一个信号序列进行判定。
- 一个简单的例子就是判定一个二进制数是奇数还是偶数,较复杂的例子例如判定一个字符串是否回文,判定一个字符串是不是某个特定字符串的子序列等等。
需要注意的是,自动机只是一个 数学模型,而 不是算法
,也 不是数据结构
。实现同一个自动机的方法有很多种,可能会有不一样的时空复杂度。
简单来说它有下面3个特征
- 状态总数(state)是有限的。
- 任一时刻,只处在一种状态之中。
- 某种条件下,会从一种状态转变(transition)到另一种状态。
一个 确定有限状态自动机(DFA) 由以下五部分构成:
- 字符集(Σ),该自动机只能输入这些字符。
- 状态集合(Q),如果把一个 DFA 看成一张有向图,那么 DFA 中的状态就相当于图上的顶点。
- 起始状态(start),
start ∈ Q
是一个特殊的状态。 - 接受状态集合(F),
F⊆Q
,是一组特殊的状态。 - 转移函数(δ),δ 是一个接受两个参数返回一个值的函数,其中第一个参数和返回值都是一个状态,第二个参数是字符集中的一个字符。如果把一个 DFA 看成一张有向图,那么 DFA 中的转移函数就相当于顶点间的边,而每条边上都有一个字符。
就本题,我们的思考是:
- 我们一个个字符输入到状态机 (autoMap如何定义)
- 我们如何定义状态集合
- 输入字符使状态机状态如何转变 (转移函数如何写)
- 跟输入什么字符有关
- 跟原本状态有关
那,我们如何画这个图呢? 你先找张纸,跟着我的思路看看 首先我们有一个初始状态 start,这个状态就是啥都没有
start
- 如果我们来了一个空字符串 ' ',根据题意要舍去的,那其实啥都没干,现在还是初始,说明我们从
start --(' ')-->
start
输入' ' 等于没啥变化还是 start状态
_(' ')___
| |
| |
--start <-
- 如果来了个数字呢, 那么就会变成现在正处于数字状态,而这个数就是数位的第一个数,
start ---(数字)--> in_number状态
- 如果来了个特殊字符或者字母啥的,那么貌似直接结束了,输出 0,就是
end
状态start ---(other)-->end
- 如果是符号
(+/-)
,因为现在是start
,第一个是符号位没问题, 进入signed
状态,start ---(+、-)--> signed 状态
我们发现没有其他路线了,就定了这 4 个状态,下面是从 start 状态转移的图
_______ _______(数字)____> 【in_number】
| |
(' ')【start】 ---(+/-)---> 【signed】
| |
------- ---(其他特殊/字母) ---> 【end】
【】中表示状态, ()中表示输入字符,-> 表示转移后状态
你可以接着想 从 in_number状态输入转移箭头,从 signed状态转移等等,最后画出下面这个图。
然后也可以用表格
这个表格这么看,左边列表示现在状态,表头表示 输入的参数,然后行列交汇的格子表示转移后的状态
,非常的清楚了。
代码实现看下面非常清晰的注释。
写法实现
parseInt 方法
利用 parseInt
API 注意第二个参数是 进制
var myAtoi = function (str) {
const res = parseInt(str, 10);
const MAX_VALUE = 2**31 - 1
const MIN_VALUE = -(2**31)
// NAN 无法转换
if (isNaN(res)) {
return 0;
}
// 超出范围的情况,截断
if (res < MIN_VALUE || res > MAX_VALUE) {
return res < 0 ? MIN_VALUE : MAX_VALUE;
}
return res;
};
console.log(myAtoi('123'))
console.log(myAtoi('123ss'))
console.log(myAtoi('ss123'))
正则法
利用正则表达式匹配出题意要求的串,再转整数。
var myAtoi = function (str) {
// 正则匹配可能以 +/- 开头的,至少含一位的数字
let reg = new RegExp(/^[\+\-]?\d+/);
// 去除空格 trim()
let res = str.trim().match(reg);
if (res) {
// 转整形
if (res[0]*1 < 0) {
return Math.max(res[0]*1, -(2**31))
} else {
return Math.min(res[0]*1, 2**31 - 1)
}
} else {
return 0
}
}
console.log(myAtoi('123'))
console.log(myAtoi('123ss'))
console.log(myAtoi('ss123'))
自动机
class Automaton{
constructor() {
// 执行阶段,默认处于开始执行阶段
this.state = 'start';
// 符号,默认是正数
this.sign = 1;
// 数值变量,做中间累计使用
this.res = 0;
//
this.autoMap = 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) {
// 根据 char 获取 autoMap 的 index
if (char === ' ') {
return 0;
} else if (char === '-' || char === '+') {
return 1;
} else if (typeof Number(char) === 'number' && !isNaN(char)) {
return 2;
} else {
return 3;
}
}
// 字符转换执行函数
transform(char) {
// this.autoMap.get(this.state) 获取当前状态
// 根据传入 char 转换成下一个状态并覆盖当前状态
this.state = this.autoMap.get(this.state)[this.getIndex(char)];
if(this.state === 'in_number') {
// char当做个位
this.res = this.res * 10 + char * 1;
// 同样要做边界截断
if (this.sign === 1) {
this.res = Math.min(this.res, 2**31 - 1)
} else {
// 注意这个 - 号
this.res = - Math.max(-this.res, -(2**31))
}
} else if (this.state === 'signed') {
this.sign = char === '+' ? 1 : -1;
}
}
}
var myAtoi = function(str) {
// 生成自动机实例
let automaton = new Automaton();
// 一个个字符按序列输入自动机,进行状态转换
for(let char of str) {
automaton.transform(char);
}
// 最后自动机输出最后状态即可
return automaton.sign * automaton.res;
};
console.log(myAtoi('123'))
console.log(myAtoi('123ss'))
console.log(myAtoi('ss123'))
console.log(myAtoi("-91283472332"))
另外向大家着重推荐下这个系列的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列
今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦 点击此处交个朋友
Or 搜索我的微信号infinity_9368
,可以聊天说地
加我暗号 "天王盖地虎" 下一句的英文
,验证消息请发给我
presious tower shock the rever monster
,我看到就通过,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧