LeetCode 剑指 Offer 20. 表示数值的字符串
题目描述



题意分析
判断整个字符串是否符合十进制数的写法,不需要把它真正转换成数值。去掉首尾空白后,主体可以带一个开头的正负号,随后是整数或小数,还可以接一个由
e或E引出的指数部分。主体至少有一位数字,小数点最多一个,点的左右两侧允许有一侧没有数字。指数则必须是整数:可以有一个正负号,但必须至少有一位数字,不能带小数点。中间的空白或其他字符都不合法。
解法:一次扫描
核心思路
[!blue]
从左到右扫描,遇到每个字符时检查它是否允许出现在当前位置。后续判断只依赖此前是否出现数字、小数点或指数,不需要保存完整前缀,因此用几个布尔变量记录状态:
seenDigit:此前是否读到过数字。在进入指数前,它用来保证主体不是空的。seenDot:主体是否已经出现小数点,防止出现第二个点。seenExp:是否已经进入指数,防止重复指数符号,并禁止之后再出现小数点。digitAfterExp:指数部分是否已经有数字。初始为真,因为没有指数时不需要额外检查;读到指数符号后先设为假,只有之后的数字能把它改回真。数字在主体和指数中都允许;读到数字就更新相应状态。小数点只检查是否重复、是否已经进入指数,不要求点前必须有数字,最终的数字检查会保证主体至少有一位数字。
指数符号要求此前已有数字,且此前没有其他指数符号。这样指数之前一定有合法的数字主体,但读到指数本身还不能接受,需要继续等到指数数字。正负号只能位于字符串开头或紧跟
e/E,连续两个符号也会因为位置不符而被拒绝。扫描中出现非法字符或非法位置立即返回假。若全部字符通过检查,重复的小数点、重复的指数、内部符号等问题已被排除,剩下只需确认必要数字齐全:返回
seenDigit && digitAfterExp。seenDigit不会在进入指数时清零,指数是否完整由独立的digitAfterExp判断。
解题步骤
- 去掉首尾空白,若结果为空就返回假;Java 代码也先处理空引用。
- 初始化四个状态,从左到右逐个检查字符。
- 数字更新出现状态;小数点、指数、正负号分别执行上述位置检查,并在合法时更新状态。
- 不属于这些类别的字符直接拒绝;扫描结束后检查主体数字与指数数字是否齐全。
只有符号或只有小数点时,最终没有主体数字;以指数符号或指数正负号结尾时,指数数字尚未补齐。这些前缀在扫描过程中可能暂时合法,必须由结束检查拒绝。
代码实现
class Solution {
// 记录是否出现过数字、点号、指数以及指数后的数字。
public boolean isNumber(String s) {
if (s == null) {
return false;
}
s = s.trim();
if (s.length() == 0) {
return false;
}
boolean seenDigit = false;
boolean seenDot = false;
boolean seenExp = false;
boolean digitAfterExp = true;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c >= '0' && c <= '9') {
seenDigit = true;
if (seenExp) {
digitAfterExp = true;
}
} else if (c == '.') {
// 小数点不能重复,也不能出现在指数部分。
if (seenDot || seenExp) {
return false;
}
seenDot = true;
} else if (c == 'e' || c == 'E') {
if (seenExp || !seenDigit) {
return false;
}
seenExp = true;
// 刚读到指数符号时还不完整,后面必须再出现数字。
digitAfterExp = false;
} else if (c == '+' || c == '-') {
// 正负号只允许在字符串开头或紧跟指数符号。
if (i != 0 && s.charAt(i - 1) != 'e' && s.charAt(i - 1) != 'E') {
return false;
}
} else {
return false;
}
}
return seenDigit && digitAfterExp;
}
}
import "strings"
func isNumber(s string) bool {
// 记录是否出现过数字、点号、指数以及指数后的数字。
s = strings.TrimSpace(s)
if len(s) == 0 {
return false
}
seenDigit := false
seenDot := false
seenExp := false
digitAfterExp := true
for i := 0; i < len(s); i++ {
c := s[i]
if c >= '0' && c <= '9' {
seenDigit = true
if seenExp {
digitAfterExp = true
}
} else if c == '.' {
// 小数点不能重复,也不能出现在指数部分。
if seenDot || seenExp {
return false
}
seenDot = true
} else if c == 'e' || c == 'E' {
if seenExp || !seenDigit {
return false
}
seenExp = true
// 刚读到指数符号时还不完整,后面必须再出现数字。
digitAfterExp = false
} else if c == '+' || c == '-' {
// 正负号只允许在字符串开头或紧跟指数符号。
if i != 0 && s[i-1] != 'e' && s[i-1] != 'E' {
return false
}
} else {
return false
}
}
return seenDigit && digitAfterExp
}
复杂度分析
- 时间复杂度:$O(n)$,其中 $n$ 为字符串长度,首尾处理与逐字符扫描都是线性工作。
- 空间复杂度:扫描状态为 $O(1)$;Java
trim最坏生成长度为 $O(n)$ 的新字符串,GoTrimSpace返回原字符串的一段,不复制字符内容。
关键点总结
[!green]
- 位置规则检查“哪些字符不能出现”,结束时的两个数字状态检查“哪些字符必须出现”。
- 主体可以含小数点,指数只能含整数数字和可选的开头符号,二者规则不同。
- 这里只验证格式,不进行数值运算,不需要考虑字符串表示的数是否超出浮点范围。
易错点总结
[!yellow]
- 仅检查字符属于数字、点和符号集合,无法检查位置。
- 允许任意位置出现正负号,会误接受夹在数字之间的符号。
- 直接套用浮点解析函数,可能接受 NaN、Infinity 等题目不允许的形式。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 8. 字符串转换整数 (atoi) | 中等 | 原题按atoi规则解析合法前缀,本题验证整串数值格式且允许小数和指数,停止规则不同。 |
| 468. 验证IP地址 | 中等 | 同样需要严格的整串语法校验,IP题按段检查,本题维护符号、小数点和指数位置规则。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!