形式语言与自动机|DFA识别句子

实验二 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类 与 状态结点类、窗口类各自执行自己的功能,数据和视图分离。

posted @ 2019-12-08 19:40  fishers  阅读(3610)  评论(9编辑  收藏  举报