前言
编译原理课里面书本有一个作业——使用C++实现:
- 将正规式变成NFA
- NFA确定化(变成DFA)
- DFA最小化(化简)
自己学习过程中,深刻知道自己动手写东西是多么的重要。所以,我开始从0开始写这个作业。
每个自学的人都要明白自己和科班的人差距在哪里——基础!基础!所以一定要花时间去补回来。
正规式变成NFA
前备知识
在动手之前,一定学会分析要怎么做怎么做,有哪几个类,不要上去就是一段代码——参考某大佬的话。
(1)第一步,我们需要分析程序的流程是什么——画一个简单的思维导图:
NFA——是一个五元组:M = ( K , ∑ , f , S , Z ) ,∑有穷字母表,即是正规式的字母表,而其他的像K 状态集,f 转移函数,S 非空初态集,Z 终态集。
接下来我们可以使用Thompson方法实现正规式转NFA。
(2)NFA如何表示——需要什么的数据结构?
我们知道MFA是有节点信息、带信息的边的有向图。所以我们可以设计这样的数据结构
-
节点
struct NodeState{ string StateName;//节点状态 };
-
边:(有向边)
struct Edge{ NodeState startNode;//开始节点状态 NodeState endNOde;//终止节点状态 char TransSymbol;//边上的转化符号_空转化符我们使用#号表示 };
-
NFA单元——一个大的NFA可以有NFA单元组成。
struct NFACeil{ NodeState startNode;//开始节点状态 NodeState endNOde;//终止节点状态 int EdgeCount;//边数 Edge Edgeset[MAX];//该NFA拥有的边数 };
(3)了解Thompson方法原理(参考编译原理书本即可)
将输入的r分解为最基本的子表达式,然后利用3条规则构造NFA。不断重复,直到输入的r的所有子表达式都得到了NFA。规则1、2和3 是NFA构建的基本单元,含有两个状态以及状态转换的边。
- 规则1:构造符号为“”,即空字符的NFA
- 规则2:构造符号为a的NFA,a属于符号集的一个字符
- 规则3:构造空集的NFA
图片来自:
https://blog.csdn.net/gongsai20141004277/article/details/52949995
上面的过程用程序来实现就是传入一个符号,得到一个NFA单元——NFACell init(char element)
之后使用下面规则合并得到的NFA单元:
- s|t——NFACell do_aORb(NFACell a,NFACell b)
- st——NFACell do_aANDb(NFACell a,NFACell b)
- s*——NFACell do_aStar(NFACell a)
图解:
上面这些过程我们需要具体程序来实现。
中缀表达式和后缀表达式(细节处理)
我们需要把中缀表达式转后缀表达式,以便处理符号优先级和去除括号。进而简化我们的代码书写。而在这个过程中很重要的一点是把经过检查后得到的有效正规式(中缀表达式)做一个预处理:
- 加入+号,将省略的连接符显式加进去,表明这两个正规式的关系,如ab变成a+b,ab变成a+b,说明该正规式由a*和b组成。要加入加号的情况有4种:
/* 给中缀表达式添加 +号 */
string joinAddSymbol(string regxValid){
//------------------需要添加+号的情况--------------------//
// ab -》 a+b ----ab //
//ab*->a+b*------ab 情况1 //
// //
// a(b|c)-》a+(b|c)--a( 情况2 //
// (a|b)c-》(a|b)+c--)b 情况3 //
// //
// a*b-》 a* + b----*b 情况4 //
//-----------------------------------------------------//
if(regxValid.length()<2) return regxValid;//边界处理
int len = regxValid.length();
int newLen = len+len+2;//添加+号之后的长度至多为该len
char *return_string = new char[newLen];//最多是两倍
int index=0;//记录返回字符串的长度
char pre,now;//两个字符来判断,pre,now必然会被初始化一次
for(int i=0;i+1<len;i++){//保证了有两个字符
pre = regxValid[i];
now = regxValid[i+1];
return_string[index++] = pre;//都是在第一个字符后面才添加的+号
//处理情况1
if(isLetter(pre) && isLetter(now)){
return_string[index++] = '+';//插入+号
}else if( isLetter(pre) && now=='('){
//情况2 a(
return_string[index++] = '+';//插入+号
}else if(pre==')' && isLetter(now)){
//情况3 )b
return_string[index++] = '+';//插入+号
}else if(pre=='*' && isLetter(now)){
//情况4 *b
return_string[index++] = '+';//插入+号
}
//上面的某些分支可以合起来写,但是为了逻辑清晰则分开写。
}
//最后写入now符号
return_string[index++] = now;
return_string[index++] = '\0';//添加结束符号
string newRegex(return_string);//创建为string
cout<<"中缀表达式添加+号:"<<newRegex<<endl;
return newRegex;
}
对中缀表达式进行了预处理以后,我们还需要处理表达式中涉及符号的优先级:
关于运算符的优先级设计参考:https://blog.csdn.net/gongsai20141004277/article/details/106607743
最后我们编写中缀表达式转后缀表达式的程序——string utils_postFix (string e)
输入正规式和转换NFA
输入正规式,我们要检查其合法性,我们编写一个工具类来做即可:
#include "utils.h"
/*输入正则式 */
string input(){
cout<<"please input one regexExperssion:( valid character included () * | a~z A~Z ) "<<endl;
string regex;
cin>>regex;
if(isValid(regex)==1){
cout<<"input is valid"<<endl;
return regex;
}else {
cout<<"no a valid regexExperssion,please check your input!"<<endl;
throw "error";
}
}
/*
* 判断输入字符e是否为字母
*/
bool isLetter(char e){
if((e>='a' && e<='z') || (e>='A' && e<='Z')){
return true;
}
return false;
}
/*
* 是否为合法字符
*/
bool isCharacter(string regex){
int n = regex.length();
bool res = true;
for(int i=0;i<n;i++){
if(!((regex[i]=='(' || regex[i]==')' || regex[i]=='|' || regex[i]=='*')
|| isLetter(regex[i]))){
res = false;
break;
}
}
return res;
}
/*
* 判断输入的正则式是否含有括号,其括号是否匹配
*/
bool isMatchParenthesis(string checkregx){
stack<char> s;
int n = checkregx.length();
int i=0;
while(!s.empty() && i<n){
if(checkregx[i]=='('){
s.push(checkregx[i]);
}
if(checkregx[i]==')'){
if(s.empty()){
cerr << "有多余的右括号" << endl;
return false;
}else s.pop();//s只允许( 入栈
}
i++;
}
if(!s.empty()){
cerr << "有多余的左括号" << endl;
return false;
}
return true;
}
/*
* 判断输入的正则式是否有效
*/
bool isValid(string regex){
bool res1 = isMatchParenthesis(regex);//括号是否匹配
bool res2 = isCharacter(regex);//是否为合法字符
return res1 && res2;
}
转化NFA:
/**表达式转NFA处理函数,返回最终的NFA结合
* 利用三条规则
*/
NFACell regex2NFA(string expression){
int length = expression.size();
char element;
NFACell Cell, Left, Right;
stack<NFACell> STACK;
for (int i = 0; i<length; i++)
{
element = expression.at(i);
switch (element)
{
case '|'://或
Right = STACK.top();
STACK.pop();
Left = STACK.top();
STACK.pop();
Cell = do_aORb(Left, Right);
// displayNFA(Cell);
// cout<<Cell.StartState.StateName<<Cell.EndState.StateName<<endl;
STACK.push(Cell);
break;
case '*'://闭包
Left = STACK.top();
STACK.pop();
cout<<Left.StartState.StateName<<Left.EndState.StateName<<endl;
Cell = do_aStar(Left);
// displayNFA(Cell);
// cout<<Cell.StartState.StateName<<Cell.EndState.StateName<<endl;
STACK.push(Cell);
break;
case '+'://链接
Right = STACK.top();
STACK.pop();
Left = STACK.top();
STACK.pop();
Cell = do_aANDb(Left, Right);
// cout<<Cell.StartState.StateName<<Cell.EndState.StateName<<endl;
STACK.push(Cell);
break;
default:
Cell = init(element);//初始化
// cout<<Cell.StartState.StateName<<Cell.EndState.StateName<<endl;
//displayNFA(Cell);
STACK.push(Cell);
}
}
cout << "处理完毕!" << endl;
Cell = STACK.top();
STACK.pop();
return Cell;
}
展示NFA:
void displayNFA(NFACell nfa){
cout << "NFA 的边数:" << nfa.EdgeCount << endl;
cout << "NFA 的起始状态:" << nfa.StartState.StateName << endl;
cout << "NFA 的结束状态:" << nfa.EndState.StateName << endl;
for (int i = 0; i<nfa.EdgeCount; i++)
{
cout << "第" << i + 1 << "条边的起始状态:" << nfa.EdgeSet[i].StartState.StateName
<< " 结束状态:" << nfa.EdgeSet[i].EndState.StateName
<< " 转换符:" << nfa.EdgeSet[i].TransSymbol << endl;
}
cout << "结束" << endl;
}
测试结果
输入ab:
输入ab*:
输入ab*|b:
输入abcb:
使用DFA程序来跑: