1. 程式人生 > >正則表達式之基本原理

正則表達式之基本原理

好的 無法 初始 狀態 特性 www 提高 影響 圖片

正則文法介紹

https://www.cnblogs.com/longhuihu/p/4128203.html

要了解正則表達式的原理,需要先了解一些計算機語言文法的基礎知識。

一個文法可以用一個四元來定義,G = {Vt,Vn,S,P}

其中Vt是一個非空有限的符號集合,它的每個元素成為終結符號。Vn也是一個非空有限的符號集合,它的每個元素稱為非終結符號,並且Vt∩Vn=Φ。S∈Vn,稱為文法G的開始符號。P是一個非空有限集合,它的元素稱為產生式。所謂產生式,其形式為α→β,α稱為產生式的左部,β稱為產生式的右部,符號“→”表示“定義為”,並且α、β∈(Vt∪Vn)*,α≠ε,即α、β是由終結符和非終結符組成的符號串。開始符S必須至少在某一產生式的左部出現一次。

文法可推導的語言標記為L(G)。

著名語言學家Chomsky(喬姆斯基)根據對產生式所施加的限制的不同,把文法分成四種類型,即0型、1型、2型和3型。

  1. 0型文法要求至少含有一個非終結符,基本沒有什麽限制,一個非常重要的理論結果是:0型文法的能力相當於圖靈機
  2. 1型文法也叫上下文有關文法,對應於線性有界自動機,要求每個產生式α→β,都有|β|>=|α|,|β|指長度;
  3. 2型文法也叫上下文無關文法,對應於下推自動機,要求在1型文法的基礎上,再滿足:每一個α→β都有α是非終結符;
  4. 3型文法也叫正則文法,它對應於有限狀態自動機。它是在2型文法的基礎上滿足:A→α|αB(右線性)或A→α|Bα(左線性)。

正則表達式就是最後一種,正則文法,的一種表達形式,以整個字母表作為終結符集合Vt。

假設有一個文法的產生式是{S->Sa; S->b;},那麽對應的正則表達式為ba*

因此正則表達式,正則文法,有限狀態自動機這個三個概念雖然指不同的東西,但是具備內在的等價性。

正則表達式是正則文法,限制多於上下文無關文法,而我們使用的編程語言語法都是上下文無關文法,因此試圖通過正則表達式去處理代碼(比如語言翻譯、代碼生成)的努力極可能歸於徒勞。不過,把代碼當做純文本,然後在處理過程中使用正則表達式,仍然能大大提高效率。

正則表達式的基礎運算符

正則表達式包含很多的元字符來表達規則,不過本文不是要介紹如何使用正則表達式,關於正則表達式規則最好的參考書是《精通正則表達式》。

實際上,正則表達式核心的運算符只有以下幾種:

名稱示例備註
或運算 r|s 匹配的語言是L(r)和L(s)的並集
連接運算 rs 匹配的語言是L(r)和L(s)連接
Kleene運算 r* 匹配的語言是L(r)和L(s)連接
括號 (r) 匹配的語言與L(r)一致

kleene運算符優先級最高,且是左結合的,連接第二,或運算優先級最低。

運算定律:

示例備註
r|s = s|r | 運算滿足交換律
r|s|t = r|(s|t) | 滿足結合律
r(st) 連接可以結合
r(s|t) = rs|rt 連接對|可以分配
?r = r? = r ?是連接的單位元
r* = (r|?)\* 閉包中一定包含?
r** = r* *具有冪等性

擴展運算符使得正則表達式更具表達力,下面僅舉幾個例子:

擴展運算符等價形式
+ r+ = rr* = r*r
r? = r | ?
字符類 [a1a2…an] = a1|a2|…|an;如果是連續的字符類,可以寫成[a1-an]

高級特性

正則表達式具備很多高級特性,比如捕獲、環視、固化分組等等,這些特性是為了提高正則表達式的實用價值被設計出來的,不屬於正則文法的範疇。

NFA和DFA

前面說過,正則文法對應於有限狀態自動機,又分確定型有限狀態自動機(DFA)和非確定型有限狀態自動機(NFA),這兩種狀態機的能力是一樣的,都能識別正則語言。正則表達式的識別引擎,都是基於DFA或NFA構造的。關於狀態機的基礎理論,這裏就不描述了,只要稍微有點印象,就不妨礙繼續閱讀。

  • NFA

    一個字母可以標記離開狀態的多條邊,並且? 也可以標記一條邊;這說明NFA的匹配過程面臨很多的岔路,需要做出選擇,一旦某條岔路失敗,就需要回朔。

    下圖是正則表達式(a|b)*abb對應的NFA,它相當直觀,基本可以從正則表達式直接轉換而來。

技術分享圖片

  • DFA

    對於每個狀態以及字母表中的每個字母,只能有一條以該字母為標記的,離開該狀態的邊;這說明DFA的匹配過程是確定的,每個字母是需要匹配一次。

    與上面NFA等價的DFA如下圖,相當地不直觀:

技術分享圖片

  • 將NFA轉化成DFA

由於NFA和DFA的能力是一樣的,每個NFA必然可以轉化成一個等價的DFA。既然DFA對每個輸入可以到達的狀態時是確定的,那麽輸入串s在NFA中可能達到的狀態集合對應為等價DFA中某個狀態。從這個思路出發,可以構造出DFA。

  1. 首先NFA的初始狀態0不接受? ,因此可以構造出DFA的初始狀態(0);
  2. 集合(0)輸入a,在NFA中能夠到達(0,1),於是構造出此狀態,以及從(0)到(0,1)的邊,標記為a
  3. 集合(0)輸入b,能到到達的還是(0),因此構造出從(0)到自身的一條標記為b的邊
  4. 集合(0,1)輸入a,能能夠到達的還是(0,1),與上一步類似
  5. 集合(0,1)輸入b,能夠給到達的是(0,2),構造狀態(0,2)及相應的邊
  6. 集合(0,2)輸入a, 能夠到達(0,1),沒有新狀態,添加一條邊
  7. 集合(0,2)輸入b,能夠給達到(0,3),構造新狀態(0,3)
  8. 集合(0,3)輸入a,能夠到達(0,1),添加一條邊即可
  9. 集合(0,3)輸入b,能夠給達到(0),添加一條邊即可
  10. 沒有新狀態,結束

最終得到的DFA如下,(0,3)包含了NFA的終結狀態3,因此也是DFA的中介狀態,對狀態重新命名可以得到上面同樣的DFA。

技術分享圖片

  • DFA和NFA的效率差異

    很容易理解,構造DFA的代價遠大於NFA,假設NFA的狀態數為K,那麽等價DFA的狀態數目理論上可達2的k次方,不過實際上幾乎不會出現這麽極端的情況,可以肯定的是構造DFA會消耗更多的時間和內存。

    但是DFA一旦構造好了之後,執行效率就非常理想了,如果一個串的長度是n,那麽匹配算法的執行復雜度是O(n);而NFA在匹配過程中,存在大量的分支和回朔,假設NFA的狀態數為s,因為每輸入一個字符可能達到的狀態數做多為s,那麽匹配算法的復雜度及時輸入串的長度乘以狀態數O(ns)。

正則表達式的NFA&DFA構造、轉化、簡化有一整套理論及方法,遠比上面的例子復雜,本文僅通過一個簡單的例子來說明原理。

NFA與DFA的能力差異

NFA和DFA這兩種匹配算法,除了效率上的差別外,從更高的視點看,形成了兩種風格的引擎,進而對正則表達式的匹配的其他方面能力造成差異。NFA被稱之為"表達式主導"引擎,而DFA被稱之為“文本主導”引擎。

NFA:表達式主導

從表達式的第一個部分開始,每次檢查一部分,同時檢查當前文本是否匹配表達式的當前部分,如果是,則繼續表達式的下一部分,如此繼續,直到表達式的所有部分都能匹配,即整個表達式匹配成功。

我們來看表達式to(nite|knight|night)匹配文本...tonight...的過程: 表達式的第一個部分是t,它會不斷重復掃描,直到在字符串中找到t,之後就檢查隨後的o,如果能匹配就繼續檢查下面的元素。這個例子中,下面的元素是(nite|knight|night),意思是nite或者knight或者night,引擎會依次嘗試這三種可能。

整個過程,控制權在表達式的元素之間轉換,因此被稱之為“表達式主導”。“表達式主導”的特點是每個子表達式都是獨立的,不存在內在聯系。 子表達式與整個正則表達式的控制結構(多選、量詞)的層級關系控制了整個匹配過程。

DFA:文本主導

DFA在讀入一個文本的時候,會記錄當前有效的所有匹配的表達式位置(這些位置集合對應於DFA的一個狀態)。
以上面的匹配過程為例:

  1. 當引擎讀入文本t時,記錄匹配的位置是to(nite|knight|night);
  2. 接著讀入o,匹配位置to(nite|knight|night);
  3. 讀入n,匹配位置to(nite|knight|night),兩個位置,knight被淘汰出局;
  4. ...

這種方式被稱之“文本主導”是因為被掃描的字符串,控制了引擎的執行過程。

差異之一:NFA表達式影響引擎

NFA表達式主導的特性,使得通過修改正則表達式來影響引擎,因此下面三個表達式盡管能夠匹配同樣的文本,但是引擎的執行過程各不相同:

  1. to(nite|knight|night)
  2. tonite|toknight|tonight
  3. to(k?night|nite)

但是對於DFA來說,沒有任何區別。

差異之二:DFA能保證最長匹配

對於包含或選項的表達式,NFA在成功匹配一個選項之後可能報告匹配成功,此時並不知道後面的選項是否也會成功,是否包含一個更長的匹配。

假設使用one(self)?(selfsufficient)?來匹配oneselfsufficient,NFA首先匹配one,然後匹配self,此時發現selfsufficient無法匹配剩余子串,但是這個子表達式不是必須的,因此可以立即返回成功,此時匹配的串為oneself

實際上NFA引擎的匹配結果與具體實現有關,而DFA必然會成功匹配oneselfsufficient

差異之三:NFA支持更多功能

NFA能夠支持“捕獲group”,“環視”,“占有優先量詞”,“固話分組”等高級功能,這些功能都基於“子表達式獨立進行匹配”這一特點。 而DFA無法記錄匹配歷史與子表達式之間的關系,因而也無法實現這些功能。

可見NFA引擎具備更大的實用價值,因而,我們在編程語言裏面使用的正則表達式庫都是基於NFA的。java的Pattern就是基於NFA的,Pattern.compile()方法顯然就是在構造NFA狀態圖。

參考資料

    1. 《精通正則表達式》
    2. 《編譯原理龍書第二版》

正則表達式之基本原理