有限自动机分为确定有限自动机(DFA)和不确定有限自动机(NFA),这里介绍DFA,即确定有限自动机。
1. DFA的形式定义
从形式上说,一个有限状态自动机可以用下面的5个参数来定义:
- Q: 状态q0, q1, ... , qN的有限集合
- Σ: 有限的输入符号字母表
- q0: 初始状态
- F: 终极状态的集合, F∈Q
- δ(q, i): 状态之间的转移函数或转移矩阵。给定一个状态q∈Q和一个输入符号i∈Σ, δ(q, i)返回一个新的状态q'∈Q。
2.DFA的java实现
这里定义了一个抽象父类FA,DFA和NFA继承了FA类。
- <span style="font-size:12px;">public abstract class FA {
- protected List<FAState> acceptingStates; //可接收状态(结束状态)
- protected List<FAState> states; //所有状态
- protected List<String> alphabet; //符号字母表
- //状态转移矩阵(使用邻接链表表示)
- protected List<List<TransitMatElement>> stateTransitionMat;
- //....
- }</span>
- <span style="font-size:12px;">public class DFA extends FA {
- protected FAState startState; //开始状态
- }</span>
用于唯一区分一个FAState。
构造DFA对象时,需要传入一个特定格式的文本文件的路径。
文件的格式,举例如下:
5 3
1 2 3 4 5
b a !
1
5
1 -1 -1
-1 2 -1
-1 3 -1
-1 3 4
-1 -1 -1
其中,第一行的两个数字分别表示自动机状态的数目和符号字母表中符号的数目
第二行的数字为每一个状态的id
第三行为符号串(符号串间以空格隔开)
第四行的数字为开始状态的id
第五行的数字为结束状态的id
最后几行为状态转换矩阵(其中数字表示跳转到的状态在状态列表中的下标,-1表示一个状态不能遇到相应的符号)
上面的例子表示的DFA对象如下图所示:
3. 使用DFA识别符号串
DFA构造好了之后就可以用来识别符号串。这里引用了NFA/DFA算法 - 于公摊的杂货铺中的一个例子:
如何通过设计状态及其转移方法来实现一个分析器呢?当然,如果一个字符串仅仅包含a或者b的话,那么分析器的状态只有四种:
“奇数a奇数b”、“奇数a偶数 b”、“偶数a奇数b”、“偶数a偶数 b”。我们把这些状态依次命名为aa、aB、Ab、AB。大写代表偶数,
小写代表奇数。当工作还没开始的时候,分析器已经读入的字符串是空串,那么理所当然的起始状态应当是AB。当分析器读完所有
字符的时候,我们期望读入的字符串的a和b的数量都是偶数,那么结束的状态也应该是AB。于是我们给出这样的一个状态图:
图3.1 检查一个字符串是否由偶数个a和偶数个b组成的状态图
在这个状态图里,有一个短小的箭头指向了AB,代表AB这个状态是初始状态。AB状态有粗的边缘,代表AB这个状态
是结束的可接受状态。 一个状态图的结束状态可以是一个或者多个。在这个例子里,起始状态和结束状态刚好是同一个状态。
标有字符”a”的箭头从AB指向aB, 代表如果分析器处于状态AB并且读入的字符是a的话,就转移到状态aB上。我们把这个状态图
应用在两个字符串上,分别是”abaabbba”和”aababbaba”。
其中,第一个字符串是可以接受的,第二个字符串是不可接受的(因为有5个a和4个b)。
分析第一个字符串的时候,状态机所经过的状态是:
AB[a]aB[b]ab[a]Ab[a]ab[b]aB[b]ab[b]aB[a]AB
分析第二个字符串的时候,状态机所经过的状态是:
AB[a]aB[a]AB[b]Ab[a]ab[b]aB[b]ab[a]Ab[b]AB[a]aB
第一个字符串”abaabbba”让状态机在状态AB上停了下来,于是这个字符串是可以接受的。第二个字符串”aababbaba”让状态机
在状态aB上停了下来 ,于是这个字符串是不可以接受的。
DFA识别符号串的核心算法实现如下:
- <span style="font-size:12px;"> /**
- * 使用自动机识别符号串
- * @param words 待匹配符号串
- * @return 如果接受,则返回true,否则,返回false
- */
- public boolean recognize(String[] words) {
- FAState currentState = this.startState;
- int countOfWordsRecognized = 0;
- while(countOfWordsRecognized <= words.length) {
- if(isAccepted(currentState, countOfWordsRecognized, words.length)) { //接收
- return true;
- } else if(wordsTerminatedButNotAccepted(currentState, words.length,
- countOfWordsRecognized)) {
- return false;
- }
- // 当前待识别的单词在alphabet中的下标
- int indexOfAlpha = alphabet.indexOf(words[countOfWordsRecognized]);
- //查找状态转移矩阵,获取将要跳转到的状态的下标
- int transition =
- getIndexOfStateToSwitch(states.indexOf(currentState), indexOfAlpha);
- if(transition == -1) { //不能匹配当前符号串,拒绝
- return false;
- } else {
- currentState = this.states.get(transition); //进行下一个符号串的接收
- countOfWordsRecognized++;
- }
- }
- return false;
- }</span>
4.DFA最小化
4.1 最小状态DFA的含义
1.没有多余状态(死状态)
2. 没有两个状态是互相等价(不可区别)
4.2 两个状态s和t等价的条件
兼容性(一致性)条件——同是终态或同是非终态
传播性(蔓延性)条件——从s出发读入某个a和从t出发经过某个a并且经过某个b到达的状态等价。
4.3 举例说明
下面就以如下图所示的DFA对象为例说明最小化DFA的操作
DFA的最小化
1. 将状态列表S中的状态分为两个子集:终结状态s1(也叫可接受状态)和非终结状态s2
对于上图中的DFA, s1 = {4, 5, 6, 7}, s2 = {1, 2, 3}
2. 查看s2 = {1, 2, 3}是否可以继续划分
状态2遇到a转向s1中的状态4,不在s2中,于是将s2继续划分,分为{1, 3}和{2}这两个集合。
继续查看{1, 3}是否可以继续划分。由于3遇到b转向s1中的状态5,于是将{1, 3}继续划分,分为{1}和{3}这两个集合。
3. 查看 s1= {4, 5, 6, 7}是否可以继续划分
需要注意的是,当最终所以的子集不可再分时,如果一个子集内的状态不止一个,则由于同一子集内的状态等价,
同一子集内的节点之间的状态跳转可以不用考虑。但是如果这个子集内的某一状态遇到一个符号转向本身时,
这种向自身的转移要体现在新的替换的状态上。例如4,5,6,7最终属于同一子集,最终用节点4来替换这4个节点时,
需要在状态转移矩阵中加上遇到a跳转到节点4,与遇到b跳转到节点4的情况。
最终得到的最小化DFA如下图所示:
4.4 具体实现
DFA最小化的核心算法如下:
- /**
- * 最小化当前DFA对象
- */
- public void simplify() {
- //用于存放最小化的过程中产生的状态划分
- List<List<FAState>> stateLists = new ArrayList<List<FAState>>();
- // phrase 1: 将化简前的DFA的状态分为非可接受状态和可接受状态两部分
- List<FAState> nonTerminalStates = new ArrayList<FAState>();
- List<FAState> copyOfOriginalState = deepCloneOfFAStateList(this.states);
- for(FAState state : copyOfOriginalState) {
- if(!this.acceptingStates.contains(state)) {
- nonTerminalStates.add(state);
- }
- }
- List<FAState> terminalStates = deepCloneOfFAStateList(this.acceptingStates);
- stateLists.add(nonTerminalStates);
- stateLists.add(terminalStates);
- // phrase 2: 看nonTerminalStates能否再分,如果可以,则进行划分
- splitStateListIfCould(stateLists, nonTerminalStates);
- // phrase 3: 看terminalStates能否再分,如果可以,则进行划分
- int leftMostEndStateIndex = splitStateListIfCould(stateLists, terminalStates);
- // phrase 4: 根据存储状态列表的列表的每一个元素作为一个状态,构造最小化DFA
- rebuildDFAWithSimplifiedStateList(stateLists, leftMostEndStateIndex);
- }