一、概念概述
給定一個單詞,判斷該單詞是否滿足我們給定的單詞描述規則,需要用到編譯原理中詞法分析的相關知識,其中涉及到的兩個很重要的概念就是正規式(Regular Expression)和有窮自動機(Finite Automata)。正規式是描述單詞規則的工具,首先要明確的一點是所有單詞組成的是一個無窮的集合,而正規式正是描述這種無窮集合的一個工具;有窮自動機則是識別正規式的一個有效的工具,它分為確定的有窮自動機(Deterministic Finite Automata,DFA)和不確定的有窮自動機(Nondeterministic Finite Automata,NFA)。對於任意的一個單詞,將其輸入正規式的初始狀態,自動機每次讀入一個字母,根據單詞的字母進行自動機中狀態的轉換,若其能夠准確的到達自動機的終止狀態,就說明該單詞能夠被自動機識別,也就滿足了正規式所定義的規則。而DFA與NFA之間的差異就是對於某一個狀態S,輸入一個字符a,DFA能夠到達的下一個狀態有且僅有一個,即為確定的概念,而NFA所能到達的狀態個數大於或等於一個,即不確定的概念。因為NFA為不確定的,我們無法准確的判斷下一個狀態是哪一個,因此識別一個正規式的最好的方式是DFA。那么,如何為一個正規式構造DFA就成了主要矛盾,解決了這個問題,詞法分析器就已經構造完成。從正規式到DFA需要通過兩個過程來完成:
①從正規式轉NFA:對輸入的正規式字符串進行處理轉成NFA;
②從NFA轉DFA:對NFA進行確定化處理轉成DFA;
二、正規式轉NFA
【1】在正式開始算法描述之前需要先了解以下一些基礎概念和規定:
1)正規式由兩種字符組成:
①操作符:(僅考慮以下幾種操作符)
或:|, 閉包:* ,左右括號:(),隱含的連接操作符:即AB;
②非操作符:除了以上操作符的字符均可作為非操作符,如字母、數字等;
2)正規式轉NFA由以下幾種基礎的情況組成:
①輸入為空 ε:
②輸入為單個字符a:
③輸入為a|b(或運算):
④輸入為a*(閉包運算):
⑤輸入為ab(隱含的連接運算):
從以上5種基礎情況的分析可以看出,對於每種運算操作都是有固定形式的,最基礎的情況就是②,其余的幾種操作符均是在這種情況下通過增加頭尾節點和狀態轉換方向導出的。因此對於不同的操作符、對應的NFA以及狀態轉換符,僅需要在原先的NFA基礎上增加首尾節點和狀態轉換即可構造新的NFA,以下為代碼:
public class GenerateNFAMethod { GetStateNumber getNum = new GetStateNumber(); char nul = 'E';//nul表示狀態轉換條件為空 //當遇到非符號數時只需新建一個NFA,其中包含起點和終點; public NFA meetNonSymbol(char nonSymbol){ NFANode headNode = new NFANode(getNum.getStateNum(),nul); NFANode tailNode = new NFANode(getNum.getStateNum(),nonSymbol);//入方向的符號為nonSymbol headNode.nextNodes.add(tailNode); NFA newNFA = new NFA(headNode,tailNode); return newNFA; } //當遇到符號數'*'時增加頭尾節點並連接 public NFA meetStarSymbol(NFA oldNFA){ NFANode oldHeadNode = oldNFA.headNode; NFANode oldTailNode = oldNFA.tailNode; NFANode newHeadNode = new NFANode(getNum.getStateNum(),nul); NFANode newTailNode = new NFANode(getNum.getStateNum(),nul); newHeadNode.nextNodes.add(oldHeadNode); newHeadNode.nextNodes.add(newTailNode); oldTailNode.nextNodes.add(newTailNode); oldTailNode.nextNodes.add(oldHeadNode); NFA newNFA = new NFA(newHeadNode,newTailNode); return newNFA; } //當遇到符號數為'.'即表示連接操作時 public NFA meetAndSymbol(NFA firstNFA, NFA secondNFA){ //前一個NFA的尾節點與后一個NFA的頭節點相連,需要增加頭尾節點重新組成一個NFA; NFANode newHeadNode = new NFANode(getNum.getStateNum(),nul); NFANode newTailNode = new NFANode(getNum.getStateNum(),nul); firstNFA.tailNode.nextNodes.add(secondNFA.headNode); newHeadNode.nextNodes.add(firstNFA.headNode); secondNFA.tailNode.nextNodes.add(newTailNode); NFA newNFA = new NFA(newHeadNode,newTailNode); return newNFA; } //當遇到符號數為'|'時添加頭尾節點進行或操作 public NFA meetOrSymbol(NFA firstNFA, NFA secondNFA){ NFANode oldFirstHeadNode = firstNFA.headNode; NFANode oldSecondHeadNode = secondNFA.headNode; NFANode oldFirstTailNode = firstNFA.tailNode; NFANode oldSecondTailNode = secondNFA.tailNode; NFANode newHeadNode = new NFANode(getNum.getStateNum(),nul); NFANode newTailNode = new NFANode(getNum.getStateNum(),nul); newHeadNode.nextNodes.add(oldFirstHeadNode); newHeadNode.nextNodes.add(oldSecondHeadNode); oldFirstTailNode.nextNodes.add(newTailNode); oldSecondTailNode.nextNodes.add(newTailNode); NFA newNFA = new NFA(newHeadNode,newTailNode); return newNFA; } }
【2】數據結構設計:
①雙棧設計:NFA棧以及符號棧,兩者均含有pop()、push()、top()操作;
②NFA棧中存儲的元素為NFA圖,以下為NFA圖的設計:
1' NFA圖由兩個NFANode組成,一個表示NFA圖的頭節點,一個表示NFA圖的尾節點,各種運算符操作都是在原先的NFA圖的首尾節點上進行操作的,而對NFA內部的節點並沒有影響,故此結構 設計具有其合理性;
2' NFANode設計:其表示NFA圖中的某一個狀態節點,其由3個屬性構成:1、stateNum表示當前狀態節點的狀態標志;2、pathChar表示由前一個狀態轉換到當前狀態所需的字符;
3、ArrayList<NFANode> nextNodes表示與當前狀態后繼相連的所有狀態節點集合;
③符號棧中存儲的元素為char類型的currentSymbol表示當前符號棧中存儲的運算符;
以下為該數據結構的代碼:
1、NFANode:
//構建NFA圖中的節點單元 public class NFANode { public int stateNum; //pathChar表示前一個節點通過字符pathChar轉到當前狀態,對於同一個狀態,它有很多入方向,故根據不同的入方向相應的改變pathChar的值 public char pathChar; public ArrayList<NFANode> nextNodes;//鏈表形式進行后繼節點存儲 public NFANode(int stateNum, char pathChar){ this.pathChar = pathChar; this.stateNum = stateNum; this.nextNodes = new ArrayList<NFANode>(); } }
2、NFA:
//定義存儲在NFA棧中的NFA結構:頭結點和尾結點 public class NFA { public NFANode headNode; public NFANode tailNode; public NFA(NFANode headNode,NFANode tailNode){ this.headNode = headNode; this.tailNode = tailNode; } }
3、NFAStack:
public class NFAStack { public NFA currentNFA;//當前位置的NFA public NFAStack nextNFA;//下一個入棧的NFA public NFAStack(NFA currentNFA){ this.currentNFA = currentNFA; this.nextNFA = null; } //定義pop方法返回棧頂元素 public void pop(){ NFA resultNFA; NFAStack tempNFA = this;//定義循環遍歷器 NFAStack lastNFA = this;//定義棧中前一個NFAStack元素 if(tempNFA.nextNFA==null){ System.out.println("NFAStack 為空!"); } while(tempNFA.nextNFA!=null){ lastNFA = tempNFA; tempNFA = tempNFA.nextNFA; } resultNFA=lastNFA.nextNFA.currentNFA; lastNFA.nextNFA=null; } //定義push方法將元素加入棧頂 public void push(NFAStack newNFA){ NFAStack tempNFA = this;//定義遍歷器 while(tempNFA.nextNFA!=null){ tempNFA = tempNFA.nextNFA; } tempNFA.nextNFA = newNFA; } //定義top方法 public NFA top(){ NFAStack tempNFA = this;//定義遍歷器 while(tempNFA.nextNFA!=null){ tempNFA = tempNFA.nextNFA; } return tempNFA.currentNFA; } }
4、SymbolStack:
public class SymbolStack { public char currentSymbol; public SymbolStack nextSymbol; public SymbolStack(char currentSymbol){ this.currentSymbol = currentSymbol; this.nextSymbol = null; } //定義pop符號棧頂元素的方法 public void pop(){ char result; SymbolStack tempStack = this;//定義符號棧遍歷器 SymbolStack lastStack = this;//定義前一個棧中元素 if(tempStack.nextSymbol==null){ System.out.println("SymbolStack為空!"); } while(tempStack.nextSymbol!=null){ lastStack = tempStack; tempStack = tempStack.nextSymbol; } result = lastStack.nextSymbol.currentSymbol; lastStack.nextSymbol = null; } //定義push方法加入棧頂元素 public void push(SymbolStack newSymbol){ SymbolStack tempStack = this; while(tempStack.nextSymbol!=null){ tempStack = tempStack.nextSymbol; } tempStack.nextSymbol = newSymbol; } public char top(){ SymbolStack tempStack = this; while(tempStack.nextSymbol!=null){ tempStack = tempStack.nextSymbol; } return tempStack.currentSymbol; } }
【3】基於以上的基本概念和規定,進行以下的算法分析設計:
1)算法整體想法闡述:將正規式轉成NFA實質上就是對輸入的字符串進行處理,通過不斷的讀入字符增加首尾節點和狀態轉換后轉化為一張NFA圖,有點類似於中綴轉后綴的思想。我的處理方式是建立兩個棧:符號棧和NFA棧。在從左至右讀入正規式的字符時對字符進行判斷,若其為操作符,則將其壓入符號棧中,若為非操作符,則將該字符轉換為NFA后壓入NFA棧中,當讀完最后一個字符后將符號棧中的操作符一一彈出,彈出一個操作符跟着彈出兩個NFA棧的棧頂NFA,根據相應的操作符對兩個NFA進行處理后轉換為新的NFA壓入NFA棧中。當處理完所有的符號棧中的符號后彈出NFA棧中的唯一元素即為我們所求的NFA(詳細處理將在下面闡述)
2)針對非操作符以及各種操作符的詳細處理:
1' 當遇到左括號’(‘時:直接壓入棧中即可;
2' 當遇到右括號')'時:依次彈出符號棧中的符號直到遇到'('為止。在依次彈出符號棧中的符號時對NFA棧中的NFA元素的操作是:彈出NFA棧頂的兩個元素,進行相應的符號操作后合成一個新的NFA並壓入棧中;
3' 當遇到或操作'|'時:此操作符的優先級最低,在壓入棧時需要對符號棧中'('以上的符號進行判斷,對於優先級高於或操作的連接操作需要將其先彈出后進行連接操作,直到棧中不存在連接操作后再將'|'壓入符號棧中;
4' 當遇到閉包操作'*'時:此操作符的優先級最高,無須將其壓入符號棧中,直接將NFA棧中的棧頂NFA彈出棧后進行閉包操作后再將新的NFA壓入NFA棧;
5' 當遇到隱含的連接操作'.'時:該操作符是隱含在正規式中的 ,如:ab,a(b|c)*。因此在掃描過程中,需要對是否添加連接符進行判斷。其有以下三種情況:當遇到非運算符時,需要對其后面的符號進行判斷,若遇到左括號或非運算符時,則需要往符號棧中添加連接符'.';當遇到閉包運算符'*'時,需要判斷其右邊的符號,若非'|'和')'則需要在符號棧中天年假連接符'*';當遇到右括號')'時需要對其右邊的符號進行判斷,若遇到'('或非運算字符時需要加入連接符'.';
在處理完正規式中的字符后,若符號棧中仍有符號存在,則依次彈出符號棧中的元素和NFA中的NFA,不斷進行計算后得到最終的NFA結果。以下代碼為即為上述描述的代碼形式:
public NFA getFinalNFA(String regExp){ //建立符號棧和NFA棧 NFAStack nfaStack = new NFAStack(null); SymbolStack symbolStack = new SymbolStack('0'); NFAStack nfaHead = nfaStack; SymbolStack symbolHead = symbolStack; //對讀入的字符串進行處理 for(int i=0;i<regExp.length();i++){ char cha = regExp.charAt(i); switch(cha){ case '(': //遇到左括號就要放入棧 symbolHead.push(new SymbolStack('(')); break; case '|': //或符號優先級最低,遇到這個符號要進行優先級的判斷,當遇到連接符'.'時就一直top和pop運算 while(symbolHead.top()=='.'){ NFA secondNFA = nfaHead.top(); nfaHead.pop(); NFA firstNFA = nfaHead.top(); nfaHead.pop(); NFA newAndNFA = generator.meetAndSymbol(firstNFA, secondNFA); nfaHead.push(new NFAStack(newAndNFA)); symbolHead.pop(); } symbolHead.push(new SymbolStack('|')); break; //遇到'*'直接改變NFA棧頂元素后再將其壓入棧 case '*': NFA topNFA = nfaHead.top(); nfaHead.pop(); NFA newNFA = generator.meetStarSymbol(topNFA); nfaHead.push(new NFAStack(newNFA)); if(i!=regExp.length()-1&®Exp.charAt(i+1)!='|'&®Exp.charAt(i+1)!=')'){ symbolHead.push(new SymbolStack('.')); } break; case ')': while(symbolHead.top()!='('){ NFA secondNFA = nfaHead.top(); nfaHead.pop(); NFA firstNFA = nfaHead.top(); nfaHead.pop(); if(symbolHead.top()=='.'){ NFA newAndNFA = generator.meetAndSymbol(firstNFA, secondNFA); nfaHead.push(new NFAStack(newAndNFA)); } else{ NFA newOrNFA = generator.meetOrSymbol(firstNFA, secondNFA); nfaHead.push(new NFAStack(newOrNFA)); } symbolHead.pop(); } symbolHead.pop(); //判斷右括號右邊的字符是否為'('或非運算符 if(i!=regExp.length()-1&®Exp.charAt(i+1)!=')'&®Exp.charAt(i+1)!='|'&®Exp.charAt(i+1)!='*'){ symbolHead.push(new SymbolStack('.')); } break; default: NFA nonSymbolNFA = generator.meetNonSymbol(cha); //判斷連接符是否要加 //連接符優先級較大,所以可以直接加 if(i!=regExp.length()-1&®Exp.charAt(i+1)!='|'&®Exp.charAt(i+1)!='*'&®Exp.charAt(i+1)!=')'){ symbolHead.push(new SymbolStack('.')); } nfaHead.push(new NFAStack(nonSymbolNFA)); break; } } //字符串讀完后符號棧中元素若不為空則需要從棧頂配合NFA棧進行清空操作 while(symbolHead.top()!='0'){ char symbol = symbolHead.top(); symbolHead.pop(); NFA secondNFA = nfaHead.top(); nfaHead.pop(); NFA firstNFA = nfaHead.top(); nfaHead.pop(); switch(symbol){ case '|': NFA newOrNFA = generator.meetOrSymbol(firstNFA, secondNFA); nfaHead.push(new NFAStack(newOrNFA)); break; case '.': NFA newAndNFA = generator.meetAndSymbol(firstNFA, secondNFA); nfaHead.push(new NFAStack(newAndNFA)); break; } } //最后僅剩NFA棧頂的一個最終的元素 return nfaHead.top(); }
三、由NFA轉DFA:
經過步驟二中的分析與設計,我們已經成功的將正規式轉成了NFA圖,剩下的就是在已知NFA的圖上進行操作,將NFA轉換成DFA。NFA與DFA之間的聯系點就是DFA中的一個狀態是由NFA中的若干個狀態所組成的,因此需要對DFA數據結構進行設計:
①DFANode:其由三個屬性組成:beginState(起始DFA狀態)、endState(終止DFA狀態)、pathChar(狀態轉換符),表示DFA的狀態轉換;
②DFAState:其由四個屬性組成:stateStr(狀態名)、NFAState(組成該DFA狀態的NFA狀態集合)、isBegin(是否為起始節點)、isEnd(是否為終止節點),表示DFA中的一個狀態;
以下為兩個數據結構的設計代碼:
//描述DFA圖中的某一個狀態的基本要素; public class DFAState { public String stateStr; public ArrayList<Integer> NFAState; public boolean isBegin; public boolean isEnd; public DFAState(String stateStr, ArrayList<Integer> NFAState, boolean isBegin, boolean isEnd){ this.stateStr = stateStr; this.NFAState = NFAState; this.isBegin = isBegin; this.isEnd = isEnd; } }
//描述DFA圖中的狀態轉換節點,包括起始狀態、終止狀態、轉換字符 public class DFANode { public DFAState beginState; public DFAState endState; public char pathChar; public DFANode(DFAState beginState, DFAState endState, char pathChar){ this.beginState = beginState; this.endState = endState; this.pathChar = pathChar; } }
NFA中存在空轉,因此能通過空轉到達的狀態都視作同一個狀態,因此如何找到NFA中相同的狀態並將它們重新組合成一個新的DFA狀態就成了我們的主要矛盾。我對該算法的設計分為以下兩步走:
對於NFA中的一個狀態N1,當前輸入的字符為a,建立一個新的空狀態集D1
①首先將狀態N1能夠通過字符a到達的狀態全部加入到空狀態集D1中;
②對D1中的狀態進行操作:對於D1中的每一個NFA狀態,將其能夠通過空跳轉所能到達的NFA狀態節點加入到D1中,該操作需要用遞歸實現,且考慮到了NFA中的后繼節點可能會產生重復,所以要檢查 到達的節點是否有重復節點;
經過以上兩步之后得到的狀態集D1即構成了NFA中的狀態N1通過字符a所能到達的DFA狀態。而在實際進行NFA轉DFA時,起始狀態的即為NFA中的起始狀態通過空跳轉所能到達的狀態所構成的一個NFA狀態的集合,因此需要通過循環來對該狀態集中的每一個NFA狀態進行以上的兩步,且輸入的字符為字符集即正規式中存在所有非運算符集。對於每一個字符,從最初的DFA狀態開始,不斷的進行以上兩步操作,得到新的狀態集,判斷該狀態集是否已經存在,若不存在則將新的狀態集加入到已知狀態集集合中,直到最終不在產生新的狀態集為止。在這一過程中,我們得到了DFA中的初始狀態、終止狀態以及轉換字符,即完成了由NFA到DFA的轉換,這就是著名的子集構造法。以下為NFA轉DFA的核心代碼:
//返回最終的DFA狀態轉換節點 ArrayList<DFANode> resultDFANodes = new ArrayList<>(); //記錄NFA狀態圖中的起始狀態和終止狀態 int beginNFAState = resultNFA.headNode.stateNum; int endNFAState = resultNFA.tailNode.stateNum; //獲取正則表達式中的除運算符外的字符 ArrayList<Character> characterList = getStateStr.getCharacters(regExp); //獲取起始節點通過控制所能到達的左右狀態節點 ArrayList<NFANode> initialState = new ArrayList<>(); initialState.add(resultNFA.headNode); ArrayList<NFANode> tempState = findNulMatchNFANodes(initialState,new ArrayList<NFANode>()); //建立一個list表示已有的未標記的狀態,其中元素為含有NFANode的list ArrayList<ArrayList<NFANode>> unsignedState = new ArrayList<>(); ArrayList<ArrayList<Integer>> unsignedStateNums = new ArrayList<>(); //建立一個Map表示存儲已產生的狀態,鍵為list,值表示狀態名;用來查找現有狀態的狀態名 Map<ArrayList<Integer>,String> existState = new HashMap<ArrayList<Integer>,String>(); unsignedState.add(tempState); unsignedStateNums.add(getStateNumList(tempState)); existState.put(getStateNumList(tempState), getStateStr.getStateStr()); while(!unsignedState.isEmpty()){ DFAState beginState = new DFAState(existState.get(getStateNumList(unsignedState.get(0))),getStateNumList(unsignedState.get(0)),testIsBegin(beginNFAState,getStateNumList(unsignedState.get(0))),testIsEnd(endNFAState,getStateNumList(unsignedState.get(0)))); for(Character cha:characterList){ ArrayList<NFANode> nextState = findNewNFAStateSet(unsignedState.get(0),cha); ArrayList<Integer> tempIntegerList = getStateNumList(nextState); //已有的狀態集中不含有當前狀態則新建一個狀態 if(!existState.containsKey(tempIntegerList)){ existState.put(tempIntegerList, getStateStr.getStateStr()); } DFAState endState = new DFAState(existState.get(tempIntegerList),tempIntegerList,testIsBegin(beginNFAState,tempIntegerList),testIsEnd(endNFAState,tempIntegerList)); DFANode tempDFANode = new DFANode(beginState,endState,cha); resultDFANodes.add(tempDFANode); if(!unsignedStateNums.contains(tempIntegerList)){ unsignedState.add(nextState); unsignedStateNums.add(tempIntegerList); } } unsignedState.remove(0); } return resultDFANodes; } //輸入舊狀態節點集合和轉換字符,輸出新狀態節點集合 public ArrayList<NFANode> findNewNFAStateSet(ArrayList<NFANode> oldNFAStateSet,char pathChar){ ArrayList<NFANode> newNFAStateSet = new ArrayList<>();//記錄最終返回的NFANode狀態集 if(oldNFAStateSet.size()==0){ return newNFAStateSet; } //先找到匹配的狀態節點加入matchNodes中 ArrayList<NFANode> matchNodes = new ArrayList<>(); for(NFANode node:oldNFAStateSet){ for(NFANode nextNode:node.nextNodes){ if(nextNode.pathChar==pathChar&&!matchNodes.contains(nextNode)){ newNFAStateSet.add(nextNode); matchNodes.add(nextNode); } } } ArrayList<NFANode> matchResult = findNulMatchNFANodes(matchNodes,new ArrayList<NFANode>()); for(NFANode node:matchResult){ if(!newNFAStateSet.contains(node)){ newNFAStateSet.add(node); } } return newNFAStateSet; } //找到能夠通過空字符轉換得到的節點 public ArrayList<NFANode> findNulMatchNFANodes(ArrayList<NFANode> currentNodes,ArrayList<NFANode> NFANodeStack) { ArrayList<NFANode> newNFAStateSet = new ArrayList<>(); ArrayList<NFANode> nextNFAStateSet = new ArrayList<>(); if(currentNodes.size()==0){ return newNFAStateSet; } for(NFANode node:currentNodes){ NFANodeStack.add(node); newNFAStateSet.add(node); for(NFANode nextNode:node.nextNodes){ if(nextNode.pathChar==nul&&!NFANodeStack.contains(nextNode)){ nextNFAStateSet.add(nextNode); } } } ArrayList<NFANode> tempNodes = findNulMatchNFANodes(nextNFAStateSet,NFANodeStack); for(NFANode node:tempNodes){ if(!newNFAStateSet.contains(node)){ newNFAStateSet.add(node); } } return newNFAStateSet; }
四、DFA的最小化
從步驟三中我們已經得到了DFA中的狀態轉換集合,每個狀態轉換包含起始狀態、轉換字符和終止狀態。然而有些DFA中的狀態是無效的,有些DFA中的狀態是重復的,因此需要對DFA中的這狀態進行最小化操作。最小化操作需要經過兩步:1、消除無用狀態;2、合並等價狀態;
1、消除無用狀態:
什么是無用狀態?無用狀態即為從該自動機的開始狀態出發,任何輸入串也不能到達的那個狀態,或者這個狀態沒有通路到達終態,這樣的狀態即稱為無用狀態。消除無用狀態的算法我是這么設計的:從初始狀態出發,遍歷各種字符,將從初始狀態能到達的狀態放入一個集合S1中,其構成了初始狀態能到達的狀態;在S1的基礎上,從終止狀態出發,逆向遍歷各種字符,將能到達的狀態構成一個新的狀態S2,其剔除了不能到達的終態的狀態節點,以下為代碼:
//定義消除無用狀態的方法 public ArrayList<DFANode> eliminateNoUseState(ArrayList<DFANode> oldDFANodes){ //定義從起點能到達的節點的組合 ArrayList<DFANode> startPointReachDFANodes = new ArrayList<>(); //定義未遍歷的DFA中的狀態 ArrayList<String> nextDFAStates = new ArrayList<>(); //定義已遍歷的DFA中的狀態 ArrayList<String> existDFAStates = new ArrayList<>(); //找出開始狀態為起點的節點放入開始集和遍歷集 for(DFANode node:oldDFANodes){ if(node.beginState.isBegin){ startPointReachDFANodes.add(node); if(!nextDFAStates.contains(node.beginState.stateStr)){ nextDFAStates.add(node.beginState.stateStr); } } } while(!nextDFAStates.isEmpty()){ String currentState = nextDFAStates.get(0); existDFAStates.add(currentState); nextDFAStates.remove(0); for(DFANode node:oldDFANodes){ if(node.beginState.stateStr.equals(currentState)){ if(!startPointReachDFANodes.contains(node)){ startPointReachDFANodes.add(node); } if(!existDFAStates.contains(node.endState.stateStr)&&!nextDFAStates.contains(node.endState.stateStr)){ nextDFAStates.add(node.endState.stateStr); } } } } //定義能夠到達終點狀態的節點的組合,其為起點的逆過程 ArrayList<DFANode> reachEndPointDFANodes = new ArrayList<>(); //重置nextDFAStates和existDFAStates nextDFAStates = new ArrayList<>(); existDFAStates = new ArrayList<>(); for(DFANode node:startPointReachDFANodes){ if(node.endState.isEnd){ reachEndPointDFANodes.add(node); if(!nextDFAStates.contains(node.endState.stateStr)){ nextDFAStates.add(node.endState.stateStr); } } } while(!nextDFAStates.isEmpty()){ String currentState = nextDFAStates.get(0); existDFAStates.add(currentState); nextDFAStates.remove(0); for(DFANode node:startPointReachDFANodes){ if(node.endState.stateStr.equals(currentState)){ if(!reachEndPointDFANodes.contains(node)){ reachEndPointDFANodes.add(node); } if(!existDFAStates.contains(node.beginState.stateStr)&&!nextDFAStates.contains(node.beginState.stateStr)){ nextDFAStates.add(node.beginState.stateStr); } } } } return reachEndPointDFANodes; }
2、合並等價狀態:
定義兩個狀態S和T是否等價狀態需要滿足以下兩個條件:
①一致性條件:狀態S和狀態T必須同時為可接受狀態和不可接受狀態;
②蔓延性條件:對於所有輸入符號,狀態S和狀態T必須轉換到等價的狀態里;
一個著名的方法“分割法”可以把DFA(不含多余的無用狀態)的狀態分成一些不相交的子集,使得任何不同的兩個子集的狀態都是可區別的,而同一子集中的任何兩個狀態都是等價的。我對分割法的實現如下:
①初始化DFA中的狀態,將其分為終止狀態和非終止狀態;
②建立一個ArrayList<ArrayList<String>> splitStates,其包含切分狀態子集;
③建立一個Map<ArrayList<String>,ArrayList<String>> aimStateTypeList,其鍵表示已存在的狀態,其值表示到達該鍵值狀態的節點的集合。對於要遍歷的狀態集合,輸入的每一個字符都將對應着一個目標狀態,將該目標狀態作為map的鍵,然后將該狀態作為該map值集合中的一個元素添加。若遍歷完狀態后,map中的元素僅存在一個,說明當前便利的狀態集合不存在分裂,所以將改狀態加入到最終的狀態集合中,若出現了分裂,則將分裂后的狀態加入到遍歷集合中。
④循環遍歷遍歷集合直至遍歷集合為空為止,最終得到的狀態集合即為我們分割法所得到的集合,故進行相同狀態的合並后得到最終的最小化DFA。
以下為代碼:
//定義分割法合並等價狀態的String集合 public ArrayList<ArrayList<String>> splitSameState(ArrayList<DFANode> oldDFANodes,String regExp){ //划分最終的狀態集 ArrayList<ArrayList<String>> splitStates = new ArrayList<>(); ArrayList<ArrayList<String>> resultSplitStates = new ArrayList<>(); //划分終態和非終態 ArrayList<String> terminalState = new ArrayList<>(); ArrayList<String> nonterminalState = new ArrayList<>(); //獲取非運算符字符集 ArrayList<Character> characterList = getter.getCharacters(regExp); for(DFANode node:oldDFANodes){ if(node.beginState.isEnd){ if(!terminalState.contains(node.beginState.stateStr)) terminalState.add(node.beginState.stateStr); } else{ if(!nonterminalState.contains(node.beginState.stateStr)) nonterminalState.add(node.beginState.stateStr); } } if(terminalState.size()>0) splitStates.add(terminalState); if(nonterminalState.size()>0) splitStates.add(nonterminalState); while(!splitStates.isEmpty()){ ArrayList<String> currentState = splitStates.get(0); //初狀態指向末狀態的map,鍵為已存在狀態,值為新分裂出來的狀態 Map<ArrayList<String>,ArrayList<String>> aimStateTypeList= new HashMap<>(); for(Character cha:characterList){ for(String oldState:currentState){ for(DFANode node:oldDFANodes){ //找到當前節點對應的轉換路徑,加入以狀態節點集合為鍵值的map中 if(node.beginState.stateStr.equals(oldState)&&node.pathChar==cha){ ArrayList<String> endStateList = getContainArrayList(splitStates,node.endState.stateStr); if(aimStateTypeList.containsKey(endStateList)){ aimStateTypeList.get(endStateList).add(oldState); } else{ ArrayList<String> temp = new ArrayList<>(); temp.add(oldState); aimStateTypeList.put(endStateList, temp); } } } } //如果map的大小為1說明對於當前字符來說,這個轉換是到相同狀態,重置后繼續對下一個字符轉換進行判斷,否則直接break if(aimStateTypeList.size()==1){ aimStateTypeList= new HashMap<>(); continue; } else{ break; } } //判斷ArrayList的長度是否為0,如果為0,說明當前的狀態均為同一個狀態,將該狀態從splitStates中移除並加入最終的狀態集 if(aimStateTypeList.size()==0){ resultSplitStates.add(currentState); splitStates.remove(0); } //否則移出舊狀態,將map中的新狀態均加入splitStates中 else{ splitStates.remove(0); for(ArrayList<String> newList:aimStateTypeList.values()){ splitStates.add(newList); } } } return resultSplitStates; } //找到包含當前狀態的那一個切分子集 private ArrayList<String> getContainArrayList(ArrayList<ArrayList<String>> splitStates, String stateStr) { for(ArrayList<String> states:splitStates){ if(states.contains(stateStr)){ return states; } } return null; }
五、程序測試:
①輸入:a(a*|b*)a|b*
輸出:s0 b s2
s1 a s3
s2 b s2
s3 a s3
s4 a s6
s0 a s1
s1 b s4
s4 b s4
分別對應起始狀態、狀態轉換符、終止狀態
②輸入:1(0|1)*101
輸出:s5 1 s6
s3 0 s5
s6 0 s5
s1 1 s3
s3 1 s3
s4 1 s3
s6 1 s3
s0 1 s1
s1 0 s4
s4 0 s4
s5 0 s4
歡迎指正,轉載請注明出處,謝謝~