题目描述

✅ 剑指 Offer 20. 表示数值的字符串

image-20261001230752544

image-20260928224404143

image-20260928224404145

题意分析

判断整个字符串是否符合十进制数的写法,不需要把它真正转换成数值。去掉首尾空白后,主体可以带一个开头的正负号,随后是整数或小数,还可以接一个由 e 或 E 引出的指数部分。

主体至少有一位数字,小数点最多一个,点的左右两侧允许有一侧没有数字。指数则必须是整数:可以有一个正负号,但必须至少有一位数字,不能带小数点。中间的空白或其他字符都不合法。

解法:一次扫描

核心思路

[!blue]

从左到右扫描,遇到每个字符时检查它是否允许出现在当前位置。后续判断只依赖此前是否出现数字、小数点或指数,不需要保存完整前缀,因此用几个布尔变量记录状态:

  • seenDigit:此前是否读到过数字。在进入指数前,它用来保证主体不是空的。
  • seenDot:主体是否已经出现小数点,防止出现第二个点。
  • seenExp:是否已经进入指数,防止重复指数符号,并禁止之后再出现小数点。
  • digitAfterExp:指数部分是否已经有数字。初始为真,因为没有指数时不需要额外检查;读到指数符号后先设为假,只有之后的数字能把它改回真。

数字在主体和指数中都允许;读到数字就更新相应状态。小数点只检查是否重复、是否已经进入指数,不要求点前必须有数字,最终的数字检查会保证主体至少有一位数字。

指数符号要求此前已有数字,且此前没有其他指数符号。这样指数之前一定有合法的数字主体,但读到指数本身还不能接受,需要继续等到指数数字。正负号只能位于字符串开头或紧跟 e/E,连续两个符号也会因为位置不符而被拒绝。

扫描中出现非法字符或非法位置立即返回假。若全部字符通过检查,重复的小数点、重复的指数、内部符号等问题已被排除,剩下只需确认必要数字齐全:返回 seenDigit && digitAfterExp。seenDigit 不会在进入指数时清零,指数是否完整由独立的 digitAfterExp 判断。

解题步骤

  1. 去掉首尾空白,若结果为空就返回假;Java 代码也先处理空引用。
  2. 初始化四个状态,从左到右逐个检查字符。
  3. 数字更新出现状态;小数点、指数、正负号分别执行上述位置检查,并在合法时更新状态。
  4. 不属于这些类别的字符直接拒绝;扫描结束后检查主体数字与指数数字是否齐全。

只有符号或只有小数点时,最终没有主体数字;以指数符号或指数正负号结尾时,指数数字尚未补齐。这些前缀在扫描过程中可能暂时合法,必须由结束检查拒绝。

代码实现

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)$ 的新字符串,Go TrimSpace 返回原字符串的一段,不复制字符内容。

关键点总结

[!green]

  • 位置规则检查“哪些字符不能出现”,结束时的两个数字状态检查“哪些字符必须出现”。
  • 主体可以含小数点,指数只能含整数数字和可选的开头符号,二者规则不同。
  • 这里只验证格式,不进行数值运算,不需要考虑字符串表示的数是否超出浮点范围。

易错点总结

[!yellow]

  • 仅检查字符属于数字、点和符号集合,无法检查位置。
  • 允许任意位置出现正负号,会误接受夹在数字之间的符号。
  • 直接套用浮点解析函数,可能接受 NaN、Infinity 等题目不允许的形式。

相似题目

题目 难度 关联与区别
8. 字符串转换整数 (atoi) 中等 原题按atoi规则解析合法前缀,本题验证整串数值格式且允许小数和指数,停止规则不同。
468. 验证IP地址 中等 同样需要严格的整串语法校验,IP题按段检查,本题维护符号、小数点和指数位置规则。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2022/39895546
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!