leetcode 第10题 正则表达式匹配

其实我并不太喜欢使用动态规划来实现正则,但我看解题大多人都在用动规,有一个大佬用了NFA但代码不是很适合新手看,所以写一个简单点的解题方法。

有穷自动机的核心就是状态转换图,以最简单的 abbc 匹配 ab*c 为例
image

上图虽然看着比较乱,但其实很好理解。图上的一个圈就是一个状态(State),一个箭头就是一条路径(Path),从起点开始, 把每个箭头当一条路线,字符串配置到哪个字符,就走哪条路径,一直走到最后,如果手里正好没有字符了,那就是配置成功了。反之,如果中途无路可走,或到终点手里还有字符,在或者走到一半手里字符没有了,就是配置失败。

当出现如 b* 这类状态时,那由于 b 可出现零次或多次,所以意味着,可以从 a 直接走到 c 即 b 出现零次,如果有 b 也可继续在 b 上绕圈,直到字符串中的 b 用完。

但有一种特殊情况,由于是NFA,即意味着不确定,这个不确定指的就是在从状态选择接下来的路径时,会出现无法确定走哪条,或是说有多条可以走。
举个例子: abcabc 匹配 .*abc 这个正则在配置第一个 a 字符时就会出现,是走 . 呢,还是走 a 呢。
image-1665585767906

这种情况就是不确定性。反之如果每个状态有且仅有一条确定的路径可走,则叫确定的有穷自动机(DFA)。

对于这种情况,通常就是都走一下试试了。。
实现一个可以击败 5% 的代码吧~

const EOF rune = 0
const ANY rune = '.'

type State struct {
	C rune
	Path map[rune][]*State
	IsAccept bool
}

func NewNFA(p string) *State {
	start := &State{}
	curStates := []*State{start}
	var preStates []*State
	var repeatState *State // 保存 * 前面的字符,也就是需要重复的字符
	for _, c := range p {
		switch c {
		case '*':
			repeatState.AddPath(repeatState)
			preStates, curStates = curStates, append(curStates, preStates...)
		default:
			newState := &State{C: c}
			repeatState = newState
			addPath(curStates, newState)
			preStates, curStates = curStates, []*State{newState}
		}
	}
	receiveState := &State{IsAccept: true}
	addPath(curStates, receiveState)
	return start
}

func addPath(states []*State, path *State) {
	for _, s := range states {
		s.AddPath(path)
	}
}

func getNextChar(s []rune, index int) rune {
	if index >= len(s) {
		return EOF
	}
	return s[index]
}

func (s *State) AddPath(path *State) {
	if s.Path == nil {
		s.Path = map[rune][]*State{}
	}
	s.Path[path.C] = append(s.Path[path.C], path)
}

// 获取可走的路径
func (s *State) GetMovablePath(c rune) []*State {
	movablePath := s.Path[c]
	if anyPath, ok := s.Path[ANY]; c == EOF || !ok {
		return movablePath
	} else {
		return append(movablePath, anyPath...)
	}
}

func (s *State) Match(str string) bool {
	return s.doMatch([]rune(str), 0)
}

func (s *State) doMatch(str []rune, index int) bool {
	if s.IsAccept {
		if index >= len(str) {
			return true
		} else {
			return false
		}
	}

	c := getNextChar(str, index)
	movablePath := s.GetMovablePath(c)
	for _, path := range movablePath {
		result := path.doMatch(str, index+1)
		if result {
			return true
		}
	}
	return false
}

func isMatch(s string, p string) bool {
    return NewNFA(p).Match(s)
}