本文原文地址 https://blog.csdn.net/xindoo/article/details/106458165
在上篇博客从0到1打造正则表达式执行引擎(一)中咱们已经构建了一个可用的正则表达式引擎,相关源码见https://github.com/xindoo/regex,但上文中只是用到了NFA,NFA的引擎建图时间复杂度是O(n),但匹配一个长度为m的字符串时由于涉及到大量的递归和回溯,最坏时间复杂度是O(mn)。与之对比DFA引擎的建图时间复杂度O(n^2),但匹配时没有回溯,因此匹配复杂度只有O(m),性能差距仍是挺大的。java
咱们已经屡次提到了NFA和DFA,它俩到底是啥?有啥区别?
首先,NFA和DFA都是有限状态机,都是有向图,用来描述状态和状态之间的关系。其中NFA全称是非肯定性有限状态自动机(Nondeterministic finite automaton),DFA全称是肯定性有限状态自动机(Deterministic finite automaton)。 git
两者的差别主要在于肯定性和非肯定性,何为肯定性? 肯定性是指面对同一输入,不会出现有多条可行的路径执行下一个节点。有点绕,看完图你就理解了。
图示分别是一个NFA和DFA,上图之因此是NFA是由于它有节点具有不肯定性,好比0节点,在输入"a"以后它分别能够到0 1 2 节点。还有,上图有$\epsilon$边,它能够在没有输入的状况下跳到下一个节点,这也带来了不肯定性。相反,下图DFA中,每一个节点对某一特定的输入都只有最多一条边。 github
总结下NFA和DFA的区别就是,有ε边或者某个节点对同一输入对应多个状态的必定是NFA。 正则表达式
DFA和NFA存在等价性,也就是说任何NFA均可以转化为等价的DFA。因为NFA的非肯定性,在面对一个输入的时候可能有多条可选的路径,因此在一条路径走不通的状况下,须要回溯到选择点去走另一条路径。但DFA不一样,在每一个状态下,对每一个输入不会存在多条路径,就不须要递归和回溯了,能够一条路走到黑。DFA的匹复杂度只有O(n),但由于要递归和回溯NFA的匹配复杂度达到了O(n^2)。 这也是为何咱们要将引擎中的NFA转化为DFA的主要缘由。算法
NFA转DFA的算法叫作子集构造法,其具体流程以下。性能
语言描述比较难理解,咱们直接上例子。 咱们已经拿上一篇网站中的正则表达式 a(b|c) 为例,我在源码https://github.com/xindoo/regex中加入了NFA输出的代码, a(b|c) 的NFA输出以下。测试
from to input 0-> 1 a 1-> 8 Epsilon 8-> 9 Epsilon 8-> 6 Epsilon 6-> 2 Epsilon 6-> 4 Epsilon 2-> 3 b 4-> 5 c 3-> 7 Epsilon 5-> 7 Epsilon 7-> 9 Epsilon 7-> 6 Epsilon
绘图以下:
咱们在上图的基础上执行步骤1 获得了节点0做为DFA的开始节点。
而后对DFA的节点0执行步骤1,找到NFA中全部a可达的NFA节点(1#2#4#6#8#9)构成NFA中的节点1,以下图。
我以dfa1为出发点,发现了a可达的全部NFA节点(2#3#4#6#7#9)和b可达的全部节点(2#4#5#6#7#9),分别构成了DFA中的dfa2和dfa3,以下图。
而后咱们分别在dfa2 dfa3上执行步骤三,找不到新节点,但会找到几条新的边,补充以下,至此咱们就完成了对 a(b|c)* 对应NFA到DFA的转化。
能够看出DFA图节点明显少于NFA,但NFA更容易看出其对应的正则表达式。以前我还写过DFA生成正则表达式的代码,详见文章http://www.javashuo.com/article/p-esyswvyp-cr.html网站
代码其实就是对上文流程的表述,更多细节见https://github.com/xindoo/regex。ui
private static DFAGraph convertNfa2Dfa(NFAGraph nfaGraph) { DFAGraph dfaGraph = new DFAGraph(); Set<State> startStates = new HashSet<>(); // 用NFA图的起始节点构造DFA的起始节点 步骤1 startStates.addAll(getNextEStates(nfaGraph.start, new HashSet<>())); if (startStates.size() == 0) { startStates.add(nfaGraph.start); } dfaGraph.start = dfaGraph.getOrBuild(startStates); Queue<DFAState> queue = new LinkedList<>(); Set<State> finishedStates = new HashSet<>(); // 若是BFS的方式从已找到的起始节点遍历并构建DFA queue.add(dfaGraph.start); while (!queue.isEmpty()) { // 步骤2 DFAState curState = queue.poll(); for (State nfaState : curState.nfaStates) { Set<State> nextStates = new HashSet<>(); Set<String> finishedEdges = new HashSet<>(); finishedEdges.add(Constant.EPSILON); for (String edge : nfaState.next.keySet()) { if (finishedEdges.contains(edge)) { continue; } finishedEdges.add(edge); Set<State> efinishedState = new HashSet<>(); for (State state : curState.nfaStates) { Set<State> edgeStates = state.next.getOrDefault(edge, Collections.emptySet()); nextStates.addAll(edgeStates); for (State eState : edgeStates) { // 添加E可达节点 if (efinishedState.contains(eState)) { continue; } nextStates.addAll(getNextEStates(eState, efinishedState)); efinishedState.add(eState); } } // 将NFA节点列表转化为DFA节点,若是已经有对应的DFA节点就返回,不然建立一个新的DFA节点 DFAState nextDFAstate = dfaGraph.getOrBuild(nextStates); if (!finishedStates.contains(nextDFAstate)) { queue.add(nextDFAstate); } curState.addNext(edge, nextDFAstate); } } finishedStates.add(curState); } return dfaGraph; }
public class DFAState extends State { public Set<State> nfaStates = new HashSet<>(); // 保存对应NFAState的id,一个DFAState多是多个NFAState的集合,因此拼接成String private String allStateIds; public DFAState() { this.stateType = 2; } public DFAState(String allStateIds, Set<State> states) { this.allStateIds = allStateIds; this.nfaStates.addAll(states); //这里我将步骤五直接集成在建立DFA节点的过程当中了 for (State state : states) { if (state.isEndState()) { this.stateType = 1; } } } public String getAllStateIds() { return allStateIds; } }
另外我在DFAGraph中封装了有些NFA节点列表到DFA节点的转化和查找,具体以下。this
public class DFAGraph { private Map<String, DFAState> nfaStates2dfaState = new HashMap<>(); public DFAState start = new DFAState(); // 这里用map保存NFAState结合是已有对应的DFAState, 有就直接拿出来用 public DFAState getOrBuild(Set<State> states) { String allStateIds = ""; int[] ids = states.stream() .mapToInt(state -> state.getId()) .sorted() .toArray(); for (int id : ids) { allStateIds += "#"; allStateIds += id; } if (!nfaStates2dfaState.containsKey(allStateIds)) { DFAState dfaState = new DFAState(allStateIds, states); nfaStates2dfaState.put(allStateIds, dfaState); } return nfaStates2dfaState.get(allStateIds); } }
dfa引擎的匹配也能够彻底复用NFA的匹配过程,因此对以前NFA的匹配代码,能够针对DFA模式取消回溯便可(不取消也没问题,但会有性能影响)。
private boolean isMatch(String text, int pos, State curState) { if (pos == text.length()) { if (curState.isEndState()) { return true; } for (State nextState : curState.next.getOrDefault(Constant.EPSILON, Collections.emptySet())) { if (isMatch(text, pos, nextState)) { return true; } } return false; } for (Map.Entry<String, Set<State>> entry : curState.next.entrySet()) { String edge = entry.getKey(); // 若是是DFA模式,不会有EPSILON边 if (Constant.EPSILON.equals(edge)) { for (State nextState : entry.getValue()) { if (isMatch(text, pos, nextState)) { return true; } } } else { MatchStrategy matchStrategy = MatchStrategyManager.getStrategy(edge); if (!matchStrategy.isMatch(text.charAt(pos), edge)) { continue; } // 遍历匹配策略 for (State nextState : entry.getValue()) { // 若是是DFA匹配模式,entry.getValue()虽然是set,但里面只会有一个元素,因此不须要回溯 if (nextState instanceof DFAState) { return isMatch(text, pos + 1, nextState); } if (isMatch(text, pos + 1, nextState)) { return true; } } } } return false; }
由于DFA的匹配不须要回溯,因此能够彻底改为非递归代码。
private boolean isDfaMatch(String text, int pos, State startState) { State curState = startState; while (pos < text.length()) { boolean canContinue = false; for (Map.Entry<String, Set<State>> entry : curState.next.entrySet()) { String edge = entry.getKey(); MatchStrategy matchStrategy = MatchStrategyManager.getStrategy(edge); if (matchStrategy.isMatch(text.charAt(pos), edge)) { curState = entry.getValue().stream().findFirst().orElse(null); pos++; canContinue = true; break; } } if (!canContinue) { return false; } } return curState.isEndState(); }
我用jmh简单作了一个非严格的性能测试,随手作的 看看就好,结果以下:
Benchmark Mode Cnt Score Error Units RegexTest.dfaNonRecursion thrpt 2 144462.917 ops/s RegexTest.dfaRecursion thrpt 2 169022.239 ops/s RegexTest.nfa thrpt 2 55320.181 ops/s
DFA的匹配性能远高于NFA,不过这里竟然递归版还比非递归版快,有点出乎意料, 详细测试代码已传至Github https://github.com/xindoo/regex,欢迎查阅。