假设有六个输入符号:字母 a b c d e 和 EOF。假设我们希望我们的语言包含七个字符串:ace add bad bed bee cab dad(每个字符串都必须以 EOF 结尾)。
为 DFA 存储转换表的最简单方法是二维数组simpleNext。一根轴是状态编号,另一根轴是输入符号编号。当 FA 处于状态 s 并看到符号 a 时,它会移动到状态 simpleNext[s][a]。
这是一个用于识别示例字符串的 DFA 的simpleNext 转换表:
st# a b c d e EOF
0 0 0 0 0 0 0
1 2 3 4 5 0 0
2 0 0 16 17 0 0
3 11 0 0 0 12 0
4 9 0 0 0 0 0
5 6 0 0 0 0 0
6 0 0 0 7 0 0
7 0 0 0 0 0 8
8 0 0 0 0 0 0
9 0 10 0 0 0 0
10 0 0 0 0 0 8
11 0 0 0 15 0 0
12 0 0 0 13 14 0
13 0 0 0 0 0 8
14 0 0 0 0 0 8
15 0 0 0 0 0 8
16 0 0 0 0 19 0
17 0 0 0 18 0 0
18 0 0 0 0 0 8
19 0 0 0 0 0 8
状态 0 是错误状态。如果 FA 到达这里,它会在没有从该状态转换的状态下读取一些符号。状态 8(唯一的其他全零行)是唯一的接受状态。
这里的问题是simpleNext 的大小为|S| × |?|(状态数乘以符号数)= 120。对于真正的 DFA(如在语言解析器中发现的那样),具有更多状态和符号,它将是更大。这些天对于 RAM 来说不是太大,但可能会浪费很多 L1 缓存行。大多数状态没有针对大多数符号的转换,因此表的大部分设置为 0(错误状态)。
我们想要一种在更小的空间内存储转换表的方法,同时还能让我们非常快速地访问它。
让我们探索本书中描述的压缩技术的类似但更简单的版本。
让我们稍微不同地排列表格的行。从第 0 行和第 1 行开始:
0 0 0 0 0 0 0
1 2 3 4 5 0 0
然后将第 2 行移至其非零条目(16 和 17)上方只有零:
0 0 0 0 0 0 0
1 2 3 4 5 0 0
2 0 0 16 17 0 0
现在移动第 3 行,直到其非零条目(11 和 12)上方只有零:
0 0 0 0 0 0 0
1 2 3 4 5 0 0
2 0 0 16 17 0 0
3 11 0 0 0 12 0
(请注意,我们必须假设每一行都附加了无数个零。)
对剩余的行一一重复。最后你有这个:
0 0 0 0 0 0 0
1 2 3 4 5 0 0
2 0 0 16 17 0 0
3 11 0 0 0 12 0
4 9 0 0 0 0 0
5 6 0 0 0 0 0
6 0 0 0 7 0 0
7 0 0 0 0 0 8
8 0 0 0 0 0 0
9 0 10 0 0 0 0
10 0 0 0 0 0 8
11 0 0 0 15 0 0
12 0 0 0 13 14 0
13 0 0 0 0 0 8
14 0 0 0 0 0 8
15 0 0 0 0 0 8
16 0 0 0 0 19 0
17 0 0 0 18 0 0
18 0 0 0 0 0 8
19 0 0 0 0 0 8
现在写下我们每行移动的数量:
st# off
0 0 0 0 0 0 0 0
1 0 2 3 4 5 0 0
2 2 0 0 16 17 0 0
3 6 11 0 0 0 12 0
4 7 9 0 0 0 0 0
5 8 6 0 0 0 0 0
6 6 0 0 0 7 0 0
7 6 0 0 0 0 0 8
8 0 0 0 0 0 0 0
9 11 0 10 0 0 0 0
10 8 0 0 0 0 0 8
11 11 0 0 0 15 0 0
12 12 0 0 0 13 14 0
13 12 0 0 0 0 0 8
14 13 0 0 0 0 0 8
15 14 0 0 0 0 0 8
16 15 0 0 0 0 19 0
17 16 0 0 0 18 0 0
18 17 0 0 0 0 0 8
19 18 0 0 0 0 0 8
现在将行折叠成一行,用非零覆盖零。将偏移量保留为单独的数组:
index 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
nextState 2 3 4 5 16 17 11 9 6 7 12 8 10 8 15 13 14 8 8 8 19 18 8 8
state# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
stateBase 0 0 2 6 7 8 6 6 0 11 8 11 12 12 13 14 15 16 17 18
此时,我们可以使用这两个数组从原始数组中找到任何非零值。对于状态s 和符号编号i,如果simpleNext[s][i] 不为零,则nextState[stateBase[s] + i] 中的值相同。
这个方案剩下的唯一问题是我们不知道原始表中的哪些值是零。我们可以通过添加另一个数组来解决这个问题,checkState,平行于nextState。对于nextState 的每个元素,checkState 的对应元素告诉我们nextState 最初是从哪个状态复制而来的:
index 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
nextState 2 3 4 5 16 17 11 9 6 7 12 8 10 8 15 13 14 8 8 8 19 18 8 8
checkState 1 1 1 1 2 2 3 4 5 6 3 7 9 10 11 12 12 13 14 15 16 17 18 19
现在,对于某些状态s 和符号编号i,我们可以查看nextState[stateBase[s] + i] 是否属于状态s,或者原始表中状态s 和符号@987654350 是否为零@:
if (checkState[stateBase[s] + i] == s) {
return nextState[stateBase[s] + i];
} else {
return 0;
}
原始simpleNext 表包含 6 * 20 = 120 个值。压缩表(stateBase、nextState 和 checkState 的组合)包含 2 * 23 + 20 = 66 个数字。对于典型的 DFA(如在语言解析器中发现的那样),具有更多状态和符号,压缩比要好得多。
在实际实现中,我可能会将 nextState 和 checkState 存储为单个结构数组,例如
typedef struct {
int nextState;
int checkState;
} TableEntry;
TableEntry table[NUM_ENTRIES];
无论如何,书中描述的压缩方案是一个更复杂的变体。对我来说已经很晚了,所以我打算离开这里,但我会尝试明天再回来解释这本书的方案与我上面解释的内容有何不同。