词法分析程序之正规式转换成NFA

点赞再看,养成习惯!觉得不过瘾的童鞋,欢迎关注公众号《机器学习算法工程师》,有非常多大神的干货文章可供学习噢…

前言

PS: 鉴于本文对很多童鞋有所帮助,小编于2020/6/7对其回炉重造,在CodeBlocks IDE(采用GCC套装 编译器)中成功编译运行,代码已上传至Github上,童鞋们可在参考文献中找到链接。

本篇是凭借Thompson方法,从而根据正规表达式(正规式)构造对应的NFA(不确定有穷自动机),其实现语言是C++。基本逻辑如下图所示:
这里写图片描述

正文

正规式

本篇中,有效的正规式的字母表 ∑ = { a − z , A − Z } \sum=\{a-z,A-Z\} ={az,AZ},辅助字母表 ∑ ′ = { ∗ , ∣ , ( , ) } \sum'=\{*,|,(,)\} ={,,(,)}。对应的规则:

  1. ‘*’:闭包;
  2. ‘|’:或;

其实还有连接符号,不过我们平时写正规式时会省略它,但小编在源码中其实加上了就是’+’,参考文献的那个课本用的是’.’。这些算符的优先顺序为先’*’,再’+’,最后’|’。这个优先关系在中缀表达式转后缀表达式的时候会用到。

非法字符与加入连接符

非法字符
评论中曾有童鞋问小编非法字符指的那些,当时文章写得并不清晰。这次,小编将从源码角度解释,非法字符指的便是除字母表与辅助字母表以外的字符,并且源码额外还对括号进行检查。

/**检测输入的正则表达式是否合法
*/
int check_legal(const string check_string)
{
	if (check_character(check_string) && check_parenthesis(check_string))
	{
		return true;
	}
	return false;
}

/**
检查输入的字符是否合适 () * | a~z A~Z
合法返回true,非法返回false
*/
int check_character(const string check_string)
/**检查括号是否匹配
*合法返回true,非法返回false
*/
int check_parenthesis(const string check_string)

加入连接符
这一步主要是为了方便后面的工作(中缀转后缀),将省略的连接符显式加进去。要加的地方如ab 、 *b 、 a( 、 )b 等情况,一次遍历即可完成。

中缀表达式转后缀表达式

这一节的算法,小编特地写了一篇博文帮助理解,童鞋可在参考文献中找到。其实在源码中这块有比较详细的注释,这里要利用上前面说的算符优先关系。

构造NFA

一个NFA M是一个五元组, M = ( K , ∑ , f , S , Z ) M=(K,\sum,f,S,Z) M=(K,,f,S,Z)。我们目前知道的只有 ∑ \sum 有穷字母表,即是正规式的字母表,而其他的像 K K K状态集, f f f转移函数, S S S非空初态集, Z Z Z终态集,就在这一节能够得到。源码实现的方法便是Thompson方法,在参考文献《编译原理》书中第4章能够找到与下面内容一样的解释,不再赘述。
下面的理论光看文字描述可能难以快速理解,小编打算以一个demo并辅助源码来细致讲解,希望能达到过一遍就能上手源码的效果!
Demo
正规式为a(a|b)*,很明显这个简单的正规式包含了所有的辅助字母表的符号(即所有的规则),和字母表里的字母a和b,不过我们不要简单地把它们看成字母a和b,实际上它们可以抽象为两个不同的符号,他们所代表的可以是一类符号,比如是数字或者是英文字母,这取决于我们在源码中把这些符号识别成什么。
首先,介绍一下源码中用到的数据结构:

//NFA的节点,定义成结构体,便于以后扩展
struct state
{
	string StateName;
};

//NFA的边,空转换符用'#'表示
struct edge
{
	state StartState;  
	state EndState;
	char TransSymbol;  //转换符号
};

//NFA单元,一个大的NFA单元可以是由很多小单元通过规则拼接起来
struct cell
{
	edge EdgeSet[MAX];  //这个NFA拥有的边
	int EdgeCount;  //边数
	state StartState;  //开始状态
	state EndState;  //结束状态
};

Thompson原理
方法:将r分解成最基本的子表达式,使用下面的规则1和2为r的每个基本符号( # 或Σ中的符号)构造NFA。用规则3逐步组合前面构造的NFA,直到获得整个正规表达式的NFA为止。
规则1. 对#, 构造NFA 。
在这里插入图片描述

这里 i 是新的开始状态,f是新的接受状态。很明显这个NFA识别{#}。
规则2. 对于Σ中的每个符号a,构造NFA。
这里写图片描述
同样,i 是新的开始状态, f 是新的接受状态。 这个NFA识别 {a}。
【小编讲解】
规则1和2是NFA构建的基本单元,含有两个状态以及状态转换的边。在源码中实现是如下面代码所示的,会新增两个状态与一条边,状态从new_StateNode函数生成,能够保证全局唯一性;而边则记录转换的符号以及起始状态信息。而最终返回的便是一个包含了新状态节点与边的NFA单元NewCell。

//处理 a
cell do_Cell(char element)
{
	cell NewCell;
	NewCell.EdgeCount = 0;
	edge NewEdge;
	//获得新的新状态节点
	state StartState = new_StateNode();
	state EndState = new_StateNode();
	//构建边
	NewEdge.StartState = StartState;
	NewEdge.EndState = EndState;
	NewEdge.TransSymbol = element;
	//构建单元
	NewCell.EdgeSet[NewCell.EdgeCount++] = NewEdge;
	NewCell.StartState = NewCell.EdgeSet[0].StartState;
	NewCell.EndState = NewCell.EdgeSet[0].EndState;//EdgeCount此时为1
	return NewCell;
}

规则3 . 如果N(s) 和 N(t) 是正规表达式s和t的NFA,则:
①对于正规表达式 s | t, 可构造复合的NFA N(s|t):
在这里插入图片描述
【小编讲解】
在规则1和2构建好基本的NFA单元后,遇到规则①,我们只需要增添2个状态和4条边,并按照要求连接起来即可,实现如下:

//处理 a|b
cell do_Unite(cell Left, cell Right)
{
	cell NewCell;
	NewCell.EdgeCount = 0;
	edge Edge1, Edge2, Edge3, Edge4;
	//获得新的新状态节点
	state StartState = new_StateNode();  //s
	state EndState = new_StateNode();  //e
	//构建边
	//e1
	Edge1.StartState = StartState;
	Edge1.EndState = Left.EdgeSet[0].StartState;
	Edge1.TransSymbol = '#';  //代表空串
	//e2
	Edge2.StartState = StartState;
	Edge2.EndState = Right.EdgeSet[0].StartState;
	Edge2.TransSymbol = '#';
	//e3
	Edge3.StartState = Left.EdgeSet[Left.EdgeCount - 1].EndState;
	Edge3.EndState = EndState;
	Edge3.TransSymbol = '#';
	//e4
	Edge4.StartState = Right.EdgeSet[Right.EdgeCount - 1].EndState;
	Edge4.EndState = EndState;
	Edge4.TransSymbol = '#';

	//构建单元
	//先将Left和Right的EdgeSet复制到NewCell
	cell_EdgeSet_Copy(NewCell, Left);
	cell_EdgeSet_Copy(NewCell, Right);

	//将新构建的四条边加入EdgeSet
	NewCell.EdgeSet[NewCell.EdgeCount++] = Edge1;
	NewCell.EdgeSet[NewCell.EdgeCount++] = Edge2;
	NewCell.EdgeSet[NewCell.EdgeCount++] = Edge3;
	NewCell.EdgeSet[NewCell.EdgeCount++] = Edge4;

	//构建NewCell的起始状态和结束状态
	NewCell.StartState = StartState;
	NewCell.EndState = EndState;

	return NewCell;
}

②对于正规表达式 st, 可构造复合的NFA N(st):
在这里插入图片描述
【小编讲解】
规则②并不需要新增边和状态,而是在已有的NFA单元上修改,把N(s)的终止状态与N(t)的起始状态合并,源码实现中是保留了N(s)的终止节点而删掉N(t)的起始节点,然后把N(t)与起始节点有关的边都要修改,代码如下:

//处理 ab
cell do_Join(cell Left, cell Right)
{
	//将Left的结束状态和Right的开始状态合并,将Right的边复制给Left,将Left返回
	//将Right中所有以StartState开头的边全部修改
	for (int i = 0; i<Right.EdgeCount; i++)
	{
		if (Right.EdgeSet[i].StartState.StateName.compare(Right.StartState.StateName) == 0)
		{
			Right.EdgeSet[i].StartState = Left.EndState;  //该边e1的开始状态就是N(t)的起始状态
			STATE_NUM--;
		}
		else if (Right.EdgeSet[i].EndState.StateName.compare(Right.StartState.StateName) == 0)
		{
			Right.EdgeSet[i].EndState = Left.EndState;  //该边e2的结束状态就是N(t)的起始状态
			STATE_NUM--;
		}
	}
	Right.StartState = Left.EndState;

	cell_EdgeSet_Copy(Left, Right);
	//将Left的结束状态更新为Right的结束状态
	Left.EndState = Right.EndState;
	return Left;
}

③对于正规表达式 s, 构造复合的NFA N(s)😗*
在这里插入图片描述
【小编讲解】
规则③要新增4条边与两个状态,图示与代码可以一一对照。

//处理 a*
cell do_Star(cell Cell)
{
	cell NewCell;
	NewCell.EdgeCount = 0;
	edge Edge1, Edge2, Edge3, Edge4;
	//获得新的新状态节点
	state StartState = new_StateNode();  //s
	state EndState = new_StateNode();  //e
	//构建边
	//e1
	Edge1.StartState = StartState;
	Edge1.EndState = EndState;
	Edge1.TransSymbol = '#';  //闭包取空串
	//e2
	Edge2.StartState = Cell.EndState;
	Edge2.EndState = Cell.StartState;
	Edge2.TransSymbol = '#';  //取字符,自连接
	//e3
	Edge3.StartState = StartState;
	Edge3.EndState = Cell.StartState;
	Edge3.TransSymbol = '#';
	//e4
	Edge4.StartState = Cell.EndState;
	Edge4.EndState = EndState;
	Edge4.TransSymbol = '#';
	//构建单元
	//先将Cell的EdgeSet复制到NewCell
	cell_EdgeSet_Copy(NewCell, Cell);
	//将新构建的四条边加入EdgeSet
	NewCell.EdgeSet[NewCell.EdgeCount++] = Edge1;
	NewCell.EdgeSet[NewCell.EdgeCount++] = Edge2;
	NewCell.EdgeSet[NewCell.EdgeCount++] = Edge3;
	NewCell.EdgeSet[NewCell.EdgeCount++] = Edge4;
	//构建NewCell的启示状态和结束状态
	NewCell.StartState = StartState;
	NewCell.EndState = EndState;

	return NewCell;
}

④对于括起来的正规表达式 (s), 使用N (s) 本身作为它的NFA。
在构造过程中,每次构造的新状态都要赋予不同的名字。这样,任何NFA的两个状态都具有不同的名字。即使同一符号在r中出现多次,我们也要为该符号的每个实例创建一个独立的带有自己状态的NFA。

测试案例

两个正规式:

  1. a(a|b)*也是构造NFA一节中的demo
  2. bb*更简单的例子

Console控制台程序结果图
这里写图片描述
这里写图片描述

对应的NFA图示
这里写图片描述
这里写图片描述

结语

编译原理的相关算法难在理论,这是小编再本科二年级的项目,距今过去了4年,其实没怎么用到过相关知识点,不过要深入一门编程语言的编译器源码看看,那这些知识还是必不可少的。另外,我们常用的正则表达式底层用到的便是这个算法,我们构造好NFA,又该如何使用它,这都是值得进一步探讨的问题。

参考文献

  1. https://github.com/Ggmatch/The-principle-of-compiling/tree/master/NFA
  2. 张素琴. 《编译原理》第二版
  3. 中缀转后缀:https://blog.csdn.net/gongsai20141004277/article/details/106607743

童鞋们,让小编听见你们的声音,点赞评论,一起加油。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 1024 设计师:白松林 返回首页