本文原文地址 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,歡迎查閱。