# 给定一字符串,包含数字、字母、小数点和正负号,从中提取最大的数字 abc56ghf-78.3hjk+65.6aaa 用高效的方式去做。 ## 解析 给定字符串,例如: ```text abc56ghf-78.3hjk+65.6aaa ``` 需要从中提取所有合法数字(支持正负号、小数点),找到最大值。 上例中的数字有:`56`、`-78.3`、`+65.6`,最大值是 `65.6`。 --- ## 思路设计 1. **字符级扫描**,一次遍历字符串,时间复杂度 O(n)。 2. 当遇到如下三种情况之一时,尝试解析数字: * 当前字符是数字 `0-9` * 当前字符是 `+` 或 `-` 3. 判断 `+` / `-` 是否是数字的符号: * 只能出现在数字最前面 * 后面必须跟数字或小数点,否则视为普通字符 4. 从当前位置继续向后扫描: * 允许出现 **0 或 1 个小数点** `.` * 必须包含至少一个数字(整数部分或小数部分) * 一旦遇到不是数字、不是小数点的字符,就认为当前数字结束 5. 用 `strconv.ParseFloat` 把切片出来的子串转成 `float64`,维护当前最大值。 6. 为了避免重复解析,成功解析一个数字后,把主循环的索引 `i` 跳到该数字末尾。 这样实现简单又高效,且只有一次线性扫描。 --- ## Go 实现代码 ```go package main import ( "fmt" "strconv" "unicode" ) // MaxNumberInString 在字符串中找到最大的数字(支持正负号和小数) // 返回值:最大值、是否找到任何数字 func MaxNumberInString(s string) (float64, bool) { n := len(s) if n == 0 { return 0, false } var maxVal float64 hasNumber := false // i 是当前扫描位置 for i := 0; i < n; i++ { ch := s[i] // 只在以下三种情况尝试解析数字: // 1. 当前是数字 // 2. 当前是 '+' 或 '-' if !isDigit(ch) && ch != '+' && ch != '-' { continue } // start 是数字串的起始位置 start := i // 如果是符号位,判断后面是否有可能是数字 if ch == '+' || ch == '-' { // 如果符号是最后一个字符, 不可能构成数字 if i+1 >= n { continue } next := s[i+1] // 如果符号后既不是数字也不是小数点, 不当作数字处理 if !isDigit(next) && next != '.' { continue } // 有可能是数字,从符号开始解析 i++ // 移动到符号后的第一个字符位置 } // 从 i 开始向后解析数字主体(整数 + 可选小数部分) dotSeen := false // 是否已经遇到过小数点 digitCount := 0 // 记录总共遇到的数字个数 j := i // j 指向正在处理的位置 for ; j < n; j++ { c := s[j] if isDigit(c) { digitCount++ continue } if c == '.' { // 不允许出现两个以上小数点 if dotSeen { break } dotSeen = true continue } // 遇到非数字非小数点,数字结束 break } // 当前数字子串的结束位置是 j(不包含 j) end := j // 如果没有任何数字(可能只有一个 ".")则跳过 if digitCount == 0 { // 恢复 i 的位置(此时 i 至少已经在符号后) i = start continue } // 子串范围是 [start, end) numStr := s[start:end] // 尝试转换成 float64 val, err := strconv.ParseFloat(numStr, 64) if err != nil { // 理论上 digitCount > 0 且最多一个 '.' 的情况下不应出错, // 出错就跳过这个子串。 i = end - 1 continue } if !hasNumber || val > maxVal { maxVal = val hasNumber = true } // 跳过已经解析过的这段数字,下一个循环从 end 开始 i = end - 1 } return maxVal, hasNumber } // isDigit 判断一个 byte 是否是十进制数字 func isDigit(b byte) bool { // 使用 unicode.IsDigit 也可以,但这里只处理 ASCII 数字更快 return b >= '0' && b <= '9' // 或者:return unicode.IsDigit(rune(b)) } func main() { s := "abc56ghf-78.3hjk+65.6aaa" maxVal, ok := MaxNumberInString(s) if !ok { fmt.Println("字符串中未找到任何数字") return } fmt.Printf("字符串:%s\n", s) fmt.Printf("最大数字为:%v\n", maxVal) } ``` 运行结果: ```text 字符串:abc56ghf-78.3hjk+65.6aaa 最大数字为:65.6 ``` --- ## 关键逻辑解析 1. **一次线性扫描 O(n)** 不使用正则等复杂工具,直接遍历字符串,遇到“数字或正负号”才尝试解析数字。 2. **正负号的判断** `+` / `-` 被视为数字符号的前提: * 本身不是最后一个字符 * 后一个字符是数字或小数点 否则当普通字符跳过。 3. **小数点处理** * 最多允许出现一个 `.` * 统计数字个数 `digitCount`,避免只解析出一个孤立的小数点 `"."`。 4. **避免重复解析** * 一旦识别出 `[start, end)` 这一段是数字串,就把 `i` 设置为 `end - 1`,主循环 `i++` 后从数字后面继续扫描,避免重复。 5. **健壮性** * 通过 `digitCount` 保证至少有一个数字 * 对 `ParseFloat` 出错做防御性处理(理论上不会错)