题目描述

✅ 65. 有效数字

image-20260928224404143

image-20260928224404145

题意分析

判断整个字符串是否符合题目定义的十进制数字格式。主体可以是带可选正负号的整数或小数,也可以在后面追加由 e 或 E 引导的指数;指数部分只能是带可选正负号的整数。

主体的小数点最多一个,点前或点后允许一侧没有数字,但主体整体必须至少含一个数字。若出现指数符号,它后面也必须至少有一个数字,不能只有符号。题目验证的是整串语法,不是读取合法前缀,也不要求把它转换为某种有限范围的数值类型。

解法:一次遍历状态判断

核心思路

[!blue]

逐个字符扫描,只需保存三个状态:seenDigit 表示当前部分已经出现数字,seenDot 表示主体已经出现小数点,seenExp 表示已经进入指数部分。数字、点、指数符号和正负号分别按自己的位置限制检查,其余字符直接失败。

遇到数字时,把 seenDigit 设为真。遇到小数点时,要求此前没有点且还没有指数;点本身不算数字,因此只设置 seenDot,不改变数字标记。这样可以允许点出现在主体数字之前或之后,同时仍能在最后拒绝完全没有数字的主体。

遇到 e 或 E 时,要求这是第一次指数符号,且此前的主体已经有数字。检查通过后设置 seenExp,并将 seenDigit 清空:主体已确认完整,接下来同一个标记改为记录指数部分是否出现数字。因为指数中不允许小数点,之后的点会由 seenExp 直接拒绝。

正负号只允许位于整个字符串开头,或紧跟指数符号之后。检查它的下标和前一个字符就足够了:第二个连续符号的前一位不是指数符号,因此会被拒绝,无需再保存独立的符号计数。

任何局部规则被违反就立即返回假。扫描结束后返回 seenDigit:没有指数时,它保证主体含数字;有指数时,它保证指数也已补上数字。结合扫描中对重复点、重复指数、符号位置和非法字符的限制,就覆盖了完整格式要求。

解题步骤

  1. 将数字、小数点、指数三个标记初始化为假。
  2. 遇到数字就设置当前部分的数字标记。
  3. 遇到点则检查尚无点、尚无指数;遇到指数则检查主体已有数字且此前没有指数,再清空数字标记。
  4. 遇到正负号则检查它是否在开头或紧随指数;其他字符直接拒绝。
  5. 全部字符处理完后,返回当前部分是否已有数字。

代码实现

class Solution {
    // 小数点只能出现在底数部分,且最多出现一次。
    public boolean isNumber(String s) {
        boolean seenDigit = false;
        boolean seenDot = false;
        boolean seenExp = false;

        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);

            if (c >= '0' && c <= '9') {
                seenDigit = true;
            } else if (c == '.') {
                if (seenDot || seenExp) {
                    return false;
                }

                seenDot = true;
            } else if (c == 'e' || c == 'E') {
                // 进入指数前必须已有底数数字,且指数只能出现一次
                if (seenExp || !seenDigit) {
                    return false;
                }

                seenExp = true;
                // 底数已经验证,重新记录指数部分是否出现数字
                seenDigit = 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;
    }
}
func isNumber(s string) bool {
    // 小数点只能出现在底数部分,且最多出现一次。
    seenDigit := false
    seenDot := false
    seenExp := false

    for i := 0; i < len(s); i++ {
        c := s[i]
        if c >= '0' && c <= '9' {
            seenDigit = true
        } else if c == '.' {
            if seenDot || seenExp {
                return false
            }
            seenDot = true
        } else if c == 'e' || c == 'E' {
            // 进入指数前必须已有底数数字,且指数只能出现一次
            if seenExp || !seenDigit {
                return false
            }
            seenExp = true
            // 底数已经验证,重新记录指数部分是否出现数字
            seenDigit = false
        } else if c == '+' || c == '-' {
            if i > 0 && s[i-1] != 'e' && s[i-1] != 'E' {
                return false
            }
        } else {
            return false
        }
    }

    // 最后一段必须有数字,单独的符号或点不能通过
    return seenDigit
}

复杂度分析

  • 时间复杂度:$O(n)$,每个字符只检查一次,单次判断为常数时间。
  • 空间复杂度:$O(1)$,只使用三个布尔状态和扫描下标,不构造子串或解析数值。

关键点总结

[!green]

  • 数字标记随主体、指数阶段切换,点和指数标记则记录全串已发生的结构。
  • 指数出现时先确认主体,再清空数字标记等待指数数字。
  • 扫描检查字符能否出现在当前位置,结尾再检查最后部分是否完整。

易错点总结

[!yellow]

  • 进入指数后不清空数字标记,会让主体中的数字掩盖指数缺少数字的问题。
  • 要求小数点前必须已有数字,或要求点后必须有数字,会错误拒绝题目允许的一侧缺省形式。
  • 只检查小数点是否重复,没有检查是否进入指数,会接受带小数指数的错误格式。
  • 允许正负号出现在任意位置,会破坏主体或指数内部的连续数字结构。
  • 一遇到不认识的字符就返回前面部分的解析结果,变成了前缀转换,而本题要求整串有效。
  • 将字符串转换为浮点数是否成功作为唯一依据,会混入语言库自己的语法和数值范围限制。

相似题目

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