1、概念概述node
給定一個單詞,判斷該單詞是否知足咱們給定的單詞描述規則,須要用到編譯原理中詞法分析的相關知識,其中涉及到的兩個很重要的概念就是正規式(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;數據結構
2、正規式轉NFA閉包
【1】在正式開始算法描述以前須要先了解如下一些基礎概念和規定:工具
1)正規式由兩種字符組成:測試
①操做符:(僅考慮如下幾種操做符)this
或:|, 閉包:* ,左右括號:(),隱含的鏈接操做符:即AB;spa
②非操做符:除了以上操做符的字符都可做爲非操做符,如字母、數字等;設計
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個屬性構成:一、stateNum表示當前狀態節點的狀態標誌;二、pathChar表示由前一個狀態轉換到當前狀態所需的字符;
三、ArrayList<NFANode> nextNodes表示與當前狀態後繼相連的全部狀態節點集合;
③符號棧中存儲的元素爲char類型的currentSymbol表示當前符號棧中存儲的運算符;
如下爲該數據結構的代碼:
一、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>(); } }
二、NFA:
//定義存儲在NFA棧中的NFA結構:頭結點和尾結點 public class NFA { public NFANode headNode; public NFANode tailNode; public NFA(NFANode headNode,NFANode tailNode){ this.headNode = headNode; this.tailNode = tailNode; } }
三、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; } }
四、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(); }
3、由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; }
4、DFA的最小化
從步驟三中咱們已經獲得了DFA中的狀態轉換集合,每一個狀態轉換包含起始狀態、轉換字符和終止狀態。然而有些DFA中的狀態是無效的,有些DFA中的狀態是重複的,所以須要對DFA中的這狀態進行最小化操做。最小化操做須要通過兩步:一、消除無用狀態;二、合併等價狀態;
一、消除無用狀態:
什麼是無用狀態?無用狀態即爲從該自動機的開始狀態出發,任何輸入串也不能到達的那個狀態,或者這個狀態沒有通路到達終態,這樣的狀態即稱爲無用狀態。消除無用狀態的算法我是這麼設計的:從初始狀態出發,遍歷各類字符,將從初始狀態能到達的狀態放入一個集合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; }
二、合併等價狀態:
定義兩個狀態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; }
5、程序測試:
①輸入: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
歡迎指正,轉載請註明出處,謝謝~