實驗二 DFA識別句子
一、實驗目的
加深對DFA工作原理的理解。
二、實驗內容
- 1.設計固定DFA。也就是說用if-then-else(一般用來實現字母表中只有兩個字母的情況)、switch(大於兩個字母的情況)、for(用於控制輸入字符串,長度為n的字符串,for循環n次)等語句表示DFA。一個函數定義一個DFA;
- 2.設計文件形式存儲DFA。設計文件格式,DFA動態生成,使用字符串來驗證DFA的有效性和正確性;(使用面向對象的方法。對於k個狀態的DFA,生成相應的k個狀態對象;狀態轉換應通過對象間的消息傳遞來實現)
- 3.圖形化表示。用java或者VC中圖形功能實現圖形化的dfa。(選作)
前置知識1:DFA
什么是FA,也叫有窮狀態自動機;書上是這么說的👇,是一個五元組(狀態集合,字母表,狀態轉移表,開始狀態,終止狀態集合)
什么是DFA,也是一個五元組,在FA的基礎上加了一個約束條件:每一個狀態結點只能發出一條具有相同符號的邊;也就是同一狀態不能發出(輸入字符相同的)兩條邊上。可以發出輸入字符不同的多條邊
下圖就是一個DFA栗子👇
下圖就是NFA的栗子👇(允許從一個狀態發出多條具有相同符號的邊,甚至允許發出標有ε(表示空)符號的邊)
完成這個實驗,只需要知道DFA就可以了。
前置知識2:有向圖
什么是有向圖:由頂點和有方向的邊構成的圖
如何在程序中存儲有向圖?
可以使用數據結構中學的“鄰接矩陣”
“鄰接矩陣”就是一個“二維表”
DFA實質就是一個有向圖,各個頂點和其它頂點之間,使用有向邊相連接;而DFA的狀態轉移表,就是它的鄰接矩陣。
核心遞歸框架
目的:DFA識別句子
先明晰目的:判斷讀入的句子是否能被我們設定的DFA識別
比如一個DFA功能是“識別偶數個0的句子”;那么讀入句子“00”,偶數個0就能被識別,讀入“000”,奇數個0就不能被識別。
什么情況下識別了句子:能到達終結點
什么情況下能被識別?:從初始狀態讀入一個句子,最終能到達終結狀態結點,就是被識別了。
什么情況下不能被識別?:從初始狀態讀入一個句子,到最后到達非終結狀態結點,就是不能被識別。
DFA的有向圖上如何判斷識別了句子?
“是否能被識別”,在DFA的有向圖上如何表示?就是,從初始結點q0,一次讀句子的一個字符,最終到達了終結狀態。
還是那個栗子:識別偶數個0的DFA,假如我們輸入的句子是 “00”👇
還是那個栗子:識別偶數個0的DFA,假如我們輸入的句子是 “000”👇
核心遞歸框架偽代碼
下面兩個方法都可以
第一種遞歸框架:遞歸參數:狀態結點q1,讀入的剩余句子字符
//遞歸參數:狀態結點q1,讀入的剩余句子字符
bool dfs(node 結點q1,string 句子sentence){
//1.遞歸出口:當句子為空 或者 句子長度為0的時候
if(sentence == NULL || sentence.length() == 0){
if(結點q1 是 終結點){
return true; //最后到達終結點;表示句子可以被這個dfa識別
}else{
return false; //否則 是非終結點;表示句子不能被這個dfa識別
}
}
//讀入sentence的第一個字符后,到達新的狀態結點q2
node q2 = 狀態轉移(q1,sentence[0]) //q1讀入一個字符到達q2
return dfs(q2,"去除sentence第一個字符的剩下句子") //下一次遞歸參數: 結點q2,去除sentence第一個字符的剩下句子
}
把句子sentence設置為全局變量
第二種遞歸框架:遞歸參數:結點q1,當前讀入的字符在"讀入的句子"中的下標
//或者把句子sentence設置為全局變量
//下面一種遞歸思路,更換了遞歸參數,可能更容易理解
//遞歸參數:結點q1,當前讀入的字符在"讀入的句子"中的下標
bool dfs(node q1,int index){
//1.遞歸出口:當讀入到句子的最后一個字符了,這時到達遞歸出口,判斷轉移后的結點是否是終結點
if(index == sentence.length() -1){
node q2 = 狀態轉移(q1,sentence[index]); //q1讀入一個字符到達q2
if(結點q2 是 終結點){
return true; //最后到達終結點;表示句子可以被這個dfa識別
}else{
return false; //否則 是非終結點;表示句子不能被這個dfa識別
}
}
//狀態轉移 從q1讀入字符sentence[indx] 到達q2
node q2 = 狀態轉移(q1,sentence[index]); //q1讀入一個字符到達q2
return dfs(q2,index+1); //繼續遞歸
}
開始寫代碼1.設計固定DFA
采用面向對象的方式編程,沒有對象就new一個。
1-1:先寫一個狀態結點類
狀態結點類的整體構造是這樣的👇
1-2:DFA類
DFA的整體構造是這樣的👇
5元組對應5個屬性,其它還應該有狀態表結點個數、字母表字符個數、最大存儲的結點個數等屬性。👇
通過書上的一個DFA例子,理解一下狀態集合、終結點集合、字母表集合是這樣存儲的👇
1-3:幾個必要的函數
1-4:選一個栗子
選用例3-1 有窮自動機M: ({q0,q1,q2},{0},轉換函數,q0,{q2}),作為樣例
這個自動機功能是:識別偶數個0,比如00是合法的句子;而000就是非法的句子。
狀態轉換表如下
下圖是例題3-1的DFA圖👇
init()函數初始化例3-1的自動機👇,把例3-1的五元組分別存儲到實例對象的5個屬性中
graphInit()函數初始化狀態轉換表
主要理解狀態轉換表(有向圖的鄰接矩陣)👇
狀態集合、終結點集合、字母表集合是這樣存儲的👇
**然后試着理解狀態轉換表的存儲**👇
1-5:核心,遞歸程序識別句子
**run()啟動函數**👇
dfs()核心遞歸程序👇
1-6:例3-1栗子運行的效果
例3-1的DFA,功能:只識別偶數個0的句子。
主函數:
運行效果:
1-7:再舉一個栗子,會不會更清楚一點
例3-3的DFA,**功能:識別末尾是000的句子。**比如:110000是能夠識別正確的;而10100,因為末尾只有兩個0所以無法識別
例3-3DFA如下圖👇
把例3-3的五元組,在計算機中存儲,**“形式化”**,今天課上學到的名詞。。還是動詞??
對應在代碼中就是這樣的👇,修改init()函數,初始化DFA實例對象的五元組
Main函數中創建DFA類的實例化對象,init2()函數讀入例3-3的dfa五元組;再調用run()啟動程序,輸入要識別的句子即可。
###運行效果如下:
例3-3的DFA,功能:識別末尾是000的句子。
所以👇
輸入句子'111000',識別成功
輸入句子'10100',識別出錯
輸入句子'101011000',識別成功
輸入句子'1111',識別出錯
綜上,實現了,一個函數對應一個固定DFA。
開始寫代碼2.文件形式存儲DFA
步驟主要是這樣的: * 1.從文件中讀取每一行 * 2.把每一行,字符串處理,解析成五元組;比如,第一行是狀態結點,就解析為結點,存儲到狀態結點集合中。 * 3.Main函數中運行run(),輸入句子
我的文件里的五元組格式是這樣的👇
字符串處理,比較繁瑣,需要解析各個字符,讀入到 DFA類的五元組中;當然如果文件存儲的格式簡單一點,就不用像我下面寫的這樣復雜了
每次讀一行
這里用,(){}等等這些字符來分隔,狀態節點、字符、轉換函數
從文件中讀入DFA的,僅供參考的 “從文件中讀入DFA,存儲dfa的五元組” 代碼如下👇
public void loadFromFile(String path) {
File file = new File(this.path);
StringBuffer result = new StringBuffer();
List<String> resultList = new ArrayList<String>();
this.clear();
try {
BufferedReader bReader = new BufferedReader(new InputStreamReader(new FileInputStream(file),"UTF-8"));
String tempString = null;
while((tempString = bReader.readLine()) != null) {
resultList.add(tempString);
}
bReader.close();
String stateNodesString = resultList.get(0);
String alphabetString = resultList.get(1);
String initNodeString = resultList.get(2);
String finalString = resultList.get(3);
if(dealInputString(stateNodesString,0) == false) {
System.out.println("DFA有誤!");
}
if(dealInputString(alphabetString,1) == false) {
System.out.println("DFA有誤!");
}
if(dealInputString(initNodeString, 2) == false) {
System.out.println("DFA有誤!");
}
if(dealInputString(finalString, 3) == false) {
System.out.println("DFA有誤!");
}
if(dealFunction(resultList) == false) {
System.out.println("DFA有誤!");
}
}catch (Exception e) {
System.out.println(e);
System.out.println("加載文件出錯");
}
}
private Boolean dealFunction(List<String> resultList) {
//從resultList的第四行開始是 狀態轉移函數
this.graphInit();
if(resultList.size() <= 4) return false;
for(int i=4;i<resultList.size();i++) {
String function = resultList.get(i);
if(dealLine(function) == false) return false;
}
return true;
}
private Boolean dealLine(String function) {
StringBuffer currentBuffer = new StringBuffer();
int charIndex = -1;
int nodeIndex1 = -1;
int nodeIndex2 = -1;
for(int i=0;i<function.length();i++) {
if(function.charAt(i) == '(' ||function.charAt(i) == ')' || function.charAt(i) == '>' || function.charAt(i) == '-') {
currentBuffer.delete(0, currentBuffer.length());
}else if(function.charAt(i) == ',' ) {
//這里是第一個結點
nodeIndex1 = findNodeIndexByName(currentBuffer.toString());
if(nodeIndex1 == -1) return false;
currentBuffer.delete(0, currentBuffer.length());
}else if(i == function.length()-1) {
//這里是第二個結點
currentBuffer.append(function.charAt(i));
nodeIndex2 = findNodeIndexByName(currentBuffer.toString());
if(nodeIndex2 == -1) return false;
currentBuffer.delete(0, currentBuffer.length());
}else if(i>0 && function.charAt(i-1) == ','){
//這里是字符
Character c = function.charAt(i);
charIndex = getCharIndex(c);
if(charIndex == -1) return false;
}else {
currentBuffer.append(function.charAt(i));
}
}
if(nodeIndex1 == -1 || nodeIndex2 == -1 || charIndex == -1) return false;
this.graph[nodeIndex1][charIndex] = nodeIndex2;
System.out.println(stateNodes.get(nodeIndex1).getName()+"," + alphabet.get(charIndex) +" -> "+stateNodes.get(nodeIndex2).getName());
return true;
}
private void clear() {
// TODO Auto-generated method stub
this.stateNodes.clear();
this.alphabet.clear();
this.finalNodes.clear();
this.graphInit();
}
public boolean dealInputString(String s,int type) {
if(s == null) return false;
StringBuffer currentStateBuffer = new StringBuffer();
//type == 0時 處理的是 狀態結點集合: 把輸入的一行字符串轉換為狀態結點,並存入狀態結點集合stateNodes中
if(type == 0) {
for(int i=0;i<s.length();i++) {
if(s.charAt(i) == '{') {
currentStateBuffer.delete(0, currentStateBuffer.length());
}else if(s.charAt(i) == ',' || s.charAt(i) == '}') {
this.stateNodes.add(new Node(currentStateBuffer.toString()));
currentStateBuffer.delete(0, currentStateBuffer.length());
}else {
currentStateBuffer.append(s.charAt(i));
}
}
this.nodesNum = stateNodes.size();
for(Node node: this.stateNodes) {
System.out.println(node.getName());
}
}else if(type == 1) {
for(int i=0;i<s.length();i++) {
if(s.charAt(i) == '{') {
continue;
}else if(s.charAt(i) == ',' || s.charAt(i) == '}') {
continue;
}else {
this.alphabet.add(s.charAt(i));
}
}
this.alphabetNum = alphabet.size();
for(Character c: this.alphabet) {
System.out.println(c);
}
}else if(type == 2) {
Node initNodeTemp = findNodeByName(s);
if(initNodeTemp == null) return false;
this.initNode = findNodeByName(s);
System.out.println(findNodeByName(s).getName());
}else if(type == 3) {
for(int i=0;i<s.length();i++) {
if(s.charAt(i) == '{') {
currentStateBuffer.delete(0, currentStateBuffer.length());
}else if(s.charAt(i) == ',' || s.charAt(i) == '}') {
Node finalNode = findNodeByName(currentStateBuffer.toString());
if(finalNode == null) return false;
this.finalNodes.add(finalNode);
currentStateBuffer.delete(0, currentStateBuffer.length());
}else {
currentStateBuffer.append(s.charAt(i));
}
}
for(Node node: this.finalNodes) {
System.out.println(node.getName());
}
}
return true;
}
public Node findNodeByName(String name) {
for(Node node : stateNodes) {
//node.getName() == name false:原因 == 用來判斷引用
if(node.getName().equals(name))
return node;
}
return null;
}
public int findNodeIndexByName(String name) {
int i = 0;
for(Node node : stateNodes) {
//node.getName() == name false:原因 == 用來判斷引用
if(node.getName().equals(name))
return i;
i++;
}
return -1;
}
運行效果和第一問相同:
3.圖形化表示DFA
用Graphics類,重寫paint方法,paint方法就是在窗口界面上畫圖。
詳細可以參考這個鏈接,很簡單但具體的栗子
這里用面向對象編程,就是寫幾個類,DFA類 與 狀態結點類、窗口類各自執行自己的功能,數據和視圖分離。