leetcode 10. 正则表达式匹配 Regular Expression Matching

https://leetcode-cn.com/problems/regular-expression-matching/

题目描述

给定一个字符串 (s) 和一个字符模式 §。实现支持 ‘.’ 和 ‘*’ 的正则表达式匹配。

‘.’ 匹配任意单个字符。
‘*’ 匹配零个或多个前面的元素。
匹配应该覆盖整个字符串 (s) ,而不是部分字符串。

说明:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
示例 1:

输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。
示例 2:

输入:
s = “aa”
p = “a*”
输出: true
解释: ‘*’ 代表可匹配零个或多个前面的元素, 即可以匹配 ‘a’ 。因此, 重复 ‘a’ 一次, 字符串可变为 “aa”。
示例 3:

输入:
s = “ab”
p = “."
输出: true
解释: ".
” 表示可匹配零个或多个(’*’)任意字符(’.’)。
示例 4:

输入:
s = “aab”
p = “cab”
输出: true
解释: ‘c’ 可以不被重复, ‘a’ 可以被重复一次。因此可以匹配字符串 “aab”。
示例 5:

输入:
s = “mississippi”
p = “misisp*.”
输出: false

解题思路

原始思路及遇到的问题

我第一个想到的是有限自动状态机和编译原理。
比如当前状态为1,输入字符a进入下一个状态2,如下图左;如果检测到是a*就可以表示为下图右。(ϵ\epsilon表示空输入)
leetcode 10. 正则表达式匹配 Regular Expression Matching
比如模式为a*b*c*a,得到的状态机如下图:
leetcode 10. 正则表达式匹配 Regular Expression Matching
检测aaa是否符合这个正则,以ϵaaϵϵa\epsilon a a \epsilon \epsilon a这样的路径是可以走到终点的。

但是在检验过程中存在一个问题,对于一个状态,输入同一个字符,可能有多条可选的下一条路径。
比如在状态1,输入a,可能到达2,也可能到达3,4,5.
如果检测每一条路径,那就太麻烦了。

解决办法

后来我了解到上面的自动机叫做不确定有限自动状态机(NFA,Nondeterminate Finite Automation),还有一种是确定有限自动状态机(DFA,Determinate Finite Automation)。DFA的状态对于一个输入,是只有一个输出的。而且NFA是可以转化为DFA的。

涉及到的知识

有限自动状态机可以用五元组表示 {状态集合,输入集合,转移矩阵,开始状态集,结束状态集},用数学表示就是:NFA M=(K,Σ,f,S,F)M=(K,\Sigma,f,S,F)

NFA的构造

构造五元组,输入集合就是{a-z},只有一个开始状态。
状态用一个类表示:

//NFA不确定有限自动状态机的状态
class NFAState {
    boolean end; // 是否是结束状态
    Map<Character, Set<NFAState>> map; // 输入char,进入下一个状态
}

依次检查正则表达式中的字符,递归构造出NFA。

/**
   * 构造非确定有限自动状态机
   * @param pattern
   * @param p 指示pattern中的位置,即pattern[p]是下一个输入
   * @param state 当前节点,根据输入,构造下一个节点,递归调用
*/
private void buildNFA(String pattern, int p, NFAState state) 

NFA转化为DFA

我找到的方法是子集构造法。(并且有堆DFA进行化简的方法,我没有学,自然也没有实现)
参考:
http://www.pchou.info/resource/2013/12/31/compiler.html
https://www.bilibili.com/video/av31939713
基本思想就是合并几个状态为一个新的状态,比如当前状态为{1},输入a,可以到达的状态为2,3,4,用集合表示{2,3,4},新状态中还要包含空输入能到达的状态,如4输入空可以到达5,那新的状态就是{2,3,4,5};然后再看新的状态的转移函数,直到没有新的状态产生为止。

下面有几个比较拗口但其实意思很简单的定义:

  1. 状态集的ϵ\epsilon闭包:状态集II中的任何状态ss经任意条ϵ\epsilon弧而能达到的所有状态的集合。表示为:ϵclosure(I)\epsilon-closure(I)
  2. 状态集的aa弧转换:状态集II中的任何状态ss经过一条aa弧而能到达的所有状态的集合。表示为:move(I,a)move(I,a)
  3. 状态集的aa弧转换的闭包IaI_aIa=ϵclosure(move(I,a))I_a=\epsilon-closure(move(I,a))

用数学语言描述NFA转换为DFA的过程是:

NFA=(K,Σ,f,S,F)NFA=(K,\Sigma,f,S,F)
DFA=(K,Σ,f,S,F)DFA=(K&#x27;,\Sigma,f&#x27;,S&#x27;,F&#x27;)
输入集合是相同的,其余都不相同。
开始状态:K=ϵclosure(K)K&#x27;=\epsilon-closure(K)
假如当前状态为II,输入为aa,那下一个状态为ϵclosure(move(I,a))\epsilon-closure(move(I,a))
递归找出所有的状态和状态转换表。
如果II中包含FF中的值,那II就是DFA的结束状态。

下面是一个例子:
leetcode 10. 正则表达式匹配 Regular Expression Matching

java源码

package algorithm;

import java.util.*;

public class FSM {
    //NFA不确定有限自动状态机的状态
    class NFAState {
        boolean end;
        Map<Character, Set<NFAState>> map; // 输入char,进入下一个状态
    }

    // 确定有限自动状态机的状态
    class DFAState {
        boolean end;
        Map<Character, DFAState> map;
        Set<NFAState> set; // 由NFA--》DFA的过程中,一个DFA的节点是哪些NFA的节点的集合

        @Override
        public boolean equals(Object object) {
            DFAState dfaState = (DFAState) object;
            return dfaState.set.equals(set);
        }

        @Override
        public int hashCode() {
            return set.hashCode();
        }
    }

    /**
     * 构造非确定有限自动状态机
     * @param pattern
     * @param p
     * @param state 当前节点,根据输入,构造下一个节点,递归调用
     */
    private void buildNFA(String pattern, int p, NFAState state) {
        int n = pattern.length();
        if (p == n) {
            state.end = true;
            return;
        }

        Character c = pattern.charAt(p);

        if ((p+1) < n && pattern.charAt(p+1) == '*') {
            NFAState next = new NFAState();
            Map<Character, Set<NFAState>> nextMap = new HashMap<Character, Set<NFAState>>();
            Set<NFAState> nextSet = new HashSet<NFAState>();
            nextSet.add(next);
            nextMap.put(c, nextSet);
            next.map = nextMap;

            f(state, c, next);
            f(state, ' ', next);

            buildNFA(pattern, p+2, next);
        } else {
            NFAState next = new NFAState();
            f(state, c, next);
            buildNFA(pattern, p+1, next);
        }
    }

    /**
     * 状态转移函数,状态state,输入c,转移到下一个状态next
     * @param state
     * @param c
     * @param next
     */
    private void f(NFAState state, Character c, NFAState next) {
        Map<Character, Set<NFAState>> map = state.map;
        if (map == null) {
            map = new HashMap<Character, Set<NFAState>>();
            state.map = map;
        }

        if (map.containsKey(c)) {
            map.get(c).add(next);
        } else {
            Set<NFAState> set = new HashSet<NFAState>();
            set.add(next);
            map.put(c, set);
        }
    }

    private DFAState buildDFA(String pattern) {
        NFAState nfa = new NFAState();
        buildNFA(pattern, 0, nfa); // 不确定的有限自动状态机NFA

        Set<NFAState> nfaStartSet = new HashSet<NFAState>(); // NFA的开始节点集合
        nfaStartSet.add(nfa);

        Set<NFAState> startClosureSet = new HashSet<NFAState>(); // NFA开始节点的epsilon闭包
        epsilonClosure(nfaStartSet, startClosureSet);

        startClosureSet.add(nfa);

        DFAState dfaState = new DFAState(); // 确定有限自动状态机DFA
        dfaState.set = startClosureSet;

        if (isEnd(startClosureSet)) {
            dfaState.end = true;
        }

        Set<DFAState> dfaStates = new HashSet<DFAState>(); // DFA所有的节点
        dfaStates.add(dfaState);

        allDFAs.put(dfaState.hashCode(), dfaState);

        nfa2dfa(dfaState);
        return dfaState;
    }

    /**
     * 递归构建DFA
     * @param state 当前节点
     */
    private void nfa2dfa(DFAState state) {

        Set<Character> characters = alphabet(state);
        Map<Character, DFAState> map = state.map;
        if (map == null) {
            map = new HashMap<Character, DFAState>();
            state.map = map;
        }
        for (Character c : characters) {
            if (c != ' ') {
                DFAState next = next(state, c);
                if (allDFAs.values().contains(next)) {
                    next = allDFAs.get(next.hashCode());
                }
                map.put(c, next);

                if (!allDFAs.values().contains(next)) {
                    allDFAs.put(next.hashCode(), next);
                    nfa2dfa(next);
                }
            }
        }
    }

    Set<Character> alphabets = new HashSet<Character>();
    Map<Integer, DFAState> allDFAs = new HashMap<Integer, DFAState>();

    /**
     * 这个状态下,经由哪些字母可以转移到下一个状态
     * @param state
     * @return
     */
    private Set<Character> alphabet(DFAState state) {
        Set<NFAState> states = state.set;
        Set<Character> characters = new HashSet<Character>();
        for (NFAState nfaState: states) {
            if (nfaState.map != null) {
                characters.addAll(nfaState.map.keySet());
            }
        }

        if (characters.contains('.')) {
            return alphabets;
        }

        return characters;
    }

    private DFAState next(DFAState state, Character c) {
        DFAState next = move(state.set, c);

        Set<NFAState> nextSet = new HashSet<NFAState>();
        nextSet.addAll(next.set);
        epsilonClosure(next.set, nextSet);

        next.set = nextSet;

        if (isEnd(nextSet)) {
            next.end = true;
        }

        return next;
    }

    /**
     * 状态集的c弧转换
     * @param set
     * @param c
     * @return
     */
    private DFAState move(Set<NFAState> set, Character c) {

        DFAState dfaState = new DFAState();
        Set<NFAState> nextSet = new HashSet<NFAState>();
        dfaState.set = nextSet;

        for (NFAState nfaState: set) {
            if (nfaState.map != null) {
                if (nfaState.map.get(c) != null) {
                    nextSet.addAll(nfaState.map.get(c));
                }
                if (nfaState.map.get('.') != null) {
                    nextSet.addAll(nfaState.map.get('.'));
                }
            }
        }

        return dfaState;
    }

    /**
     * 状态集list的epsilon-闭包
     * @param set
     * @param closureSet 代表闭包,返回值,应该传入初始化过的集合
     */
    private void epsilonClosure(Set<NFAState> set, Set<NFAState> closureSet) {
        for (NFAState nfaState: set) {
            if (nfaState.map != null) {
                Set<NFAState> nextSet = nfaState.map.get(' ');
                if (nextSet != null) {
                    closureSet.addAll(nextSet);
                    epsilonClosure(nextSet, closureSet);
                }
            }
        }
    }

    private boolean isEnd(Set<NFAState> set) {

        for(NFAState nfaState: set) {
            if (nfaState.end) return true;
        }
        return false;
    }

    public boolean isMatch(String s, String p) {

        for (char i = 'a'; i < 'z'; i++) {
            alphabets.add(i);
        }

        DFAState state = buildDFA(p);

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

            Map<Character, DFAState> map = state.map;

            if (map == null) break;

            if (map.containsKey(c)) {
                state = map.get(c);
            } else if (map.containsKey('.')) {
                state = map.get('.');
            } else {
                break;
            }
        }

        return state.end && (i==s.length());
    }

    public static void main(String[] args) {
        FSM fsm = new FSM();
        String s = "aab", p = "c*a*b";
        boolean b = fsm.isMatch(s, p);
        System.out.println(b);
    }
}