前端算法必刷题系列[70]

389 阅读8分钟

这是我参与更文挑战的第 23 天,活动详情查看 更文挑战

这个系列没啥花头,就是纯 leetcode 题目拆解分析,不求用骚气的一行或者小众取巧解法,而是用清晰的代码和足够简单的思路帮你理清题意。让你在面试中再也不怕算法笔试。

134. 字符串转换整数 (atoi) (string-to-integer-atoi)

标签

  • 字符串
  • 自动机
  • 中等

题目

leetcode 传送门

这里不贴题了,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 的每一个数、数从高到低的每一位等。
  • 判定”是指针对某一个命题给出或真或假的回答。有时我们需要对一个信号序列进行判定。
    • 一个简单的例子就是判定一个二进制数是奇数还是偶数,较复杂的例子例如判定一个字符串是否回文,判定一个字符串是不是某个特定字符串的子序列等等。

需要注意的是,自动机只是一个 数学模型,而 不是算法,也 不是数据结构。实现同一个自动机的方法有很多种,可能会有不一样的时空复杂度。

image.png

简单来说它有下面3个特征

  • 状态总数(state)是有限的。
  • 任一时刻,只处在一种状态之中。
  • 某种条件下,会从一种状态转变(transition)到另一种状态。

一个 确定有限状态自动机(DFA) 由以下五部分构成:

  • 字符集(Σ),该自动机只能输入这些字符。
  • 状态集合(Q),如果把一个 DFA 看成一张有向图,那么 DFA 中的状态就相当于图上的顶点。
  • 起始状态(start),start ∈ Q一个特殊的状态。
  • 接受状态集合(F),F⊆Q,是一组特殊的状态。
  • 转移函数(δ),δ 是一个接受两个参数返回一个值的函数,其中第一个参数和返回值都是一个状态,第二个参数是字符集中的一个字符。如果把一个 DFA 看成一张有向图,那么 DFA 中的转移函数就相当于顶点间的边,而每条边上都有一个字符。

就本题,我们的思考是:

  1. 我们一个个字符输入到状态机 (autoMap如何定义)
  • 我们如何定义状态集合
  1. 输入字符使状态机状态如何转变 (转移函数如何写)
  • 跟输入什么字符有关
  • 跟原本状态有关

那,我们如何画这个图呢? 你先找张纸,跟着我的思路看看 首先我们有一个初始状态 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状态转移等等,最后画出下面这个图。

image.png

然后也可以用表格

image.png

这个表格这么看,左边列表示现在状态,表头表示 输入的参数,然后行列交汇的格子表示转移后的状态,非常的清楚了。

代码实现看下面非常清晰的注释。

写法实现

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,我看到就通过,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧

参考