C++实现正则表达式转最小化DFA过程详解

目前学校正在开编译原理这门课。为了加深对正则表达式转最小化DFA的理解,利用c++编写了这个程序。现在把我的程序及实现的方法分享出来,希望能对大家的学习有所帮助。

完整代码:https://github.com/GgBondXiang/RexToMinDFA

正则表达式

操作数

本程序的支持的操作数为小写字母‘a’~‘z’

运算符

正则表达式包含三个运算符

1)或运算符“|”

2)连接运算符“.”,一般省略不写,本程序中用“&”代替

3)闭包运算符“*”,即任意有限次的自重复连接

规定算符的优先顺序为先“*”,再“.”,最后“|”。

算法流程

1.正则表达式的预处理

预处理是把表达式中省略的连接运算符加上,方便计算机运算

需要添加“&”的有六种情况,分别为“a a” 、“a (” 、“* a” 、“* (” 、“) a” 、 “) (”
总结起来为当第一位是操作数、“*”、“)”且第二位为操作数或“(”

以表达式"(a|b)* abb"为例,预处理后的表达式为:“(a|b)* &a&b&b”

2.中缀表达式转后缀表达式

运算符的优先级为 闭包‘*’ > 连接‘&’ > 或‘|’

转换过程中需要用到一个运算符栈,具体过程如下:

  • 如果遇到操作数,直接将其输出。
  • 如果遇到运算符:
    • 遇到‘(’直接压入栈中;
    • 遇到‘)’将运算符出栈并输出,直到遇到‘(’,将‘(’出栈但不输出;
    • 遇到‘*’、‘&’、‘|’运算符:
      • 如果栈为空,直接将运算符压入栈中;
      • 如果栈不为空,弹出栈中优先级大于等于当前运算符的运算符并输出,再将当前运算符入栈。
  • 当输入串读取完之后,如果栈不为空,则将栈中元素依次出栈并输出。
/*
*本程序中比较优先级的方法是为每一个运算符返回一个权值,通过权值的大小来比较优先级。
*但是实际操作中会遇到bug,这是因为程序无法返回左括号的优先级。
*为了保证弹出优先级大于等于当前运算符的运算符时,左括号不会出栈,将左括号的权值设为0
*/
int priority(char ch)
{

	if(ch == '*')
	{
		return 3;
	}
		
	if(ch == '&')
	{
		return 2;
	}
		
	if(ch == '|')
	{
		return 1;
	}
	
	if(ch == '(')
	{
		return 0;
	}
}

下面看这个例子“(a|b)* &a&b&b”

1.遇到“(”,直接入栈
栈:(
输出区:
2.遇到“a”,直接输出
栈:(
输出区:a
3.遇到“|”,栈中没有优先级大于等于它的运算符,直接压入栈中
栈:( |
输出区:a
4.遇到“b”,直接输出
栈:( |
输出区:a b
5.遇到“)”,“|”出栈并输出,左括号出栈
栈:
输出区:a b |
6.遇到“*”,栈为空,直接入栈
栈:*
输出区:a b |
7.遇到“&”,“*”出栈并输出,再将“&”入栈
栈:&
输出区:a b | *
8.遇到“a”,直接输出
栈:&
输出区:a b | * a
9.遇到“&”,“&”出栈并输出,再将“&”入栈
栈:&
输出区:a b | * a &
10.遇到“b”,直接输出
栈:&
输出区:a b | * a & b
11.遇到“&”,“&”出栈并输出,再将“&”入栈
栈:&
输出区:a b | * a & b &
12.遇到“b”,直接输出
栈:&
输出区:a b | * a & b & b
13.字符串读完,栈不为空,将栈中元素出栈并输出
栈:
输出区:a b | * a & b & b &

所以转换完的后缀表达式为“ab|* a&b&b&”

3.后缀表达式创建NFA

首先介绍一下NFA的存储结构,下图为一个NfaState
NfaState

struct NfaState				/*定义NFA状态*/
{
	
	int index;				/*NFA状态的状态号*/ 
	
	char input;				/*NFA状态弧上的值,默认为“#”*/
	int chTrans;			/*NFA状态弧转移到的状态号,默认为-1*/ 
	
	IntSet epTrans;			/*当前状态通过ε转移到的状态号集合*/ 
};

struct NFA
{
	
	NfaState *head;			/*NFA的头指针*/
	NfaState *tail;			/*NFA的尾指针*/
};

程序中定义了一个NfaState类型的数组NfaStates和一个int类型的全局变量nfaStateNum,初值为0,每次需要创建一个NFA时就通过NfaStates[nfaStateNum]和NfaStates[nfaStateNum + 1]从数组中取出两个状态,nfaStateNum加2,再更新NFA的头尾指针即可。

转换过程中要用到一个NFA栈,具体流程如下:

  • 按顺序读取后缀表达式,每次读取一个字符
    • 如果遇到操作数a,则新建一个NFA,转换弧上的值为a,将这个NFA压入栈中
      在这里插入图片描述

    • 如果遇到闭包运算符“*”,则新建一个NFA n,从NFA栈中弹出一个元素 n1,连接关系如下,将NFA n压入栈中
      在这里插入图片描述

    • 如果遇到或运算符“|”,则新建一个NFA n,并在NFA栈中弹出两个元素n1,n2,连接关系如下,将NFA n压入栈中
      在这里插入图片描述

    • 如果遇到连接运算符“&”,不用新建NFA,只需在NFA栈中弹出两个元素n1,n2,改变n1 n2的头尾指针,连接关系如下,最后将n压入栈中
      在这里插入图片描述

后缀表达式“ab|* a&b&b&”具体转换过程如下
  1. 遇到“a”,新建一个NFA n,压入栈中
    在这里插入图片描述
  1. 遇到“b”,新建一个NFA n,压入栈中
    在这里插入图片描述
  1. 遇到“|”,新建一个NFA n,从栈中弹出两个NFA,改变连接关系,再将n压入栈中
    在这里插入图片描述
  1. 遇到“*”,新建一个NFA n,从栈中弹出一个NFA,改变连接关系,再将n压入栈中
    在这里插入图片描述
  1. 遇到“a”,新建一个NFA n,压入栈中
    在这里插入图片描述
  1. 遇到“&”, 新建一个NFA n,从栈中弹出两个NFA,改变连接关系,再将n压入栈中
    在这里插入图片描述
7.遇到“b”,新建一个NFA n,压入栈中
在这里插入图片描述
8.遇到“&”, 新建一个NFA n,从栈中弹出两个NFA,改变连接关系,再将n压入栈中
在这里插入图片描述
9.遇到“b”,新建一个NFA n,压入栈中
在这里插入图片描述
10.遇到“&”, 新建一个NFA n,从栈中弹出两个NFA,改变连接关系,再将n压入栈中
在这里插入图片描述
后缀表达式读取结束,最后NFA栈中的唯一元素即为创建好的NFA

4.NFA转化为DFA

struct Edge			/*定义DFA的转换弧*/
{
	
	char input;			/*弧上的值*/
	int Trans;			/*弧所指向的状态号*/
};

DFA存储结构如下,下图为一个DfaState
在这里插入图片描述

struct DfaState		/*定义DFA状态*/
{
	
	bool isEnd;			/*是否为终态,是为true,不是为false*/
	
	int index;			/*DFA状态的状态号*/
	IntSet closure;		/*NFA的ε-move()闭包*/
	
	int edgeNum;		/*DFA状态上的射出弧数*/
	Edge Edges[10];		/*DFA状态上的射出弧*/
};

struct DFA			/*定义DFA结构*/
{
	
	int startState;				/*DFA的初态*/
	
	set<int> endStates;			/*DFA的终态集*/
	set<char> terminator;		/*DFA的终结符集*/
	
	int trans[MAX][26];		/*DFA的转移矩阵*/
};

算法过程如下:

set s;   		//用于判断集合是否出现过。如果没有出现,则需新建一个DFA状态
queue q;

dfa状态总数 = 0;			//dfa状态总数

T0 = ε-closure(0);		//计算ε-closure(0),令T0 = ε-closure(0)
s.insert(T0);			//将子集T0加入子集族s中
q.push(T0);				//将子集T0入队列

为T0新建一个dfa状态;
dfa状态总数++;			//dfa状态总数加一

while(!q.empty())
{
	
	T = q.front();		//出队列
	q.pop();
	
	for ch in 终结符集	//对每个终结符进行ε-closure(move(T, ch))运算
	{

		temp_set = ε-closure(move(T, ch));
		
		if(!temp_set.empty())		//如果子集不为空
		{
			if(!s.count(temp_set)//如果子集不在s中
			{
		
				为temp_set新建一个dfa状态;
			
				s.insert(temp_set);	//将子集temp_set加入子集族s中
				q.push(temp_set);	//将子集temp_set入队列
	
				dfa状态总数++;
			}
			else    //该状态在子集族s中,假设标号为T_temp
			{
			
				为当前状态T新建一条弧;
				
				//弧上的值为当前终结符
				弧上的值 = ch;	
				//弧转移到的状态为标T_temp	
				弧转移到的状态 = temp;		
			}
		}
	}
}

以“ab|* a&b&b&”为例:

子集ab
T0 {0 2 4 6 7 8}{0 1 2 4 5 6 7 8 9 10} T1{0 2 3 4 5 6 7 8} T2
T1 {0 1 2 4 5 6 7 8 9 10}{0 1 2 4 5 6 7 8 9 10} T1{0 2 3 4 5 6 7 8 11 12} T3
T2 {0 2 3 4 5 6 7 8}{0 1 2 4 5 6 7 8 9 10} T1{0 2 3 4 5 6 7 8} T2
T3 {0 2 3 4 5 6 7 8 11 12}{0 1 2 4 5 6 7 8 9 10} T1{0 2 3 4 5 6 7 8 13} T4
T4 {0 2 3 4 5 6 7 8 13}{0 1 2 4 5 6 7 8 9 10} T1{0 2 3 4 5 6 7 8} T2

至此,子集族中不存在新的子集,为每个子集创建dfa状态后,dfa则构造完成。

5.DFA的最小化

struct stateSet			/*划分状态集*/
{
	
	int index;			/*该状态集转换到的状态集标号*/  
	IntSet s;			/*该状态集中的dfa状态号*/
};

stateSet temp[128];

定义一个缓冲区,用来存储划分集合中的元素和该集合转移到的集合号。
如果某个DFA状态没有与某个终结符对应的弧,规定此类DFA状态转移到的集合号为-1。

判断当前划分集合是否需要进行划分的依据为缓冲区中的元素个数
如果个数为1,表示当前划分集合中的所有元素都转移到同一个集合中,则不需要划分
反之,如果个数大于1,表示当前划分集合中的元素转移到不同集合中,则需要划分

例如当前划分集合为

划分集合划分集合中的元素
PartSet[0]{0 2 4 6 7}
PartSet[1]{1 3 5}

假设划分集合 PartSet[0]:{0 2 4 6 7}对于终结符“a”,有这样一个缓冲区:

缓冲区转移到的集合号划分集合中的元素
temp[0]1{0 6}
temp[1]-1{2}
temp[2]0{4 7}
该缓冲区表示
temp[0]:DFA状态0、6都能转移到划分集合 PartSet[1]中
temp[1]:DFA状态2没有与“a”对应的转换弧
temp[2]:DFA状态4、7都能转移到划分集合 PartSet[0]中

因为缓冲区的元素个数大于1,即PartSet[0]中的元素能转移到不同集合中,所以应对PartSet[0]进行划分。

算法过程如下:

set PartSet[128];		//用于存储所有的划分集合

//将终态和非终态划分开
for state in DfaStates		//遍历DFA状态数组
{
	if(state.isEnd == true)		//如果该DFA状态是终态
	{
		PartSet[0].insert(state);			//加入到划分集合[0]中
	}
	else						//如果不是终态
	{
		PartSet[1].insert(state);			//加入到划分集合[1]中
	}
}

//实际实现过程中为了遍历划分集合还应判断DFA中是否包含非终态。
//如果有则第一次划分后划分集合个数为2,如果没有则为1。

bool flag = true;		//上次产生新的划分则为true,反之为false
while(flag)		//一直循环,直到上次没有产生新的划分
{
	for set in PartSet	//对每个划分集合set
	{
			
		int cutCount = 0;	//划分次数
		
		for ch in 终结符集	//遍历每个终结符
		{
			for state in set		//遍历集合set中的每个DFA状态state
			{
				
				//判断该DFA状态是否存在与该终结符对应的弧
				bool haveEdge = false;	
				
				for edge in sate.Edge	//遍历DFA状态sate的每条边edge
				{
					//如果存在某条边的输入为当前终结符
					if(set.state.edge.input == ch)
					{
						//找到该弧转换到的状态所属的划分集合号
						setNum = findSet(set.state.edge.Trans);
						将该state加入到缓冲区中能转换到setNum的状态集合中;
						
						haveEdge = true;
						break;	
					}
				}
		
				if(!haveEdge)
				{
					将该state加入到缓冲区中能转换到-1的状态集合中;
				}
			}
		}

		if(缓冲区中元素个数 > 1)		//缓冲区中元素个数大于1则需要划分
		{
			
			cutCount++;		//划分次数+1
			
			//这里是从1开始,因为要将temp[0]中的元素保留在原划分集合中
			for(i = 1; i < temp的元素个数; i++)
			{
				在原划分集合set中删除temp[i]中的元素;
				为temp[i]创建新的划分集合;
			}
		}
	}
	
	if(cutCount > 0)	//划分次数大于0说明本次产生了新的划分
	{
		flag = true;
	}
}

for set in PartSet
{
	为set创建一个DfaState;
}

还是之前的例子:
在进行第一次划分,即将终态与非终态分开后

划分集合划分集合中的元素
PartSet[0]{4}
PartSet[1]{0 1 2 3}

PartSet[1]对终结符“a”进行划分

缓冲区转移到的集合号划分集合中的元素
temp[0]1{0 1 2 3}

PartSet[1]对终结符“b”进行划分

缓冲区转移到的集合号划分集合中的元素
temp[0]1{0 1 2}
temp[1]0{3}

缓冲区中的个数大于1,需要进行划分。划分后的集合为:

划分集合划分集合中的元素
PartSet[0]{4}
PartSet[1]{0 1 2}
PartSet[2]{3}

PartSet[2]对终结符“b”进行划分

缓冲区转移到的集合号划分集合中的元素
temp[0]1{0 2}
temp[1]2{1}

缓冲区中的个数大于1,需要进行划分。划分后的集合为:

划分集合划分集合中的元素
PartSet[0]{4}
PartSet[1]{0 2}
PartSet[2]{3}
PartSet[3]{1}

对每个划分集合进行判断,发现不再需要进行新的划分,
则为每个划分集合创建一个DFA状态,DFA最小化完成。

  • 65
    点赞
  • 255
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
编译原理是学习计算机科学的一门基础课程,主要涉及语言的识别和化,而正则表达式则是其中一个非常重要的工具。在编译原理中,正则表达式通常用于描述一些模式,比如关键字、标识符等。因此,掌握正则表达式过程对于理解编译原理课程非常重要。 正则表达式过程主要包括以下几个部分:正则表达式NFA、NFADFADFA最小化。其中,NFA(非确定有限状态自动机)和DFA(确定有限状态自动机)都是描述正则表达式的模型。 正则表达式NFA: 首先,正则表达式中的基本元素是字符、括号和运算符。在换为NFA的过程中,需要设计出一些状态来描述不同的字符和运算符。 对于字符来说,我们可以为它们设计出一个状态,状态的入口边是字符,出口边为空。 对于括号和运算符来说,可以为它们设计出一些连接状态。例如在括号中的字符可以通过连接状态直接连接到后面的状态,或者通过其他运算符先连接到其他的状态再连接到后面的状态。 最后,需要定义一个起始状态和一个终止状态,起始状态与第一个字符状态相连,最后一个字符状态与终止状态相连。这样,我们就得到了一张NFA图。 NFADFA: 将一个NFA图换成DFA图的主要目的是为了简化图结构,以便后续对文本进行识别。 首先,需要定义DFA的状态集合,每个集合都对应一个状态。因为DFA是完全确定的有限状态自动机,所以在DFA中只能有一个状态。 然后,需要将NFA图中的每个状态都映射为DFA图中的一个状态,以便对文本进行识别。当NFA图中有多个状态对应于DFA图中的同一状态时,需要将它们合并,并将它们的出口边合并成一个出口边。 DFA最小化: 最后,对DFA进行最小化处理,以便减少状态数,提高运行效率。在最小化处理时需要考虑不同状态之间的等价关系。 可以采用遍历算法,将DFA中的状态按照等价关系划分为若干个等价类,然后构造一个等价类访问表,每个表项对应一个状态集。 最小化后的DFA图是可以识别文本的,可以用于在编译器中进行文本匹配和词法分析等操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值