詞法分析器的任務是按照一定模式從源程序中識別出記號(token).
我們使用正規式描述這一模式,並通過有限自動機進行識別.
正規式與正規集
語言是在有限字母表上有限長字符串的集合.
正規式又稱正則表達式, 是一種特殊的字符串用來描述一類的字符串的集合.
我們把可用正規式描述(其結構)的語言稱為正規語言或正規集.
在介紹正規式之前, 我們先定義幾個字符串集合中的概念:
-
\(\epsilon\): 空串, 沒有字符的串不是空格由空格組成的串
-
並運算: $ L \cup M = {s| s \in L or s \in M }$
-
交運算: $ L \cap M = {s| s \in L and s \in M}$
-
連接運算: $ LM = {st | s \in L and t \in M } $ , 任意屬於L的字符串與任意屬於M的字符串按順序連接
-
閉包運算: L* $ = L^0 \cup L \cup L^2 $ ..., 其中\(L^0 = {\epsilon}, L^2 = LL\)
-
正閉包: $ L+ = L \cup L^2... $
正規式采用遞歸定義:
-
\(\epsilon\)是正規式, 表示集合\({\epsilon}\)
-
任意字符a是正規式, 表示集合{a}
-
若r和s是正規式, L(r)和L(s)是它們則:
-
r|s是正規式, 表示的集合為$ L(r) \cup L(s)$
-
rs是正規式, 表示的集合為$ L(r)L(s) $
-
r是正規式, 表示的集合為L(r)
-
r+是正規式, 表示的集合為$ L(r)+ $
-
上面同樣定義了正則表達式的基本運算, 運算均為左結合, 優先級從高到低為閉包, 連接, 交並運算,正規式運算可以使用括號改變順序.
兩個正規式描述的集合相同則稱它們是等價的, 也就是說正規式和集合之間是多對一的關系.
有限自動機
不確定的有限自動機(Nondeterministic Finite Automaton, NFA)是識別模式的方法, 我們用狀態圖來描述NFA.
下圖的NFA用於識別正規式= < | <= | <> | = | > | >=
NFA的初始狀態為0, 若其第一個字符為<
則轉移到狀態1, 其它類推.
若下一個字符為=
則轉移到狀態2並返回, 若下一個字符為>
則轉移到狀態3並返回, 否則直接返回狀態1.這里的返回是指以當前狀態作為終態, 終止匹配.
上面的策略體現了最長匹配原則, 即達到狀態1時不立即返回而是繼續嘗試狀態2或狀態3.
NFA識別字符串就是反復試探所有路徑,直到到達終態后返回,或者到達不了終態后放棄.一般使用回溯法試探所有路徑.
我們使用五個要素描述NFA:
-
狀態集S
-
字母表
-
move(i, j)狀態轉移函數
-
S0初態
-
F終態集
狀態轉移函數接受兩個參數, 當前狀態和轉移條件, 返回新的狀態.
轉移條件是指后續字符串滿足該條件才會發生狀態轉移, 比如要求下一個字符為特定字符.
NFA的問題在於:
-
只有嘗試了全部可能的路徑,才能確定一個輸入序列不被接受
-
識別過程中需要進行大量回溯,時間復雜度很高
NFA中對狀態轉移函數幾乎沒有限制, 允許其出現一對多的狀態轉移和\(\epsilon\)狀態轉移.
所謂\(\epsilon\)狀態轉移是指轉移條件為空, 狀態轉移可以隨意發生, 這種狀態轉移經常在識別閉包時出現.
確定的有限自動機(Deterministic Finite Automaton, DFA)是NFA的一個特例,其最大的特點是其狀態轉移函數都是一對一的且不允許\(\epsilon\)狀態轉移.
因為NFA對狀態轉移不加限制在實際應用中帶來很多問題, 通常我們將NFA轉換為等價的DFA. 這里所謂的自動機等價是指它們識別同樣的正規集.
以正規式(a|b)*abb
為例, 其NFA可以表示為:
可以看到狀態為0, 下一個字符為a時出現一對多的問題.
DFA的狀態轉移復雜一些:
詞法分析器
構建詞法分析器一般需要幾個步驟:
-
用正規式描述記號的模式
-
為正規式設計NFA
-
將NFA轉換為等價的DFA, 這一步稱為確定化
-
優化DFA使其狀態數最少, 這一步稱為最小化
從正規式到NFA
Thompson算法可以用來為一個正規式構建NFA.
-
對\(\epsilon\), 構造NFA:
-
對字符a構造NFA:
-
r和s是正規式, 它們的NFA為N(r)和N(s):
- r|s的NFA為:
- rs的NFA為:
- r*的NFA為:
使用Thompsonn算法構造正規式(a|b)*abb
的NFA, 自下而上構建:
作出狀態轉移圖:
從NFA到DFA
基於NFA構造DFA的核心在於將一對多的狀態轉移確定化.
使用回溯法在發現無法匹配的路徑后返回是一種自然的思路, 不過我們可以采用並行的方法.
當發現一對多的情況時我們可以同時試探所有路徑, 當發現某條路徑不通時直接放棄該路徑不必回溯.
\(\epsilon\)閉包
為了消除\(\epsilon\)狀態轉移, 我們引入\(\epsilon\)閉包的概念:
從狀態集T出發,不經任何字符可達到的狀態的集合稱為T的\(\epsilon\)閉包, 記作\(\epsilon(T)\)
建立一個集合V, 將T添加到V中, 遍歷V中的每一個狀態s, 將s可以通過\(\epsilon\)狀態轉移到達的狀態添加到V中, 最終得到的V即為T的\(\epsilon\)閉包.
{s2}的\(\epsilon\)閉包為{s2, s4, s5}
為了便於敘述, 我們將從狀態集S出發通過條件a可以到達的下一狀態全體記作smove(S, a).則$ smove(\epsilon(T), \epsilon) \subset \epsilon(T) $
我們可以把\(\epsilon\)閉包當做一個狀態來看待:
在(a|b)*abb
的NFA上識別輸入序列abb:
-
計算初態集: $ \epsilon({0}) = {0, 1, 2, 4, 7} = A $
-
由A出發經條件a到達: $ \epsilon(smove(A,a)) = {1, 2, 3, 4, 6, 7, 8} = B $
-
由B出發經條件b到達: $ \epsilon(smove(B, b)) = {1, 2, 4, 5, 6, 7, 9} = C $
-
由C出發經條件b到達: $ \epsilon(smove(C, b)) = {1, 2, 4, 5, 6, 7, 10} = D $
-
10為終態,接受
識別abab:
-
計算初態集: $ \epsilon({0}) = {0, 1, 2, 4, 7} = A $
-
由A出發經條件a到達: $ \epsilon(smove(A,a)) = {1, 2, 3, 4, 6, 7, 8} = B $
-
由B出發經條件b到達: $ \epsilon(smove(B, b)) = {1, 2, 4, 5, 6, 7, 9} = C $
-
由C出發經條件a到達: $ \epsilon(smove(C, a)) = {1, 2, 3, 4, 6, 7, 8} = B $
-
由B出發經條件b到達: $ \epsilon(smove(B, b)) = {1, 2, 4, 5, 6, 7, 9} = C $
未到達終態, 不接受
子集構造法
算法流程:
- 初始化數據結構: DFA自動機D, 狀態集的集合DS, 狀態轉義關系集DT
將epsilon({0})加入到DS中, 所有狀態置為未標記
當DS中仍有未標記的狀態集T時執行循環:
標記T
遍歷每一個字符a: // 只有可以從T中轉移出去的字符才有意義
令 S = epsilon(smove(T, a))
若S非空:
令DT(T, a) = S
若S不在DS中:
將S作為未標記的狀態集加入DS
示例, 由(a|b)*abb
的NFA構造DFA:
-
初始化:$ A = \epsilon({0}) = {0, 1, 2, 4, 7}; DS.append(A); $
-
$ B = \epsilon(smove(A, a)) = {1, 2, 3, 4, 6, 7, 8}; DS.append(B); DT(A, a) = B $
-
$ C = \epsilon(smove(A, b)) = {1, 2, 4, 5, 6, 7}; DS.append(C); DT(A, b) = C $
-
$ S = \epsilon(smove(B, a)) = {1, 2, 3, 4, 6, 7, 8}; S == B; DT(B, a) = B $
-
$ D = \epsilon(smove(B, b)) = {1, 2, 3, 4, 6, 7, 8}; DS.append(D); DT(B, b) = D $
-
$ S = \epsilon(smove(C, a)) = {1, 2, 3, 4, 6, 7, 8}; S == B; DT(C, a) = B $
-
$ S = \epsilon(smove(C, b)) = {1, 2, 4, 5, 6, 7}; S == C; DT(C, b) = C $
-
$ S = \epsilon(smove(D, a)) = {1, 2, 3, 4, 6, 7, 8}; S == B; DT(D, a) = B $
-
$ E = \epsilon(smove(D, b)) = {1, 2, 4, 5, 6, 7, 10}; DS.append(E); DT(D, b) = E $
-
$ S = \epsilon(smove(E, a)) = {1, 2, 3, 4, 6, 7, 8}; S == B; DT(E, a) = B $
-
$ S = \epsilon(smove(E, b)) = {1, 2, 4, 5, 6, 7}; S == C; DT(E, b) = C $\
根據DS和DT繪制狀態轉移圖:
最小化DFA
首先引入可區分的概念:對於DFA中任意兩個狀態s和t, 接受輸入字符串w, 若s和t轉移到不同狀態則稱w對於s和t是可區分的.
最小化DFA的目的是使DFA的狀態數最少, 定義一個DFA自動機的狀態集為S, 終態集為F, 算法流程:
初始化U = {S-F, F}
遍歷U中每一個狀態集T:
初始化N = U
遍歷T中任意狀態的組合(s,t, ..):
若對於任意字符a, move(s,a)與move(t,a)均屬於U中同一個狀態集G:
將s,t划分入同一組, 使用新划分的組代替N中的G
若N == U退出, 以N作為最終划分
令U=N
遍歷U中沒一個狀態集T:
從T中選擇一個狀態s, 令T中出發的狀態轉移改為從s出發, 到T的狀態轉移改為轉移到s
清除所有死狀態(只能轉移到自身且不是終態)和不可達狀態.
我們用該算法簡化上面的DFA:
-
U = {ABCD, E}
-
U = {ABC, D, E}
-
U = {AC, B, D, E}
AC可合並為一個狀態: