词法分析原理 Lexical Analysis
Regex, DFA, NFA 算法理論
算法1:根據Regex構建NFA - McNaughton-Yamada-Thompson Construction
輸入:字母表∑上的一個正則表達式r。
輸出:一個接受L(r)的NFA N。
方法:首先對r進行語法分析,分解出組成它的子表達式。構建NFA的規則分為基本規則和歸納規則。
基本規則:處理不包含運算符的子表達式。
對于表達式ε,構造如下NFA:[ start ]---->[ i ]--(ε)-->[[ f ]]。其中i和f都是新狀態,分別為起始狀態和接受狀態。
對于表達式a,構造如下NFA:[ start ]---->[ i ]--(a)-->[[ f ]]。其中i和f都是新狀態,分別為起始狀態和接受狀態。
對于每次ε或a作為r的子表達式出現,都會用新狀態構建一個獨立的NFA。
歸納規則:根據給定表達式r的直接子表達式的NFA構造總NFA。
假設正則表達式s和t的NFA分別為N(s)和N(t),可如圖根據三種情況構建r的DFA。
算法2:根據NFA構建等價DFA - 子集構造算法 - The Subset Construction
輸入:一個NFA N。
輸出:一個等價的DFA D。
方法:DFA的每個狀態是一個NFA的狀態集合,構造DFA轉換方程Dtran,使得DFA并行地模擬NFA遇到給定輸入串時可能執行的所有動作。首先定義如下操作,其中s表示NFA的單個狀態,T表示NFA的一個狀態集合。
ε-closure(s): 能夠從NFA狀態s開始,只通過ε轉換到達的NFA狀態集合。
ε-closure(T): 能夠從NFA的狀態集T中某個狀態s開始,只通過ε轉換到達的NFA狀態集合。
move(T, a):能夠從NFA的狀態集T中某個狀態s開始,通過標記為a的轉換到達的NFA狀態集合。
算法:初始狀態可能是ε-closure(s0)中的任意狀態,其中s0是NFA的起始狀態。讀入輸入符號a后,NFA可以立即移動到move(T, a)中任何狀態,同樣可以進一步移動到ε-closure(move(T, a))中的任何狀態。
子集構造法:
一開始,ε-closure(s0)是Dstates中的唯一狀態,且未標記; while (在Dstates中有一個為標記狀態T) { 標記T; for (每個字母表中符號a) { U = ε-closure(move(T, a)); if (U不在Dstates中) 將未標記的U加入到Dstates中; Dtran[T, a] = U; } }
計算ε-closure(T):
將T的所有狀態壓入棧stack中, 將ε-closure(T)初始化為T;
while (stack不為空) {
t = stack.pop;
for (每個滿足如下條件的u: 從t出發有一個標號為的轉換到達狀態u)
if (u不在ε-closure(T)中) {
將u加入到ε-closure(T)中;
將u壓入棧中;
}
}
算法3:模擬NFA執行過程
輸入:一個以eof結尾的輸入串x,一個NFA N,開始狀態為s0,接受狀態集為F,轉換函數為move。
輸出:接受串x則返回yes,否則返回no。
方法:保存一個當前狀態集合S,即可以從開始狀態沿著標號到當前已讀入的輸入部分的路徑到達狀態的集合。如果c是函數nextChar()讀到的下一個輸入字符,那么就首先計算move(S, c),然后通過ε-closure()求閉包。
模擬NFA執行過程:
S = ε-closure(s0);
c = nextChar();
while (c != eof) {
S = ε-closure(move(S, c));
c = nextChar();
}
if (S ∩ F != ?) return yes;
else return no;
算法4:直接通過Regex創建DFA
重要狀態:如果一個NFA狀態有離開轉換,且都不是基于ε的轉換,則該狀態為重要狀態。NFA重要狀態直接對應于regex中符號的位置。
過程函數:這些函數基于增廣正則表達式(r)#所構成的抽象語法樹。
1) nullable(n),該節點所代表的子表達式對應的語句包含ε語句,則為真。
2) firstpos(n),可以出現在該節點表達的語句的第一個位置的符號。
3) lastpos(n),可以出現在該節點表達的語句的最后一個位置的符號。
4) followpos(p),可以出現在p所代表的語句的后面的第一個字符。
a) 計算nullable,firstpos,lastpos
可以根據語法樹,自底向上遞歸獲得。
b) 計算followpos
只有兩種情況會使得一個regex的某個位置跟在另一個位置之后:
1) 如果n是一個cat結點,左右子節點為c1、c2,那么對于lastpos(c1)中的每個位置i,followpos(i) = firstpos(c2);
2) 如果n是star結點,對于lastpos(n)中的每個位置i,followpos(i) = firstpos(n)。
輸入:一個regex r。
輸出:一個識別L(r)的DFA D。
方法:
1) 根據擴展正則表達式(r)#構造語法樹T。
2) 計算函數nullable(), firstpos(), lastpos()和followpos()。
3) 根據如下算法構造:
從Regex構造DFA:
初始化Dstates, 使其只包含未標記狀態集firstpos(n0), 其中n0是T的根節點;
while (Dstates存在未標記狀態集S) {
標記S;
for (每個輸入符號a) {
令U為S中和a對應的所有位置p的followpos(p)的并集;
if (U不在Dstates中)
將未標記的U加入Dstates;
Dtran[S, a] = U;
}
}
算法5:最小化DFA - Hopcroft's Algorithm
DFA的等價狀態:對于任意輸入串產生同樣狀態。
方法:首先粗劃分為兩組p0 = Daccept,p1 = {D - Daccpet}。對于ps中的狀態di和dj,他們必須滿足?c∈Σ,δ(i, c) = x,δ(j, c) = y 且 dx, dy ∈ pt。為了劃分P,該算法檢查每一個p∈P以及每一個c∈Σ。如果c劃分p,該算法就將p劃分為兩個子集并添加至T中。
創建DFA:根據等價狀態分組,對于每一組狀態集p∈P,在DFA中對應建立一個狀態。對于dj∈pl,dk∈pm,且δ(dj, c) = dk,我們就創建兩個狀態分別對應pl和pm,且δ(pl, c) = pm。
DFA等價狀態劃分:
T = {Da, {D - Da}};
P = ?;
while (P != T) {
P = T;
T = ?;
for (each set p ∈ P)
T = T ∪ Split(p);
}
Split(S) {
for (each c ∈ Σ) {
if (c splits S into s1 and s2)
then return {s1, s2};
}
return S;
}
算法6:直接從NFA構建最小化DFA- Brzozowski's Algorithm
應當注意到在子集構建DFA時,不會有相同前綴出現。該算法運用了這一特性,直接從NFA構建最小化DFA。
對于給定NFA N:
reverse(N): 將NFA倒轉,將所有原接受狀態連接至新的起始狀態。
reachable(N): 所有從起始狀態可以到達的狀態。
subset(N): 子集構建法獲得的DFA。
于是可得NFA N對應的最小化DFA為:
reachable(subset(reverse( reachable(subset(reverse(n))) )))
其中,內部的subset(reverse(n))精簡了NFA的后綴,reachable()棄掉了所有無用的狀態。然后外部的三個函數再一次精簡了NFA的前綴,并棄掉其余無用狀態,最終構建最小化NFA。
將DFA轉化為代碼實現
實現1:表驅動分析器 - Table-Driben Scanner
方法:將轉換函數輸入到一個對應的二維數組中,scan過程中通過查表來模擬DFA行為。還可以提供另外一個表用來將輸入字符分類,實現壓縮轉換表。
Table-Driven Scanner:
Nextword() { /* initalization */ state = S0; lexeme = ""; stack.clear(); stack.push(bad); /* scanning loop to model the DFA's behavior */ while (state != Serror) { char = Nextchar(); lexeme += char; if (state ∈ Saccept) stack.clear(); stack.push(state); category = CharCat[char]; // 表1: 字符分類表, e.g. digit, letter, punc, space state = δ[state, cat]; // 表2: 轉換表, 包含每個狀態下對應不同輸入的轉換狀態 } /* roll back loop in case the DFA overshoots the end of the token */ while (state ? Saccept && state != bad) { stack.pop(); truncate lexeme; RollBack(); } /* interprets and reports the result */ if (state ∈ Saccept) return Type[state]; else return invalid; }
實現2:直接編碼分析器 - Direct-Coded Scanners
方法:直接將轉換表的行為編入代碼,節省查表時間。
Sinit: lexeme = "";
stack.clear();
stack.push(bad);
goto S0;
S0: Nextchar(char);
lexeme += char;
if (state ∈ Saccept)
stack.clear();
stack.push(state);
if (char == 'r')
goto S1;
else goto Sout;
S1: ...
S2: ...
Sout: while (state ? Saccept && state != bad) {
state = stack.pop();
truncate lexeme;
RollBack();
}
if (state ∈ Saccept)
then return Type[state];
return invalid;
實現3:手工構建分析器 - hand-Coded Scanner
方法:利用雙重緩沖技術,通過指針高效讀取輸入字符。
initialization:
Input = 0;
Fence = 0;
fill Buffer[0 : n];
implementing NextChar():
Char = Buffer[Input];
Input = (Input + 1) mod 2n;
if (Input mod n == 0) {
fill Buffer[Input : Input + n - 1];
Fence = (Input + n) mod 2n;
}
return Char;
implementing RollBack():
if (Input == Fence)
then signal roll back error;
Input = (Input - 1) mod 2n;
總結
以上是生活随笔為你收集整理的词法分析原理 Lexical Analysis的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用MetaMask实现转账交易时附带I
- 下一篇: STL源码剖析 内存基本处理工具 初始化