有效数字
在 LeetCode 上查看 ↗语音讲解
开车或通勤时可听,跟着思路走一遍
题目描述
给定一个字符串 s,返回 s 是否是一个有效数字。
例如,下面的都是有效数字:"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"。
有效数字可由「整数 + 可选指数」或「小数 + 可选指数」构成;指数部分为 e/E 后跟整数。
示例 1
示例 2
示例 3
模拟答题者思考
1. 最直接:用正则或语言内置解析(如 Python float(s))——面试/竞赛通常不允许,且手写正则极易漏边界(如 "4."、"-.9")。
2. 重复在哪里?每读一个字符,「当前已读前缀是否仍可能是有效数字的一部分」只取决于当前所处阶段(有没有符号、整数、小数点、指数等),与更早细节无关——适合有限状态机。
3. 拆成 8 个状态:起始(0) → 可选符号(1) → 整数(2) → 仅点(3) → 小数(4) → 指数 e(5) → 指数符号(6) → 指数整数(7)。合法结尾必须是 2、4 或 7(至少读过一位数字)。
4. 手推 "-.9":'-' 0→1,'.' 1→3,'9' 3→4,结束 state=4 ∈ VALID → true。手推 "1e":'1' 0→2,'e' 2→5,结束 state=5 ∉ VALID → false。
5. 也可用 seen_digit / seen_dot / seen_e 三旗标逐字符模拟,但状态机转移表更不易漏 case;本题困难主要体现在边界枚举完整。
变量语义(先读这三句再编码)
| 变量 | 类型 | 语义(三句法) |
|---|---|---|
state | int | 定义:当前解析所处的 DFA 状态(0 起始 → 1 已见符号 → 2 整数 → 3 仅小数点 → 4 小数 → 5 已见 e → 6 指数符号 → 7 指数整数) 维护:每读一个字符,按转移表查下一状态;非法转移置 -1更新:遍历完整个字符串后, state ∈ {2, 4, 7} 才为有效数字 |
TRANS[state] | dict | 定义:状态转移表,键为字符类别(sign/digit/dot/e)维护:编码题目规则——如状态 2 可读数字/小数点/指数,状态 3 只能接数字( ".5" 型)更新:只读常量,不随输入改变;当前字符映射到类别后查表得下一 state |
c | char | 定义:当前扫描的字符 维护:按 +-、eE、.、数字四类分发到转移键更新:从左到右逐字符处理,遇未知字符直接返回 false |
VALID | set | 定义:合法终态集合 {2, 4, 7}(纯整数、小数、带指数)维护:状态 1/3/5/6 不能作为结尾——如 "+"、"."、"1e"、"1e+" 均非法更新:遍历结束后用 state in VALID 判定 |
落码步骤
1. 定义 8 行转移表 TRANS 与合法终态 VALID = {2, 4, 7}
2. state = 0,从左到右遍历 s 每个字符 c
3. 将 c 映射为类别:数字 → digit,+- → sign,. → dot,eE → e;未知字符返回 false
4. state = TRANS[state].get(类别, -1),若为 -1 说明非法转移,返回 false
5. 遍历结束后返回 state in VALID
代码实现
class Solution:
def isNumber(self, s: str) -> bool:
# 状态: 0起始 1符号 2整数 3仅点 4小数 5指数e 6指数符号 7指数整数
TRANS = [
{'sign': 1, 'dot': 3, 'digit': 2}, # 0
{'digit': 2, 'dot': 3}, # 1
{'digit': 2, 'dot': 4, 'e': 5}, # 2
{'digit': 4}, # 3
{'digit': 4, 'e': 5}, # 4
{'sign': 6, 'digit': 7}, # 5
{'digit': 7}, # 6
{'digit': 7}, # 7
]
VALID = {2, 4, 7}
state = 0
for c in s:
if c.isdigit():
kind = 'digit'
elif c in '+-':
kind = 'sign'
elif c == '.':
kind = 'dot'
elif c in 'eE':
kind = 'e'
else:
return False
state = TRANS[state].get(kind, -1)
if state == -1:
return False
return state in VALID
class Solution {
public:
bool isNumber(string s) {
// 状态: 0起始 1符号 2整数 3仅点 4小数 5指数e 6指数符号 7指数整数
vector<unordered_map<string, int>> trans = {
{{"sign",1}, {"dot",3}, {"digit",2}},
{{"digit",2}, {"dot",3}},
{{"digit",2}, {"dot",4}, {"e",5}},
{{"digit",4}},
{{"digit",4}, {"e",5}},
{{"sign",6}, {"digit",7}},
{{"digit",7}},
{{"digit",7}},
};
unordered_set<int> valid = {2, 4, 7};
int state = 0;
for (char c : s) {
string kind;
if (isdigit(c)) kind = "digit";
else if (c == '+' || c == '-') kind = "sign";
else if (c == '.') kind = "dot";
else if (c == 'e' || c == 'E') kind = "e";
else return false;
auto it = trans[state].find(kind);
if (it == trans[state].end()) return false;
state = it->second;
}
return valid.count(state);
}
};
// 时间 O(n),空间 O(1)
复杂度分析
O(n)
O(1)
常见坑
终态判断遗漏:仅 "+"、"."、"1e"、"1e+" 等中间态不能返回 true,必须 state ∈ {2,4,7}。
指数后不能再有小数点:"99e2.5" 在读到 . 时状态 7 无 dot 转移,应判 false。
符号位置错误:"--6"、"-+3" 在状态 2 遇到第二个 sign 无转移;指数符号只能在状态 5 后出现,不能出现在整数中间。
必测边界 Case
"0" → true(示例 1,纯整数)
"e" → false(示例 2,无任何数字)
"." → false(示例 3,点后无数字)
"-.9" → true,"+.5" → true(符号后直接小数点再接数字)
"4." → true(状态 2 遇 . 进入 4,无需小数部分)
"1e" → false,"1e+" → false(指数部分必须有至少一位数字)