编译原理-提取左公因子(java算法实现)

源代码地址

一. 相同公共前缀

自顶向下分析 CFG 树,如果相同产生式左部对应的产生式右部相同公共前缀,那么在语法分析的时候,就不知道该选择那个产生式了。

CFG 树是由2型文法(即上下文无关文法)生成的树。详情看文法定义

即如下例子:

S -> aAc | aBd

对于非终结符 S,它对应的候选式(即产生式)有相同前缀 a, 当我们要替换 S 的时候,就不知道该选择那一个候选式,因此我们要消除这种情况。

二. 提取左公因子

这里就用到了提取左公因子算法:

S -> aAc | aBd | cC | d
变成
S -> aS' | cC | d
S' -> Ac|Bd

算法理解起来很简单,就是有相同前缀的多个产生式除相同前缀剩余部分联合起来变成一个新的产生式组,让它成为一个新的非终结符对应的候选式组。

即通过改写产生式来推迟决定,等读入了足够多的输入,获得足够信息后再做出正确的选择。

在这里吐槽一句啊,网上很多文章到这里就结束了,告诉你就这么提取左公因子就行了啊。

提取左公因子算法就是属于看起来很简单,也很容易理解,但是真的想实现这个算法,你会发现很困难。

比如说:

S -> aAb | bbBd | bbbBf | ccCd | ccDd| bbbCf | bbCd | aBb | d | f

对于非终结符 S, 你会发现就是想找出拥有相同前缀的产生式都非常困难啊。有人会说很容易分辨啊,我一眼就看出来了,aAbaBb 有相同前缀啊,但是你怎么告诉计算机用眼睛看呢。

三. 提取左公因子算法描述

提供左公因子.png
  1. 最长公共前缀a 是一个文法符号串(即 a∈(VN∪VT)*),但是空串 ε 除外。
  2. 要寻找最长公共前缀
S -> aaB | aaaC | aaaD
先变成
S -> aaB | aaaS'
S' -> C | D
然后再变成
S -> aaS''
S'' -> B | aS'
S' -> C | D

注意这里进行了两次转换,先提取最长公共前缀 aaa ,再提取公共前缀 aa

  1. 只要两个以上的产生式存在公共前缀,就需要进行提取。

记得算法中有一道经典的题目,求字符串数组最长公共前缀,里面提供了很多算法,但是那道题和提取左公因子是有区别的,例如:

// 对于产生式
S -> aaB | aaC | d |f
// 即字符串数组为
["aaB", "aaC", "d", "f"]

产生式中是有公共前缀 aa ,但是对于字符串数组,是没有公共前缀的,因此那个题的大部分算法都解决不了这个问题。

四. 代码实现

4.1 借助树这种数据结构

刚开始的时候,我想使用循环来查找多项之间的最长公共前缀,但是很快我发现这个方法行不通,因为无从下手,比如

S -> d | aaB | aaaC | aaaDd | f

很难找到一个基准,作为循环判断的依据。
在思考良久之后,我突然想到了一种方式,那就是借助树这种数据结构来解决这个问题。

将一个产生式看成树的一个路径,产生式的字符看出路径上的节点,一个非终结符对应的产生式组就对应一颗完整的树。
例如对于下面这组产生式

S -> d | aaB | aaaC | aaaDd | f

对应树的结构:


初始树.png

观察这颗树,我们很容易发现有两组公共前缀,分别是 aaaCaaaDd ,以及 aaBaaaS' (S'C | Dd)。

我们也很容易实现这个方法,通过树的后序遍历,先处理子节点,找出包含两个以上路径的子节点,对其进行处理,最后一直遍历到根节点。
比如这里的第二层和第三层的 a 节点(根节点表示第零层),注意根节点除外。

4.2 Symbol

/**
 * @author by xinhao  2021/8/13
 * 表示文法中字母表中一个符号,包括终结符和非终结符
 */
public class Symbol {
    // 表示符号的值, 这里用 String 表示,而不是 char,
    // 是因为有些符号我们不好用一个 char 表示。 比如 A 对应的 A'
    private final String label;
    private final boolean isTerminator;
}

表示字母表中的一个符号,包括终结符和非终结符。有两个属性:

  1. label : 表示字母表中的符号,使用字符串类型,是因为需要表示 A' 这种组合符号。
  2. isTerminator : 这个符号是终结符还是非终结符。

4.3 Alphabet

/**
 * @author by xinhao  2021/8/13
 * 字母表, 这个类不用实例化。用来记录字母表中所有的符号
 */
public abstract class Alphabet {

    /**
     * 字母表中所有的符号
     */
    public static final Map<String, Symbol> ALL_SYMBOL = new HashMap<>();

    // 初始化
    static {
        for (int index = 0; index <= 9; index++) {
            ALL_SYMBOL.put("" + index, new Symbol("" + index, true));
        }
        // a-z
        for (char ch = 97; ch <= 122; ch++) {
            ALL_SYMBOL.put(String.valueOf(ch), new Symbol(String.valueOf(ch), true));
        }
        // A - Z
        for (char ch = 65; ch <= 90; ch++) {
            ALL_SYMBOL.put(String.valueOf(ch), new Symbol(String.valueOf(ch), false));
        }
        ALL_SYMBOL.put("+", new Symbol("+", true));
        ALL_SYMBOL.put("-", new Symbol("-", true));
        ALL_SYMBOL.put("*", new Symbol("*", true));
        ALL_SYMBOL.put("(", new Symbol("(", true));
        ALL_SYMBOL.put(")", new Symbol(")", true));
    }
    
    public static Symbol addSymbol(char ch) {
        return addSymbol(ch, true);
    }

    public static Symbol addSymbol(char ch, boolean isTerminator) {
        return addSymbol(String.valueOf(ch), isTerminator);
    }

    public static Symbol addSymbol(String label) {
        return addSymbol(label, true);
    }

    public static Symbol addSymbol(String label, boolean isTerminator) {
        Symbol symbol = new Symbol(label, isTerminator);
        ALL_SYMBOL.put(label, symbol);
        return symbol;
    }

    public static Symbol getSymbol(char ch) {
        return getSymbol(String.valueOf(ch));
    }

    public static Symbol getSymbol(String label) {
        return ALL_SYMBOL.get(label);
    }
}

这个类表示字母表,储存所有的字符。

4.4 Production

/**
 * @author by xinhao  2021/8/13
 * 文法中的产生式
 */
public class Production {
    public static final List<Symbol> EMPTY = new ArrayList<>(0);
    /** 产生式左边,而且必须是非终结符 */
    private final Symbol left;

    /** 这里的 List<Symbol> 希望是不可变的,你们可以自己引入 ImmutableList */
    private final List<Symbol> right;

    public Production(Symbol left, List<Symbol> right) {
        this.left = left;
        this.right = right;
    }
    public boolean isEpsilon() {
        return right.isEmpty();
    }

    public Symbol getLeft() {
        return left;
    }

    public List<Symbol> getRight() {
        return right;
    }

   @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append(left).append("->");
        for (Symbol symbol : right) {
            sb.append(symbol.getLabel());
        }
        if (isEpsilon()) {
            sb.append('ε');
        }
        return sb.toString();
    }
}

表示一个产生式:

  1. left : 表示产生式的左部,必须是非终结符。
  2. right : 表示产生式的右部, 是一个文法符号串对应的集合。如果集合数量为 0 ,表示它是一个空串,这个产生式也就是一个空产生式。

4.5 TrieNode

4.5.1 重要属性

/**
 * @author by xinhao  2021/8/14
 * 产生式组对应的树
 * 也表示树中的一个节点,通过 childList 属性,和子节点一起构建一棵树
 * 例如:
 * 非终结符S的产生式组:  S -> d | aaB | aaaC | aaaDd | f
 * 每一个产生式 d 或 aaB 都对应树中的一个路径,而产生式各个字符就变成树路径上的一个个节点
 */
public class TrieNode {

    /**
     * 表示这棵树属于那一个非终结符
     * 这棵树中每一个节点这个字段是一样的,也与productions中产生式左部相同。
     */
    private final Symbol productionKey;

    /**
     * 当前树节点对应的符号
     */
    private final Symbol symbol;

    /**
     * 表示当前树节点到根节点符号的集合
     * 例如 产生式是 P -> abcAB
     * 当前节点是 c ,那么这个 prefixSymbols 就是 [a, b, c]
     */
    private List<Symbol> prefixSymbols;
    /**
     * 当前树节点的子节点
     */
    private List<TrieNode> childList = new ArrayList<>();
    /**
     * 当前树节点包含的所有产生式,因为产生式对应一个路径,所以也就是当前树节点下面有几条路径。
     */
    private List<Production> productions = new ArrayList<>();

    private TrieNode(Symbol productionKey, Symbol symbol, List<Symbol> prefixSymbols) {
        this.productionKey = productionKey;
        this.symbol = symbol;
        this.prefixSymbols = prefixSymbols;
    }
    ...
}

TrieNode 表示树中的一个节点,但是和它的子节点 childList 一起,就会组成一棵树了。主要属性:

  1. productionKey : 表示这棵树属于那一个非终结符,

因为一棵树对应一个非终结符的产生式组,productionKey 就表示这个非终结符,因此这棵树中每一个节点 productionKey 字段都是一样的。

  1. symbol : 当前树节点对应的符号。
  2. prefixSymbols : 表示当前树节点到根节点符号的集合。

例如 产生式是 S -> abcAB, 当前树节点是 c ,那么这个 prefixSymbols 就是 [a, b, c]

  1. childList : 当前树节点的子节点。
  2. productions : 当前树节点包含的所有产生式。

因为产生式对应一个路径,所以也就是当前树节点下面有几条路径。

4.5.2 createRoot 方法

    public static TrieNode createRoot(Symbol productionKey) {
        /**
         * 树根不包含具体字符, 前缀字符列表也是空
         */
        return new TrieNode(productionKey, null,  new ArrayList<>(0));
    }

4.5.3 addProductionToTreePath 方法

    /**
     * 通过递归调用的方式,将产生式变成树中一个路径
     * 从树的根节点开始调用,pos 等于 0,表示
     * @param production
     * @param pos           产生式对应位置
     */
    public void addProductionToTreePath(Production production, int pos) {
        // 首先要将这个产生式添加到当前树节点的产生式列表中
        productions.add(production);

        // 说明 pos 已经是最后的位置了,产生式已经转换完成,直接返回
        if (pos >= production.getRight().size()) {
            return;
        }
        // 获取产生式 production 在 pos 位置的字符
        Symbol symbol = production.getRight().get(pos);

        // 表示该字符 symbol 对应的树节点
        TrieNode symbolNode = null;
        /**
         * 看当前节点的子节点中,包不包含该字符 symbol;
         * 如果包含,就说明有共同前缀的路径,使用这个树节点,将产生式后面的字符,添加到这个树节点另一条路径中
         * 如果不包含,就创建新的树节点,添加到当前节点的子节点列表中,再将产生式后面的字符,添加到这个新树节点路径中
         */
        for (TrieNode child : childList) {
            // 当前节点的符号与产生式对应位置pos 符号相同,那么直接赋值
            if (child.symbol.equals(symbol)) {
                symbolNode = child;
                break;
            }
        }

        // 如果没有找到,那么就创建新的TrieNode
        if (symbolNode == null) {
            /**
             * 这个新节点对应的前缀
             */
            List<Symbol> newPrefixSymbols = new ArrayList<>(prefixSymbols);
            newPrefixSymbols.add(symbol);
            symbolNode = new TrieNode(productionKey, symbol, newPrefixSymbols);
            childList.add(symbolNode);
        }

        /**
         * 通过递归的方式,将 production 剩余字符添加到树的路径中
         */
        symbolNode.addProductionToTreePath(production, pos + 1);
    }

这个方法就是通过递归调用的方式,将产生式变成树中一个路径。具体方法流程,代码中已经注释很清楚了。

另外再提供一个非递归的实现方式:

    /**
     * 向树中添加一个产生式
     * @param production
     */
    public void addProductionToTree(Production production) {
        TrieNode parentNode = this;
        parentNode.productions.add(production);
        for (Symbol symbol : production.getRight()) {
            TrieNode symbolNode = null;
            for (TrieNode childNode : parentNode.childList) {
                if (childNode.symbol.equals(symbol)) {
                    symbolNode = childNode;
                    break;
                }
            }
            if (symbolNode == null) {
                List<Symbol> prefixSymbols = new ArrayList<>(parentNode.prefixSymbols);
                prefixSymbols.add(symbol);
                symbolNode = new TrieNode(productionKey, symbol, prefixSymbols);
                parentNode.childList.add(symbolNode);
            }
            symbolNode.productions.add(production);
            parentNode = symbolNode;
        }
    }

4.5.4 extractLeftCommonFactor 方法

    /**
     * 通过递归的方式,获取提取左公因子后的产生式列表,包括新字符对应的产生式列表
     * 返回的 Production 表示处理过的路径对应产生式,即合并了多条子路径后的产生式
     * 因为我们使用递归的方式,所以用 newProductionList 来存储产生的新的字符串
     * @param newProductionList
     * @return
     */
    public Production extractLeftCommonFactor(List<Production> newProductionList) {
        /**
         * 如果 productions.size() == 1,表示从当前树节点往下的路径只有一个,没有分叉,
         * 那么它下面就不会有公共前缀的产生式,不用再向下递归了。
         */
        if (productions.size() == 1) {
            // 直接返回列表中唯一的产生式,也不需要对产生式做处理
            return productions.get(0);
        }
        /**
         * 表示当前树节点下面已经做过处理的产生式列表。
         * 因为如果当前树节点下面子路径有公共前缀,那么有公共前缀的多个产生式要合并成一个产生式返回才行。
         */
        List<Production> childHandledProductions = new ArrayList<>();
        for (TrieNode child : childList) {
            // 返回做过处理的子产生式
            Production handledProduction = child.extractLeftCommonFactor(newProductionList);
            childHandledProductions.add(handledProduction);
        }
        /**
         * 当 symbol == null, 表示是树的根节点。
         * 根节点的分叉路径不用做处理。
         */
        if (symbol == null) {
            newProductionList.addAll(childHandledProductions);
            return null;
        }
        /**
         * 处理过的产生式路径只有一条,那么也是直接返回它
         */
        if (childHandledProductions.size() == 1) {
            return childHandledProductions.get(0);
        }


        /**
         * 需要合并路径,生成新的字符,例如 S' S'' S'''
         * 将新的字符指向分叉路径
         */
        Symbol newSymbol = newSymbol(productionKey);

        /**
         * 整个计算规则就是:
         * S -> aaaC | aaaD
         * 先生成新的字符S' (newSymbol),
         * 将 S' -> C | D
         * 最后再生成合并后的产生式 S -> aaaS' 返回。
         *
         */
        for (Production production : childHandledProductions) {
            List<Symbol> newSymbolSymbolList = new ArrayList<>();
            // 当前节点之后的字符,
            for (int index = prefixSymbols.size(); index < production.getRight().size(); index++) {
                newSymbolSymbolList.add(production.getRight().get(index));
            }
            // 生成新字符对应的产生式
            Production newSymbolProduction = Production.create(newSymbol, newSymbolSymbolList);
            newProductionList.add(newSymbolProduction);
        }


        /**
         * 生成合并后的产生式
         */
        List<Symbol> newSymbolList = new ArrayList<>(prefixSymbols);
        newSymbolList.add(newSymbol);
        Production newProduction = Production.create(productionKey, newSymbolList);
        return newProduction;
    }

    private static int newSymbolCount = 0;
    private static Symbol newSymbol(Symbol currentProductionLeft) {
        newSymbolCount++;
        StringBuilder sb = new StringBuilder(currentProductionLeft.getLabel());
        for (int index = 0; index < newSymbolCount; index++) {
            sb.append("'");
        }
        return Alphabet.addSymbol(sb.toString(), false);
    }

通过递归的方式,获取提取左公因子后的产生式列表。具体流程,代码中已经分析很清晰了,对应下面这个图:


产生式组png.png

五. 例子

   private static List<Symbol> createSymbols(String str){
        List<Symbol> symbolList = new ArrayList<>();
        for (char ch : str.toCharArray()) {
            symbolList.add(Alphabet.getSymbol(ch));
        }
        return symbolList;
    }

    public static void main(String[] args) {
        Symbol S = Alphabet.getSymbol('S');
        List<Production> productions = new ArrayList<>();
        productions.add(new Production(S, createSymbols("d")));
        productions.add(new Production(S, createSymbols("aaB")));
        productions.add(new Production(S, createSymbols("aaaC")));
        productions.add(new Production(S, createSymbols("aaaDd")));
        productions.add(new Production(S, createSymbols("f")));

        TrieNode root = TrieNode.createRoot(S);

        for (Production production : productions) {
            root.addProductionToTreePath(production, 0);
        }

        List<Production> newProductionList = new ArrayList<>();
        root.extractLeftCommonFactor(newProductionList);

        System.out.println(productions);
        System.out.println(newProductionList);
    }

运行结果:

[S->d, S->aaB, S->aaaC, S->aaaDd, S->f]
[S'->C, S'->Dd, S''->B, S''->aS', S->d, S->aaS'', S->f]
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容