编译原理老师讲到了求文法每个非终结符的FIRST集可以使用拓补排序实现,正好最近在卷大厂笔试复习到了图方面的内容,就小小实现了一下。。
直接上代码,注释都有详解:
(输入的数据我都规定了一下,e表示空串,不考虑 | 或者非终结符有 ' 的情况...)
方法一:深度优先搜索+记忆化
import java.util.*;
public class Main {
static Map<String,Set<String>> map;//存储每个非终结符对应的右边字符串
static Map<String,Set<Character>> ans;//存储答案集合
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
map = new HashMap<>();
ans = new HashMap<>();
int n = sc.nextInt();//输入n个文法,以->分割
String z = sc.nextLine();//去除空格
for (int i=0;i<n;i++)
{
String s = sc.nextLine();//接收输入的文法,不考虑|运算符
String[] t = s.split("->");//分割
if(!map.containsKey(t[0]))//首次进入先初始化集合
{
Set<String> set = new HashSet<>();
map.put(t[0],set);
ans.put(t[0],new HashSet<Character>());
}
map.get(t[0]).add(t[1]);//加入右边的表达式
}
//遍历对每一个左边的非终结符求first集
for(Map.Entry<String,Set<String>> each:map.entrySet())
{
dfs(each.getKey());
System.out.println(each.getKey()+"的FIRST集为:"+ans.get(each.getKey()));
}
}
static void dfs(String str){//求字符串I的first集
Set<String> set = map.get(str);
//获得这个元素的first集
Set<Character> temp = ans.get(str);
if(temp.size()!=0)//记忆化搜索
return;
for(String a:set)//对I的每一个元素
{
if(a.charAt(0)>='A'&&a.charAt(0)<='Z')//如果开头是非终结符
{//继续寻找它的first
dfs(a.substring(0,1));
//之后把它的first集加入I的first,要除去e
Set<Character> pre = ans.get(a.substring(0,1));
pre.remove('e');
temp.addAll(pre);
}
else {//是终结符
temp.add(a.charAt(0));
}
}
}
}
对应的输入:
6
S->Ap
S->Bq
A->a
A->cA
B->b
B->dB
输出:
A的FIRST集为:[a, c]
B的FIRST集为:[b, d]
S的FIRST集为:[a, b, c, d]
输入(2):
8
E->TG
G->+TG
G->e
T->FH
H->*FH
H->e
F->(E)
F->i
输出:
T的FIRST集为:[(, i]
E的FIRST集为:[(, i]
F的FIRST集为:[(, i]
G的FIRST集为:[e, +]
H的FIRST集为:[e, *]
方法一总结:我上课的时候就想到了这种解法,问题是把节点之间的关系表明,于是想到了用java中的map实现一个非终结符和多个右边文法字符串的对应关系。缺点是递归存在重复调用,于是用记忆化的方法进行剪枝。
方法二:逆拓扑序列实现
这个方法是老师上课提出了,课后和老师讨论了一下实现方法,于是自己试着写了写,确实如何建图是个难点,我这里使用了java中的map实现,并且用map存储了每个结点的出度,写的不是很优雅,大家可以提出能改进的地方:
import java.util.*;
public class Main {
static Map<String,Set<String>> map = new HashMap<>();//图对应的邻接表表示
static Map<String,Set<Character>> ans = new HashMap<>();//对应的答案
static Map<String,Integer> outdegree = new HashMap<>();//出度的哈希表
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();//n个文法
String z = sc.nextLine();//去除空格
for (int i=0;i<n;i++)
{
String s = sc.nextLine();//接收输入
String[] t = s.split("->");//分割左右边的字符串
if(!ans.containsKey(t[0]))
ans.put(t[0],new HashSet<Character>());//初始化
//初始化出度
outdegree.put(t[0],0);
if(t[1].charAt(0)<='Z'&&t[1].charAt(0)>='A')//如果是非终结符
{
String NoEnd = t[1].substring(0,1);//非终结符
if (!map.containsKey(NoEnd))//如果是第一次
{
//初始化
map.put(NoEnd,new HashSet<String>());
}
//由t[0]指向NoEnd
//加入图的邻接表
map.get(NoEnd).add(t[0]);//把指向它的加入集合
outdegree.put(t[0],outdegree.getOrDefault(t[0],0)+1);//更新出度
}
else{//如果是终结符
ans.get(t[0]).add(t[1].charAt(0));//加入答案集
}
}
ArrayDeque<String> queue = new ArrayDeque<>();//队列
for(Map.Entry<String,Integer> each:outdegree.entrySet())//遍历出度
{
if(each.getValue()==0)//出度为0
queue.offer(each.getKey());//加入队列
}
while(!queue.isEmpty())
{
String temp = queue.poll();//出度为0的左边字符串
Set<String> set = map.get(temp);//得到指向它的字符串集合
if (set==null)//未初始化的都是起点空处理
continue;
for(String each:set)
{
//把当前的出度为0的字符串的答案(除了空字符串e)加入所有指向它的字符串的答案集合中
Set<Character> aset = ans.get(temp);
aset.remove('e');//移除空字符串e
ans.get(each).addAll(ans.get(temp));//集合添加
outdegree.put(each,outdegree.get(each)-1);//更新出度
if(outdegree.get(each)==0)//出度为0入队
queue.offer(each);
}
}
//输出答案
for (Map.Entry<String,Set<Character>> a: ans.entrySet()){
System.out.println(a.getKey()+"的FIRST集为:"+a.getValue());
}
}
}
输入:
6
S->Ap
S->Bq
A->a
A->cA
B->b
B->dB
输出:
A的FIRST集为:[a, c]
B的FIRST集为:[b, d]
S的FIRST集为:[a, b, c, d]
输入(2):
8
E->TG
G->+TG
G->e
T->FH
H->*FH
H->e
F->(E)
F->i
输出:
T的FIRST集为:[(, i]
E的FIRST集为:[(, i]
F的FIRST集为:[(, i]
G的FIRST集为:[e, +]
H的FIRST集为:[e, *]
方法二总结:遍历使用拓扑排序就可以实现思路很明确,缺点是思考建图和建图存储出度的细节相对来说比较复杂,本人是java选手,不知道其他语言能不能用其他的数据结构实现,还有FOLLOW集我就没实现,思路应该大差不差。