给定字符串,例如:
abc56ghf-78.3hjk+65.6aaa
需要从中提取所有合法数字(支持正负号、小数点),找到最大值。
上例中的数字有:56、-78.3、+65.6,最大值是 65.6。
-
字符级扫描,一次遍历字符串,时间复杂度 O(n)。
-
当遇到如下三种情况之一时,尝试解析数字:
- 当前字符是数字
0-9 - 当前字符是
+或-
- 当前字符是数字
-
判断
+/-是否是数字的符号:- 只能出现在数字最前面
- 后面必须跟数字或小数点,否则视为普通字符
-
从当前位置继续向后扫描:
- 允许出现 0 或 1 个小数点
. - 必须包含至少一个数字(整数部分或小数部分)
- 一旦遇到不是数字、不是小数点的字符,就认为当前数字结束
- 允许出现 0 或 1 个小数点
-
用
strconv.ParseFloat把切片出来的子串转成float64,维护当前最大值。 -
为了避免重复解析,成功解析一个数字后,把主循环的索引
i跳到该数字末尾。
这样实现简单又高效,且只有一次线性扫描。
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)
}运行结果:
字符串:abc56ghf-78.3hjk+65.6aaa
最大数字为:65.6
-
一次线性扫描 O(n) 不使用正则等复杂工具,直接遍历字符串,遇到“数字或正负号”才尝试解析数字。
-
正负号的判断
+/-被视为数字符号的前提:- 本身不是最后一个字符
- 后一个字符是数字或小数点 否则当普通字符跳过。
-
小数点处理
- 最多允许出现一个
. - 统计数字个数
digitCount,避免只解析出一个孤立的小数点"."。
- 最多允许出现一个
-
避免重复解析
- 一旦识别出
[start, end)这一段是数字串,就把i设置为end - 1,主循环i++后从数字后面继续扫描,避免重复。
- 一旦识别出
-
健壮性
- 通过
digitCount保证至少有一个数字 - 对
ParseFloat出错做防御性处理(理论上不会错)
- 通过