第四步,消去ε邊 python
理論上,即便不消ε邊也是沒有問題的,頂天多轉移兩次狀態,多花點時間罷了,對匹配的影響,嗯,不大。 正則表達式
不過出於效率,咱們仍是要消一下的。 數組
只要在紙上寫寫畫畫幾回,就能看出來了,消ε邊的規則以下: 數據結構
1)遍歷狀態圖,找到一個ε邊,設其起點爲A,終點爲B 函數
2)若是A==B,也就是起點終點重合,這個ε邊是壞邊,刪掉它 spa
3)若是B只有ε邊一個輸入邊,那麼咱們只須要將B的全部輸出轉移到A上,取代ε邊的位置便可 指針
4)若是A只有ε邊一個輸出邊,那麼咱們只須要將全部指向A的邊的目標都改爲B並刪掉ε邊就好了 string
5)不然,咱們就只好複製B的全部輸出,插入A的ε邊的位置(刪掉ε邊) ast
遍歷狀態圖完成上述的操做後,ε邊是消去了,可是還有大量的無用節點(沒有輸入邊的節點)保留了下來,因此咱們還要刪掉這些節點。 模板
另外,咱們在出列"(: )"是,只是添加了報告位置的輸出邊,那麼如今,咱們須要對這些邊進行編號了,以方便講報告的數據寫入相應的分組裏。(分組進行編號要在消去ε邊以前!否則會由於邊的複製出現錯誤)
注意在消去ε邊以前,記得要在起點添加一個輸入邊(進入匹配),在終點添加一個輸出邊(匹配成功),以避免在消ε邊時把正常節點也給誤刪了。
我不是使用列表來表示狀態圖的,那樣實在是不夠直觀,很差理解,我使用的是梳狀鏈表,分別有節點和路徑兩種類型,而後節點造成一個鏈表,每一個節點下有一個路徑的鏈表(表示以該節點爲起點的路徑),每一個路徑都包含着目標節點的指針(和條件、操做)。因而,不考慮目標路徑的指針的話,整個結構就好像是良莠不齊的梳子同樣。
這樣的好處是比較直觀,利於插入刪除節點和邊,缺點是打字較多……,還有一個好處,就是保持了節點的相對位置——因而咱們能夠正確的對報告邊進行編號,至於這個編號就比較簡單了,堆棧也好,狀態數組也好,實現起來都簡單的很,很少說。
第五步,進行匹配
匹配麼,有了有限自動機,反而容易多了,vczh的有限自動機是沒有我另加的三個邊(進入重複、累計重複、退出重複——這三個邊是配合起來實現限定次數重複的)的,他只須要記錄當前節點、當前匹配位置、分組數據便可,而我還須要記錄重複堆棧和當前重複指示,效率無疑是低了些。
匹配的方法,至關簡單。
首先,咱們須要一個堆棧,堆棧的成員是上述的數據(當前節點、當前匹配位置、分組數據、重複堆棧和當前重複指示),我使用的是一個定長的struct,相對簡單,但缺點是對分組數目和重複的嵌套次數都有限制。
先往堆棧里加入最初狀態;隨後進入循環。每一次循環,都從堆棧裏取出一個成員,而後根據成員的當前節點下的路徑,凡是能走通的都生成新的狀態加入堆棧裏。這個循環不斷進行,直到堆棧清空(匹配失敗),或者到達最後結束節點爲止(我使用的就是NULL)。
如此,這個正則表達式引擎已經基本完成了。只要再作一下包裝(好比我用一個類進行了封裝,添加了一些基於基本匹配的進階方法),就能夠投入使用了。
總結
其實,整個表達式寫下來,包括數據結構和各類函數等等等等,也不過不到1000行,20kb左右大小。
可是,不是這麼計算的。
寫正則表達式,我有了很多之前的積累:
使用了List(相似python的列表)、Graph(梳狀鏈表)、BlockStack(塊狀數組堆棧)三個容器模板;
使用了Label(char*的封裝,差很少等於C++的string)這個類;
使用了Operator(可定製表達式解析器類模板)來第二次分析表達式構建自動機;
而Operator使用了Cast類來實現映射,Cast類是使用RBT(紅黑樹)和Item(映射)類實現的……
使用了Environment(利用類的構造函數和全局變量模擬預處理函數)來進行數據的預處理和註冊函數。
所有加起來,即便去掉無關部分,估計也要2000-3000行,50kb左右吧。
之前的積累真的救了我一命。