源代码的地址
一. 相同公共前缀
自顶向下分析 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
, 你会发现就是想找出拥有相同前缀的产生式都非常困难啊。有人会说很容易分辨啊,我一眼就看出来了,aAb
和 aBb
有相同前缀啊,但是你怎么告诉计算机用眼睛看呢。
三. 提取左公因子算法描述
- 最长公共前缀
a
是一个文法符号串(即 a∈(VN∪VT)*),但是空串ε
除外。 - 要寻找最长公共前缀
S -> aaB | aaaC | aaaD
先变成
S -> aaB | aaaS'
S' -> C | D
然后再变成
S -> aaS''
S'' -> B | aS'
S' -> C | D
注意这里进行了两次转换,先提取最长公共前缀
aaa
,再提取公共前缀aa
。
- 只要两个以上的产生式存在公共前缀,就需要进行提取。
记得算法中有一道经典的题目,求字符串数组最长公共前缀,里面提供了很多算法,但是那道题和提取左公因子是有区别的,例如:
// 对于产生式
S -> aaB | aaC | d |f
// 即字符串数组为
["aaB", "aaC", "d", "f"]
产生式中是有公共前缀 aa
,但是对于字符串数组,是没有公共前缀的,因此那个题的大部分算法都解决不了这个问题。
四. 代码实现
4.1 借助树这种数据结构
刚开始的时候,我想使用循环来查找多项之间的最长公共前缀,但是很快我发现这个方法行不通,因为无从下手,比如
S -> d | aaB | aaaC | aaaDd | f
很难找到一个基准,作为循环判断的依据。
在思考良久之后,我突然想到了一种方式,那就是借助树这种数据结构来解决这个问题。
将一个产生式看成树的一个路径,产生式的字符看出路径上的节点,一个非终结符对应的产生式组就对应一颗完整的树。
例如对于下面这组产生式
S -> d | aaB | aaaC | aaaDd | f
对应树的结构:
观察这颗树,我们很容易发现有两组公共前缀,分别是 aaaC
和 aaaDd
,以及 aaB
与 aaaS'
(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;
}
表示字母表中的一个符号,包括终结符和非终结符。有两个属性:
- label : 表示字母表中的符号,使用字符串类型,是因为需要表示
A'
这种组合符号。 - isTerminator : 这个符号是终结符还是非终结符。
4.3 Alphabet
类
/**
* @author by xinhao 2021/8/13
* 字母表, 这个类不用实例化。用来记录字母表中所有的符号
*/
public abstract class Alphabet {
/**
* 字母表中所有的符号
*/
public static final Map 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 EMPTY = new ArrayList<>(0);
/** 产生式左边,而且必须是非终结符 */
private final Symbol left;
/** 这里的 List 希望是不可变的,你们可以自己引入 ImmutableList */
private final List right;
public Production(Symbol left, List right) {
this.left = left;
this.right = right;
}
public boolean isEpsilon() {
return right.isEmpty();
}
public Symbol getLeft() {
return left;
}
public List 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();
}
}
表示一个产生式:
-
left
: 表示产生式的左部,必须是非终结符。 -
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 prefixSymbols;
/**
* 当前树节点的子节点
*/
private List childList = new ArrayList<>();
/**
* 当前树节点包含的所有产生式,因为产生式对应一个路径,所以也就是当前树节点下面有几条路径。
*/
private List productions = new ArrayList<>();
private TrieNode(Symbol productionKey, Symbol symbol, List prefixSymbols) {
this.productionKey = productionKey;
this.symbol = symbol;
this.prefixSymbols = prefixSymbols;
}
...
}
TrieNode
表示树中的一个节点,但是和它的子节点 childList
一起,就会组成一棵树了。主要属性:
-
productionKey
: 表示这棵树属于那一个非终结符,
因为一棵树对应一个非终结符的产生式组,
productionKey
就表示这个非终结符,因此这棵树中每一个节点productionKey
字段都是一样的。
-
symbol
: 当前树节点对应的符号。 -
prefixSymbols
: 表示当前树节点到根节点符号的集合。
例如 产生式是
S -> abcAB
, 当前树节点是c
,那么这个prefixSymbols
就是[a, b, c]
。
-
childList
: 当前树节点的子节点。 -
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 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 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 newProductionList) {
/**
* 如果 productions.size() == 1,表示从当前树节点往下的路径只有一个,没有分叉,
* 那么它下面就不会有公共前缀的产生式,不用再向下递归了。
*/
if (productions.size() == 1) {
// 直接返回列表中唯一的产生式,也不需要对产生式做处理
return productions.get(0);
}
/**
* 表示当前树节点下面已经做过处理的产生式列表。
* 因为如果当前树节点下面子路径有公共前缀,那么有公共前缀的多个产生式要合并成一个产生式返回才行。
*/
List 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 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 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);
}
通过递归的方式,获取提取左公因子后的产生式列表。具体流程,代码中已经分析很清晰了,对应下面这个图:
五. 例子
private static List createSymbols(String str){
List 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 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 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]