编译原理老师讲到了求文法每个非终结符的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集我就没实现,思路应该大差不差。