算法#20--正則表達式匹配原理

本文不會介紹正則表達式的語法,重點介紹正則表達式匹配原理,算法實現。相信大家也都知道正則表達式應用強大之處,這裏也不再介紹其應用範圍。

1. 正則引擎

我們可以將前面KMP算法,看作一臺由模式字符串構造的能夠掃描文本的有限狀態自動機。對於正則表達式,我們要將這個思想推而廣之。

KMP的有限狀態自動機會根據文本中的字符改變自身的狀態。當且僅當自動機達到停止狀態時它找到一個匹配。算法本身就是模擬這種自動機,這種自動機的運行很容易模擬的原因是因爲它是確定性的:每種狀態的轉換都完全由文本中的字符所確定。

而正則表達式需要一種更加抽象的自動機(引擎),非確定有限狀態自動機(NFA)。正則引擎大體上可分爲不同的兩類:DFA和NFA,而NFA又基本上可以分爲傳統型NFA和POSIX NFA。

DFA–Deterministic finite automaton 確定型有窮自動機

NFA–Non-deterministic finite automaton 非確定型有窮自動機

  • Traditional NFA
  • POSIX NFA

2. 引擎區別

  • DFA:

DFA 引擎在線性時狀態下執行,因爲它們不要求回溯(並因此它們永遠不測試相同的字符兩次)。DFA 引擎還可以確保匹配最長的可能的字符串。但是,因爲 DFA 引擎只包含有限的狀態,所以它不能匹配具有反向引用的模式;並且因爲它不構造顯示擴展,所以它不可以捕獲子表達式。

  • NFA:

傳統的 NFA 引擎運行所謂的“貪婪的”匹配回溯算法,以指定順序測試正則表達式的所有可能的擴展並接受第一個匹配項。因爲傳統的 NFA 構造正則表達式的特定擴展以獲得成功的匹配,所以它可以捕獲子表達式匹配和匹配的反向引用。但是,因爲傳統的 NFA 回溯,所以它可以訪問完全相同的狀態多次(如果通過不同的路徑到達該狀態)。

因此,在最壞情況下,它的執行速度可能非常慢。因爲傳統的 NFA 接受它找到的第一個匹配,所以它還可能會導致其他(可能更長)匹配未被發現。

NFA最重要的部分:回溯(backtracking)。回溯就像是在道路的每個分岔口留下一小堆麪包屑。如果走了死路,就可以照原路返回,直到遇見麪包屑標示的尚未嘗試過的道路。如果那條路也走不通,你可以繼續返回,找到下一堆麪包屑,如此重複,直到找到出路,或者走完所有沒有嘗試過的路。

  • POSIX NFA:

POSIX NFA 引擎與傳統的 NFA 引擎類似,不同的一點在於:在它們可以確保已找到了可能的最長的匹配之前,它們將繼續回溯。因此,POSIX NFA 引擎的速度慢於傳統的 NFA 引擎;並且在使用 POSIX NFA 時,您恐怕不會願意在更改回溯搜索的順序的情況下來支持較短的匹配搜索,而非較長的匹配搜索。

DFA與NFA對比:

  • DFA對於文本串裏的每一個字符只需掃描一次,比較快,但特性較少。

NFA要翻來覆去吃字符、吐字符,速度慢,但是特性豐富,所以反而應用廣泛。
當今主要的正則表達式引擎,如Perl、Ruby、Python的re模塊、Java和.NET的regex庫,都是NFA的。

  • 只有NFA支持lazy、backtracking、backreference,NFA缺省應用greedy模式,NFA可能會陷入遞歸險境導致性能極差。

DFA只包含有窮狀態,匹對相配過程中無法捕獲子表達式(分組)的匹對相配結果,因此也無法支持backreference。

DFA不能支持捕獲括號和反向引用。

POSIX NFA會繼續嘗試backtracking,以試圖像DFA相同找到最長左子正則式。因此POSIX NFA速度更慢。

  • NFA是最左子式匹配,而DFA是最長左子式匹配。

  • NFA的編譯過程通常要快一些,需要的內存也更少一些。

對於“正常”情況下的簡單文本匹配測試,兩種引擎的速度差不多。一般來說,DFA的速度與正則表達式無關,而NFA中兩者直接相關。

  • 對正則表達式依賴性較量強的操作系統(大量應用正則做搜索匹對相配),最好完全把握NFA->DFA算法,充分理解所應用的正則表達式引擎的思想和特性。

3. 匹配過程

首先構造NFA,如下圖:

它是一個有向圖,邊表示了引擎匹配時的運行軌跡。從起始狀態0開始,到達1的位置(也就是“((”後),它有兩種選擇,可以走2,也可以走6,…直到最後的接受狀態。

得到有向圖後,匹配實現就簡單多了。這裏用到了有向圖的多點可達性問題–DirectedDFS算法。

  1. 首先我們查找所有從狀態0通過ε-轉換可達的頂點(狀態)來初始化集合。對於集合的每個頂點,檢查它是否可能與第一個輸入字符相匹配。檢查之後,就得到了NFA在匹配第一個字符之後可能到達的其他頂點。這裏還需要向該集合中加入所有從該集合中的任意狀態通過ε-轉換可以到達的頂點。

  2. 有了這個匹配第一個字符之後可能到達的所有頂點的集合,ε-轉換有向圖中的多點可達性問題的答案就是可能匹配第二個輸入字符的頂點集合。

  3. 重複這個過程直到文本結束,得到兩種結果:最後的集合含有可接受的頂點;不含有。

註釋,什麼是ε-轉換。

4. NFA的構造

將正則表達式轉化爲NFA的過程在某種程度上類似於Dijkstra的雙棧算法對表達式求值的過程。

構造規則:

邏輯很容易理解,請參考如下代碼和軌跡圖:


5. 代碼實現

附上DirectedDFSDigraph類.


public class NFA 
{ 
    private Digraph graph;     // digraph of epsilon transitions
    private String regexp;     // regular expression
    private int m;             // number of characters in regular expression

    /**
     * Initializes the NFA from the specified regular expression.
     *
     * @param  regexp the regular expression
     */
    public NFA(String regexp) 
    {
        this.regexp = regexp;
        m = regexp.length();
        Stack<Integer> ops = new Stack<Integer>(); 
        graph = new Digraph(m+1); 
        for (int i = 0; i < m; i++) 
        { 
            int lp = i; 
            if (regexp.charAt(i) == '(' || regexp.charAt(i) == '|') 
            {
                ops.push(i); 
            }
            else if (regexp.charAt(i) == ')') 
            {
                int or = ops.pop(); 

                // 2-way or operator
                if (regexp.charAt(or) == '|') 
                { 
                    lp = ops.pop();
                    graph.addEdge(lp, or+1);
                    graph.addEdge(or, i);
                }
                else if (regexp.charAt(or) == '(')
                {
                    lp = or;
                }
                else assert false;
            } 

            // closure operator (uses 1-character lookahead)
            if (i < m-1 && regexp.charAt(i+1) == '*') 
            { 
                graph.addEdge(lp, i+1); 
                graph.addEdge(i+1, lp); 
            } 
            if (regexp.charAt(i) == '(' || regexp.charAt(i) == '*' || regexp.charAt(i) == ')') 
            {
                graph.addEdge(i, i+1);
            }
        }
        if (ops.size() != 0)
        {    
            throw new IllegalArgumentException("Invalid regular expression");        
        }
    } 

    /**
     * Returns true if the text is matched by the regular expression.
     * 
     * @param  txt the text
     * @return {@code true} if the text is matched by the regular expression,
     *         {@code false} otherwise
     */
    public boolean recognizes(String txt) 
    {
        DirectedDFS dfs = new DirectedDFS(graph, 0);
        Bag<Integer> pc = new Bag<Integer>();
        for (int v = 0; v < graph.V(); v++)
        {
            if (dfs.marked(v)) pc.add(v);
        }

        // Compute possible NFA states for txt[i+1]
        for (int i = 0; i < txt.length(); i++) 
        {
            if (txt.charAt(i) == '*' || txt.charAt(i) == '|' || txt.charAt(i) == '(' || txt.charAt(i) == ')')
            {
                throw new IllegalArgumentException("text contains the metacharacter '" + txt.charAt(i) + "'");
            }

            Bag<Integer> match = new Bag<Integer>();
            for (int v : pc) 
            {
                if (v == m) 
                {
                    continue;
                }
                if ((regexp.charAt(v) == txt.charAt(i)) || regexp.charAt(v) == '.')
                {
                    match.add(v+1); 
                }
            }
            dfs = new DirectedDFS(graph, match); 
            pc = new Bag<Integer>();
            for (int v = 0; v < graph.V(); v++)
            {
                if (dfs.marked(v)) pc.add(v);
            }

            // optimization if no states reachable
            if (pc.size() == 0) 
            {
                return false;
            }
        }

        // check for accept state
        for (int v : pc)
        {
            if (v == m) return true;
        }
        return false;
    }

    /**
     * Unit tests the {@code NFA} data type.
     *
     * @param args the command-line arguments
     */
    public static void main(String[] args) 
    {
        String regexp = "(" + "(A*B|AC)D" + ")";
        String txt = "AABD";
        NFA nfa = new NFA(regexp);
        System.out.println(nfa.recognizes(txt));
    }
} 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章