1. 程式人生 > >編譯原理 詞法分析三

編譯原理 詞法分析三

本文是詞法分析的第三篇文章。之前的第一篇文章介紹了詞法單元、模式和詞素的三者間的關係,以及正則表示式;第二篇文章介紹了有窮自動機,以及如何把NFA轉換成等價的DFA。本文首先將介紹如何把一個正則表示式轉換成一個有窮自動機,接著會給出一個最小化DFA狀態數的演算法,最後會回顧整個詞法分析過程。

從正則表示式到有窮自動機
對我們來說,用正則表示式描述一種語言是十分方便的。比如說[a-zA-Z]可以表示所有大小寫字母,[abc]owo[xyz]可以表示所有長度為5,以a、b、c中的一個字母開頭,接著owo三個字母,並以x、y、z中的一個字母結尾的字串。但是,機器沒辦法直接識別一個抽象的正則表示式。為此,我們需要把正則表示式轉換成一個有窮自動機。

如何把一個正則表示式轉換成有窮自動機,其實有三種選擇:

直接轉換成一個NFA;
直接轉換成一個DFA;
先轉換成一個NFA,再把這個NFA轉換成DFA。
由於把NFA轉換成DFA已經介紹過了,因此我們把重點放在前兩種方式上。

把正則表示式轉換成NFA
我們先來回顧一下正則表示式的遞迴構建規則:

為了把正則表示式轉換成NFA,我們需要把正則表示式的遞迴構建規則用相應的流圖表示:

注意到,在每個NFA中,都會引入兩個新狀態——初始狀態和終止狀態。下面給出將正則表示式轉換成一個NFA的McMaughton-Yamada-Thompson演算法:

輸入:符號集合∑上的一個正則表示式r;
輸出:一個接受L(r)的NFA N;
方法:首先分解出r的每個子表示式,接著按照正則表示式-NFA轉換表對每個子表示式得到對應的子NFA,最後合併所有的子NFA得到N。
現在我們可以利用McMaughton-Yamada-Thompson演算法將正則表示式r=(a|b)*a轉換成一個NFA:

把正則表示式轉換成DFA
現在我們已經知道了如何把一個正則表示式轉換成一個NFA,但是得到的NFA中可能有多餘的轉換,一個典型的例子就是ε轉換,換言之,NFA中可能有多餘的狀態,因此我們需要判斷NFA的某個狀態是否是必須的。

NFA的重要狀態是有一個標號不為ε的轉換(出邊)的狀態。因此,以輸入符號作為轉換標記的狀態都是重要狀態。另外,NFA的終止狀態也應該是一個重要狀態,但是它並沒有一個離開轉換,因此,可以擴充套件正則表示式r,在其後加上一個特殊的終止符號#,形成一個擴充套件的正則表示式(r)#,這樣NFA的終止狀態也成為了一個重要狀態。

為了說明重要狀態對構建DFA的作用,先把正則表示式轉換成抽象語法樹。一棵抽象語法樹包括:

葉子結點:表示字元集合中的符號,包括擴充套件的#結尾符號;
內部結點:表示正則表示式中的運算子,並、連線、閉包運算分別稱為cat、or、star結點,並分別用○、|、*表示;
結點位置:對每個標號不為ε的葉子結點,自底向上、從左到右的自1開始順序編號。被編號的結點對應於NFA中的重要狀態,編號的號碼代表重要狀態在NFA中的位置。
對於正則表示式(a|b)*abb,它的抽象語法樹和其對應的NFA如下,其中的重要狀態用結點位置編號,非重要狀態用大寫字母編號:

有了抽象語法樹以後,需要對每個結點計算四個函式,定義Rn是以結點n為根的子樹,L(Rn)是Rn對應的表示式能夠描述的語言:

nullable(n):如果L(Rn)中包括空串ε,返回true,否則返回false;
firstpos(n):返回一個位置集合,這些位置是L(Rn)中的某個串的第一個符號的位置;
lastpos(n):返回一個位置集合,這些位置是L(Rn)中的某個串的最後一個符號的位置;
followpos(n):返回一個位置集合,這些位置是跟在L(Rn)中的某個串之後的符號的位置。
上述四個函式的計算規則可以總結為下表:

仍以正則表示式(a|b)*abb為例,計算這四個函式,為了方便指明是哪個結點,我們在其抽象語法樹中用n1-n6標註內部結點:

以自底向上、從左到右的順序計算其四個函式,結果如下:

注意到,只有n2的nullable函式返回true,因為n2是一個star結點;另外,followpos函式只會在cat結點和star結點處計算,並把結果賦給葉子結點,表項為calculate表示計算followpos函式。

現在可以根據上表構建出一個DFA了。對於這個DFA D,需要計算它的狀態集Dstates和轉換函式Dtran(轉換表)。D的初始狀態是firstpos(n6)(n6是根結點),然後用下面的方法計算其Dstates和Dtran:

D的初始狀態是firstpos(n6)={ 1, 2, 3 },稱其為狀態A,我們需要計算Dtran[A, a]和Dtran[A, b]。在{ 1, 2, 3 }中,位置1和3處的標號為a,位置2處的標號為b,Dtran[A, a]=followpos(1)∪followpos(3)={ 1, 2, 3, 4 },Dtran[A, b]=followpos(2)={ 1, 2, 3 }。由於Dtran[A, a]產生了一個新狀態,稱其為狀態B,因此需要繼續計算,而Dtran[A, b]與狀態A相同,就不用再計算了。最後的結果見下表:

這樣就從正則表示式(a|b)*abb構建得到了一個DFA:

現在我們正式給出由一個正則表示式構建DFA的演算法:

輸入:一個正則表示式r;
輸出:一個識別L(r)的DFA D;
方法:首先根據擴充套件的正則表示式(r)#構建一棵抽象語法樹T,接著計算T的nullable、firstpos、lastpos和followpos函式,最後根據這四個函式的結果構造D的狀態集Dstates和轉換函式Dtran。
最小化DFA的狀態數
對同一個語言來說,可能存在多個識別此語言的DFA。例如對正則表示式(a|b)*abb,下面兩個DFA都能識別它:

這兩個DFA不僅每個狀態的名字不同,而且連狀態數也不一樣。實際上,任何正則語言都有一個唯一的狀態數目最少的DFA,本節就將介紹如何把一個DFA的狀態數最小化。

在一個DFA中,如果有這樣兩個狀態:

都不是終止狀態;
對任意輸入總是轉移到同一個狀態。
那麼這兩個狀態是等價的。例如在上面提到的正則表示式(a|b)*abb的左邊的DFA中,狀態A和C是等價的,因為它們都不是終止狀態,並且對輸入a都轉移到狀態B,對輸入b都轉移到狀態C。

這就給出了一個最小化DFA狀態數的思路——把等價的狀態合併。因此我們有下面的演算法:

輸入:一個DFA D1,其狀態集合是S,輸入符號集合為∑,初始狀態為s0,終止狀態集合為F;
輸出:一個DFA D2,它和D1接受相同的語言,且狀態數最少;
方法:

構造包含兩個組F和S-F的初始劃分∏,這兩個組分別是D1的終止狀態組和非終止狀態組;
應用下圖中的方法構造新的分劃∏new: 

如果∏new=∏,令∏final=∏並跳到步驟4,否則重複步驟2;
在分劃∏final的每個組中選一個狀態作為該組的代表,這些代表構成了D2的狀態集。D2的初始狀態是包含D的初始狀態組的代表,D2的終止狀態是包含D的終止狀態組的代表。令s是∏final中某個組的代表,在D1中從s經輸入a到達狀態t,令r是t所在組的代表,則在D2中有一個從s經輸入a到達r的轉換。
對於上面給出的正則表示式(a|b)*abb左邊的DFA,最小化其狀態數的過程如下:

初始分劃∏為{ A, B, C, D }和{ E }兩個組,分別是非終止狀態組和終止狀態組;
在∏中,{ E }無法再分,需要對{ A, B, C, D }進行劃分。對輸入a,A、B、C、D都轉移到B;對輸入b,A和C轉移到C,B轉移到D,D轉移到E,由於A、B、C對輸入b都轉移到同一個組{ A, B, C, D }中的狀態,而D對輸入b轉移到另一個組{ E }中的狀態,因此把{ A, B, C, D }劃分成{ A, B, C }和{ D }。此時∏new為{ A, B, C }、{ D }和{ E };
在∏new中,需要對{ A, B, C }進行劃分。對輸入a,A、B、C都轉移到B;對輸入b,A和C轉移到C,B轉移到D,因此把{ A, B, C }劃分成{ A, C }和{ B }。此時∏new為{ A, C }、{ B }、{ D }和{ E };
在∏new中,此時所有組都不能再分,因此此時的∏new就是∏final。合併狀態A和C後得到的狀態數最少的DFA就是右邊的DFA。
詞法分析過程
到此,我們已經介紹了和詞法分析相關的大部分內容,包括基本的詞法單元、模式和詞素,到正則表示式,再到有窮自動機,最後是從正則表示式轉換到有窮自動機,現在簡單回顧一下整個詞法分析的過程:

首先,對某個正則語言L,構造能夠描述其的正則表示式r;
然後,需要將r轉換成一個有窮自動機。這裡有三種方法,一是直接轉換成NFA,而是直接轉換成DFA,三是先轉換成NFA,再把NFA轉換成DFA;
最後,如果將r轉換成了一個DFA,需要將此DFA的狀態數最小化。
--------------------- 
作者:jzyhywxz 
來源:CSDN 
原文:https://blog.csdn.net/jzyhywxz/article/details/78307168 
版權宣告:本文為博主原創文章,轉載請附上博文連結!