Skip to content

Latest commit

 

History

History
199 lines (154 loc) · 5 KB

File metadata and controls

199 lines (154 loc) · 5 KB

给定一字符串,包含数字、字母、小数点和正负号,从中提取最大的数字 abc56ghf-78.3hjk+65.6aaa 用高效的方式去做。

解析

给定字符串,例如:

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 实现代码

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

关键逻辑解析

  1. 一次线性扫描 O(n) 不使用正则等复杂工具,直接遍历字符串,遇到“数字或正负号”才尝试解析数字。

  2. 正负号的判断 + / - 被视为数字符号的前提:

    • 本身不是最后一个字符
    • 后一个字符是数字或小数点 否则当普通字符跳过。
  3. 小数点处理

    • 最多允许出现一个 .
    • 统计数字个数 digitCount,避免只解析出一个孤立的小数点 "."
  4. 避免重复解析

    • 一旦识别出 [start, end) 这一段是数字串,就把 i 设置为 end - 1,主循环 i++ 后从数字后面继续扫描,避免重复。
  5. 健壮性

    • 通过 digitCount 保证至少有一个数字
    • ParseFloat 出错做防御性处理(理论上不会错)