编译原理 词法分析 Java代码实现
词法分析代码步骤
借助GraphViz
生成NFA,DFA,miniDFA等借助GraphViz工具
环境配置详情参见https://blog.csdn.net/qq_18208435/article/details/70464654
生成逆波兰表达式
对正则表达式添加操作符
如a(a|b)*b应该改为a·(a|b)*·b
给定输入字符串st,添加操作符,代码如下:
//假定字符串输入为小写字母。
for(int i = 0; i < st.length(); i++) {
if(i-1>=0) {
char t0 = st.charAt(i-1);
char t1 = st.charAt(i);
if((t0 >= 'a' && t0 <= 'z') && (t1 >= 'a' && t1 <= 'z')) {
//ab->a·b
st.insert(i, "·");
}
else if(t0 == ')' && (t1 >= 'a' && t1 <= 'z')) {
//)a->)·a
st.insert(i, "·");
}
else if((t0 >= 'a' && t0 <= 'z') && t1=='(') {
//a(->a·(
st.insert(i, "·");
}
else if(t0 == '*' && (t1 >= 'a' && t1 <= 'z')) {
//*a->*·a
st.insert(i,"·");
}
else if(t0 == '*' && t1 == '('){
//*(->*·(
st.insert(i,"·");
}
}
}
生成逆波兰表达式
采用调度场算法
- 顺序扫描表达式,如果当前字符是字母(优先级为0的符号) ,则直接输出;如果当前字符为运算符或者括号(优先级不为 0的符号),则判断:
- 若当前运算符为’(’ ,直接入栈;
- 若为’)’ ,出栈并顺序输出运算符直到遇到第一个’(’ ,遇到的第一个 '('出栈但不输出;
- 若为其它,比较运算符栈栈顶元素与当前元素的优先级:
- 如果栈顶元素是’(’,当前元素直接入栈
- 如果栈顶元素优先级>=当前元素优先级,出栈并顺序输出运算符直到 栈顶元素优先级<当前元素优先级,然后当前元素入栈
- 如果栈顶元素优先级<当前元素优先级,当前元素直接入栈
- 重复上述操作直至表达式扫描完毕
- 顺序出栈并输出运算符直到栈元素为空
代码实现:
StringBuilder ans = new StringBuilder();
//char[] Table= {'*','·','|'};
Map<Character,Integer> map = new HashMap<Character,Integer>();//数字表示优先级
map.put('|', 1);
map.put('·', 2);
map.put('*', 3);
Stack<Character> stack = new Stack<>();
for(int i = 0; i < st.length(); i++) {
char t = st.charAt(i);
if(t >= 'a' && t <= 'z') {
ans.append(t);
}
else if(t == '(' || stack.empty()) {
stack.push(t);
}
else if(t == ')') {
char top = stack.pop();
while(top!='(') {
ans.append(top);
top = stack.pop();
}
}
else {
char top = stack.pop();
if(top == '(') {
stack.push(top);
stack.push(t);
}
else {
if(map.get(top) >= map.get(t)){
while(map.get(top) >= map.get(t)) {
ans.append(top);
if(!stack.empty()) {
top = stack.pop();
if(top == '(') {
stack.push(top);
stack.push(t);
break;
}
}
else {
stack.push(t);
break;
}
}
}
else {
stack.push(top);
stack.push(t);
}
}
}
}
while(!stack.empty()) {
ans.append(stack.pop());
}
生成NFA
采用存储边的思想,利用如下数据结构将每一条边存储:
public class Part{
public int s;//存储一条边的start
public int e;//存储一条边的end
public char op;//存储一条边上的op
public Part(int s,int e,char op) {
this.s = s;
this.e = e;
this.op = op;
}
}
通过处理逆波兰表达式得到最终的NFA,处理流程如下:
对你逆波兰表达式扫描
-
如果当前字符为字母,则增加一条新的边Part(num,num+1,op),并将该部分两端的节点(num,num+1)入栈
-
如果当前字符为’ · ',则从Stack中取出两条边,p1,p2。增加三条边:
new Part(p1.e,p2.s,'ε') new Part(num,p1.s,'ε') new Part(p2.e,++num,'ε')
并将该部分两端的节点(num-1,num)入栈
-
如果当前字符为’ | ',则从Stack中取出两条边,p1,p2。增加四条边:
v.add(new Part(num,p1.s,'ε')); v.add(new Part(num,p2.s,'ε')); ++num; v.add(new Part(p1.e,num,'ε')); v.add(new Part(p2.e,num,'ε'));
并将改部分两端的节点(num-1,num)入栈
-
如果当前字符为’ * ',则从Stack中取出一条边,p。增加四条边:
v.add(new Part(p.e,p.s,'ε')); v.add(new Part(num,p.s,'ε')); ++num; v.add(new Part(num-1,num,'ε')); v.add(new Part(p.e,num,'ε'));
并将该部分两端的节点(num-1,num)入栈
注:完成后stack中栈底的边p,p.s为整个NFA的start,p.e为整个NFA的end(终态)。
while(!stack.isEmpty()) {
p = stack.pop();
}
start = p.s;
end = p.e;
生成DFA
这部分比较思路上比较简单,主要是做一下几个方面的工作
-
求ε闭包(状态合并)
-
状态合并后标记终态(含NFA终态节点的DFA节点都是终态,可能不止一个)
注:含NFA初态节点的DFA节点为初态,只有一个
-
将合并后的节点重新编号,重新将DFA的边记录
代码实现思路为:
- 求NFA start节点的epsilon闭包
- 求闭包内节点通过不同操作符到达的节点的epsilon闭包,如果该节点是一个新的节点,则重复2操作直至没有新的节点产生
以下为关键代码实现:
//求start节点的epsilon闭包
public void GetIEpsilon(TreeSet<Integer> s,int start){
vis[start] = 1;
s.add(start);
for (Part part : v) {
if (part.s == start && part.op == 'ε') {
//s.add(v.get(i).e);
if (vis[part.e] == 0)
GetIEpsilon(s, part.e);
}
}
}
//求经过op操作符为a所到达的点的epsilon闭包,存入一个TreeSet
public void GetI(TreeSet<Integer> s,int start,char a){
for (Part part : v) {
if (part.s == start && part.op == a) {
s.add(part.e);
Arrays.fill(vis, 0);
GetIEpsilon(s, part.e);
}
}
}
//将含NFA的start和end节点标记出来,PointSet[i]为DFA的节点,集合内容为NFA的节点编号。
public void GetSE(){
System.out.println("start:"+ TurnNFA.start);
System.out.println("end:"+ TurnNFA.end);
for(int i = 0; i < setNum; i++){
if(pointSet[i].contains(TurnNFA.start)){
start.add(i+1);//只有一个
}
if(pointSet[i].contains(TurnNFA.end)){
end.add(i+1);
}
}
Iterator