從0到1打造正則表達式執行引擎(二)

本文原文地址 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

DFA和NFA

咱們已經屢次提到了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

算法

NFA轉DFA的算法叫作子集構造法,其具體流程以下。性能

  • 步驟1: NFA的初始節點和初始節點全部ε可達的節點共同構成DFA的初始節點,而後對初始DFA節點執行步驟2。
  • 步驟2: 對當前DFA節點,找到其中全部NFA節點對輸入符號X全部可達的NFA節點,這些節點溝通構成的DFA節點做爲當前DFA節點對輸入X可達的DFA節點。
  • 步驟3: 若是步驟2中找到了新節點,就對新節點重複執行步驟2。
  • 步驟4: 重複步驟2和步驟3直到找不DFA新節點爲止。
  • 步驟5: 把全部包含NFA終止節點的DFA節點標記爲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/regexui

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引擎匹配過程

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();
    }

DFA和NFA引擎性能對比

我用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,歡迎查閱。

參考資料

相關文章
相關標籤/搜索