正則表達式之基本原理

正則文法介紹

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

一個文法能夠用一個四元來定義,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*翻譯

所以正則表達式,正則文法,有限狀態自動機這個三個概念雖然指不一樣的東西,可是具有內在的等價性。設計

正則表達式是正則文法,限制多於上下文無關文法,而咱們使用的編程語言語法都是上下文無關文法,所以試圖經過正則表達式去處理代碼(好比語言翻譯、代碼生成)的努力很可能歸於徒勞。不過,把代碼當作純文本,而後在處理過程當中使用正則表達式,仍然能大大提升效率。3d

正則表達式的基礎運算符

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

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

名稱 示例 備註
或運算 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. 《編譯原理龍書第二版》
相關文章
相關標籤/搜索