# 0065. 有效数字

### 题目地址(65. 有效数字)

<https://leetcode-cn.com/problems/valid-number/>

### 题目描述

```
有效数字（按顺序）可以分成以下几个部分：

一个 小数 或者 整数
（可选）一个 'e' 或 'E' ，后面跟着一个 整数

小数（按顺序）可以分成以下几个部分：

（可选）一个符号字符（'+' 或 '-'）
下述格式之一：
至少一位数字，后面跟着一个点 '.'
至少一位数字，后面跟着一个点 '.' ，后面再跟着至少一位数字
一个点 '.' ，后面跟着至少一位数字

整数（按顺序）可以分成以下几个部分：

（可选）一个符号字符（'+' 或 '-'）
至少一位数字

部分有效数字列举如下：

["2", "0089", "-0.1", "+3.14", "4.", "-.9", "2e10", "-90E3", "3e+7", "+6e-1", "53.5e93", "-123.456e789"]

部分无效数字列举如下：

["abc", "1a", "1e", "e3", "99e2.5", "--6", "-+3", "95a54e53"]

给你一个字符串 s ，如果 s 是一个 有效数字 ，请返回 true 。

 

示例 1：

输入：s = "0"
输出：true


示例 2：

输入：s = "e"
输出：false


示例 3：

输入：s = "."
输出：false


示例 4：

输入：s = ".1"
输出：true


 

提示：

1 <= s.length <= 20
s 仅含英文字母（大写和小写），数字（0-9），加号 '+' ，减号 '-' ，或者点 '.' 。
```

### 前置知识

* 暂无

### 公司

* 暂无

### 三个变量一次遍历

#### 思路

我们可以直接进行一次遍历，边遍历边判断是否合法。

如果要边遍历边判断是否合法则需要记录一些关键信息。比如，当我遍历途中遇到了 .，那么我实际上需要知道一些信息，比如前面是否已经出现过 . 了。如果已经出现过了，那么就可以得出结论，该数字非法。

除了前面是否出现 . 这样的信息，我们还需要关注其他信息。具体地，我们需要关注：

* .
* e/E
* 前面是否有数字

以上三个信息。 我们可以用三个变量，分别表示上一次遇到其的位置（索引），用 -1 表示还没有遇到。

实际上，这道题的关键点就是分析出哪些是非法，这样不是非法的，那么就是合法的。 之所以如此是因为合法的实在是太多了，我们没有办法一一判断，而只能从非法的角度入手。而非法的情况比较多，如何分类是个问题，这也是本题是困难难度的原因。

让我们来分析一下非法的情景。

* 点前面有 e 或者 点，比如 1.1.1 或者 3e5.2
* e 前面有 e ，比如 e12e。或者 e 前面没有数字，比如 e123
* `+ -` 前面要么是 e，要么其位于第一位
* 出现了非法字符。也就是出现了除了 +-eE 数字. 之外的字符

代码上，我们可以使用三个变量：

1. last\_dot 上一次遇到的 . 的位置
2. last\_e 上一次遇到的 e/E 的位置
3. last\_d 上一次遇到的数字的位置

接下来我们需要遍历字符串 s，遍历的同时记得更新三个变量。

* 如果我们遇到了字符 "."，那么需要前面没有 "."，也不能有 e/E，否则就不合法。
* 如果遇到了 e/E，那么前面不能有 e/E。除此之前前面还有有数字才行。
* 如果遇到了 +-，要么它是第一个字符，要么它前面是 e/E，否则不能合法
* 其他非数字字符均为不合法

#### 关键点

* 分析非法的情况，用三个变量记录上一次出现的点，指数，数字的位置来复制判断

#### 代码

```py
class Solution:
    def isNumber(self, s: str) -> bool:
        last_dot = last_e = last_d = -1
        for i, c in enumerate(s):
            if c.isdigit():
                last_d = i
            elif c == '.':
                if last_dot != -1 or last_e != -1: return False
                last_dot = i
            elif c.lower() == 'e':
                if last_d == -1 or last_e != -1: return False
                last_e = i
            elif c == '+' or c == '-':
                if i == 0 or s[i-1].lower() == 'e':
                    continue
                else:
                    return False
            else:
                return False

        return s[-1].isdigit() or (s[-1] == '.' and last_d != -1)
```

**复杂度分析**

令 n 为数组长度。

* 时间复杂度：$O(n)$
* 空间复杂度：$O(1)$

### 状态机

#### 思路

![](https://p.ipic.vip/tzx4ia.jpg)

对于状态机，我们需要解决的就是：

1. 状态有哪些
2. 选择有哪些

> 和动态规划是类似的

对于这道题来说，打底的状态就是各种字符的类型。即：

* 数字
* .
* eE
* +-

打底就是这四种。

> 我们没有必要将 eE 或者 +- 进行区分，这是因为在这里二者的逻辑不会有差别。

那么这四种就够了。这是不够的。这是因为题目描述决定的。比如题目说了 e 后面不能是小数。 那么对于 . 来说，

* 我们就需要**分裂** 为两种状态： e 前面的 . 和 e 后面的 .。
* 类似地，+- 号，我们需要区分是第一位还是 e 后面的（紧邻），这是因为第一位后面可以跟 . ，而 e 后面紧邻的不可以。
* 数字也是一样。 由于 e 后面不能有点，也需要进行类似的**分裂**

最后一个比较容易漏掉，我们需要一种数字状态，这个数字状态后面只能跟数字，不能跟其他。比如 +2e+3 ，这个时候的 3 后面就只能是数字了，其他都是非法的。

对于这道题来说：

* 图中黄色的四个部分就是选择。由于 +-，以及 \[1-9] 对我们的算法没有影响，因此没有单独抽离出来，而是将其归为一类。
* 图中虚线部分就是状态。

不难看出，"." 前后的状态选择是不同的。因此除了："+-", "\[1-9]", "e/E", "." 这几种基本状态，还要分别对 \[1-9], e/E 进行区分是 “.”前还是后。从左到右我将其进行编号，靠左的是 1，靠右的是 2, 因此就有了 sign1,digit1, exp, D digit2 exp sign2 D 的状态命名。

> 注意这里是 D，不是 digit2。因为 digit2 可以接 E/e，因此需要单独定义一种状态

另外由于：dot 前面和后面必须有至少一个数字，并且有没有数字对选择也有影响，因此我们也需要对此区分。我这里用 dot1 表示前面有数字的 dot，dot2 表示前面没有数字的 dot

关于如何转化，我就不一一分析了，大家直接看代码吧。虽然思路不难理解，但是细节还是蛮多的，大家自己写写就知道了。

#### 关键点

* 建立状态机模型
* 如果知道一共有多少状态

#### 代码

代码上，我们 xxx1 表示前面的 xxx，xxx2 表示后面的 xxx。D 表示只能跟数字

* 语言支持：Python3

Python3 Code:

```python

class Solution:
    def isNumber(self, s: str) -> bool:
        # 任何状态机的核心都是建立如下的状态机模型
        states = {
            "start": {"SIGN":"sign1",  "DIGIT":"digit1",  "DOT":"dot1"},
            "sign1": {"DIGIT":"digit1",  "DOT":"dot1"},
            "sign2": {"DIGIT":"D"},
            "digit1": {"DIGIT":"digit1",  "DOT":"dot2",  "EXP":"exp",  "END": True},
            "digit2": {"DIGIT":"digit2",  "EXP":"exp",  "END": True},
            "dot1": {"DIGIT":"digit2"}, # 前面没数字
            "dot2": {"DIGIT":"digit2",  "EXP":"exp",  "END": True}, # 前面有数字
            "exp": {"SIGN":"sign2",  "DIGIT":"D"},
            "D": {"DIGIT":"D",  "END": True}
        }

        def get(ch):
            if ch == ".": return "DOT"
            elif ch in "+-": return "SIGN"
            elif ch in "Ee": return "EXP"
            elif ch.isdigit(): return "DIGIT"

        state = "start"
        for c in s:
            state = states[state].get(get(c))
            if not state: return False

        return "END" in states[state]

```

**复杂度分析**

令 n 为数组长度。

* 时间复杂度：$O(n)$
* 空间复杂度：虽然使用了 states 存放状态，但是其不会随着数字增大而变大，而是一个定值，因此空间复杂度为 $O(1)$

> 此题解由 [力扣刷题插件](https://leetcode-pp.github.io/leetcode-cheat/?tab=solution-template) 自动生成。

力扣的小伙伴可以[关注我](https://leetcode-cn.com/u/fe-lucifer/)，这样就会第一时间收到我的动态啦\~

以上就是本文的全部内容了。大家对此有何看法，欢迎给我留言，我有时间都会一一查看回答。更多算法套路可以访问我的 LeetCode 题解仓库：<https://github.com/azl397985856/leetcode> 。 目前已经 40K star 啦。大家也可以关注我的公众号《力扣加加》带你啃下算法这块硬骨头。

关注公众号力扣加加，努力用清晰直白的语言还原解题思路，并且有大量图解，手把手教你识别套路，高效刷题。

![](https://p.ipic.vip/57bg3l.jpg)
