LL(1)分析法完整落地:从First集、Follow集到C语言实现
发布时间:2026/9/26 14:58:11来源:尧图网络
简介山东科技大学2022年编译原理实验中的LL(1)语法分析实现资料面向正在完成编译原理课程实验或希望掌握预测分析算法原理的本科生与自学者。资料围绕包含加减乘除、括号及变量i的表达式文法给出了完整的LL(1)分析程序可在Code::Blocks中直接编译运行并支持对任意输入符号串进行语法分析同时通过预测分析表和分析栈的变化展示推导过程帮助理解FIRST集、FOLLOW集、预测分析表及错误处理机制。资源包大小约1.08MB内含可直接运行的源码与完整实验报告报告包括实验题目、算法流程、核心数据结构、测试样例及结果分析可配合代码逐行阅读也便于在此基础上扩展或复用。已有913人学习/下载对于需要完成同类实验或复习LL(1)方法的读者来说是一份实用的参考实现。1. LL(1)分析法实现从文法到可运行实验的完整落地这篇笔记拆的是山东科技大学编译原理实验里的一道经典题对给定的算术表达式文法用LL(1)分析法做语法分析要求能在Code::Blocks里直接编译运行实验报告也一并配齐。很多同学做这个实验时卡在同一个节点上First集和Follow集手算没问题一写代码就不知道预测表用什么结构存、主循环的栈操作边界在哪容易出错这些点没人点破就得折腾一晚上。这套资源把预测表构建和栈驱动分析完整串起来输入任意符号串能实时看到栈内容、剩余输入和每一步用到的产生式对正在做编译原理LL(1)实验、想快速跑通又想彻底看懂原理的人非常合适。下面从手算推导到代码实现再到踩坑排查逐步展开关键步骤都附了可直接复现的代码。2. 计算First集与Follow集这份文法的预测分析表是怎么来的2.1 首先做文法体检没有左递归和左公因子是硬前提写代码之前必须先确认题目给的是不是合法的LL(1)文法。题目给出的八条产生式如下E - T G G - T G | - T G | ε T - F M M - * F M | / F M | ε F - ( E ) | i这是把算术表达式常见的「加减、乘除、括号、变量」语法做了改造之后的产物。原始的表达式文法如果写成 E - E T | E - T | T 这种形式直接带左递归没法套LL(1)的分析流程所以第一步就是消除左递归顺便提取左公因子最后得到上面这组产生式。对照这组产生式做体检结论是E只有一个右部TG不存在多候选分支所以不会冲突G的候选右部以、-、ε开头首符号互不相同M的候选右部以*、/、ε开头也是互不相同。既没有直接左递归也没有间接左递归因此这组文法满足LL(1)的使用前提。这个「体检」步骤在实验报告里应该明确写出来它解释了你为什么可以放心使用预测分析表而不是去写带回溯的递归下降分析。我批过不少实验报告有些同学拿到文法就开始写代码连左递归检查都跳过了。等预测表里出现一个格子对应多个产生式的情况才意识到是文法本身有问题。所以把这步放在最前面是帮你省下后面一整晚的排查时间。2.2 FIRST集手算从F开始逐层往上推计算First集有个固定顺序从产生式最底层的非终结符开始逐层往上。本题最底层是F。F - ( E ) | i两个候选右部分别以(和i开头所以FIRST(F) {(, i}。注意这里F的First集不含ε因为F不可能推导出空串这一点会直接影响后面所有关于「是否可空」的判断。然后是T - F M右部第一个符号是F所以FIRST(F)里非ε的符号全部进入FIRST(T)于是FIRST(T) {(, i}。因为F不可空这里不需要再看M。接着E - T G同理FIRST(E) FIRST(T) {(, i}。E是整个文法的开始符号但求First集不要受开始符号身份的影响该从右部首符号取就从右部首符号取。G - T G | - T G | ε三个候选右部里有两个直接以终结符开头一个推导为空。直接归并得到FIRST(G) {, -, ε}。这里ε必须单独列出因为它标记了G是可空的后面Follow集和预测表构造都离不开这个信息。M - * F M | / F M | ε同理FIRST(M) {*, /, ε}。到这里整个文法的First集全部得到非终结符FIRST集E{ (, i }G{ , -, ε }T{ (, i }M{ *, /, ε }F{ (, i }注意G和M这两行都带ε这意味着它们在推导过程中可以选择推导为空串。这个「可空」属性是后面Follow集计算里最容易用错的一环。2.3 FOLLOW集手算最容易漏掉的就是ε产生式那一步Follow集的计算规则可以浓缩成三条。第一开始符号的Follow集里一定有#也就是输入结束符。第二对形如 A - α B β 的产生式FIRST(β)去掉ε后全部进入FOLLOW(B)。第三如果β可以推导出ε或者B就是右部最后一个符号那么FOLLOW(A)整体进入FOLLOW(B)。按这个规则逐层推导。E是开始符号FOLLOW(E)先有{ # }。再找E出现在哪些产生式的右部只有F - ( E )E后面跟的是)所以)也进入FOLLOW(E)最终FOLLOW(E) {), #}。然后是G。G出现在E - T G这个产生式的右部末尾所以FOLLOW(E)整体进入FOLLOW(G)FOLLOW(G) {), #}。G不会出现在其他产生式右部计算到此结束。接着是T。T出现在E - T G以及G - T G | - T G中。先看E - T GT后面是GFIRST(G)去掉ε得到{, -}进入FOLLOW(T)又因为G可空FOLLOW(E)里的{), #}也要进入FOLLOW(T)。再看G - T GT后面同样是G用同样的两步规则推出来的还是{, -}和FOLLOW(G) {), #}没有新增。所以FOLLOW(T) {, -, ), #}。再是M。M出现在T - F M和M - * F M | / F M。以T - F M为例M在右部末尾FOLLOW(T)整体进入FOLLOW(M)所以FOLLOW(M) {, -, ), #}。最后是F。F出现在T - F M和M - * F M | / F M中。以T - F M为例F后面是MFIRST(M)去掉ε得到{, /}进入FOLLOW(F)又因为M可空FOLLOW(T) {, -, ), #}也要进入FOLLOW(F)。汇总得到FOLLOW(F) {, /, , -, ), #}。如果把M可空这一步漏掉FOLLOW(F)就会少四个符号后面预测表F行就会缺项输入串只要出现对应符号就报分析失败这是实验里最常见的隐藏bug来源。最终Follow集汇总非终结符FOLLOW集E{ ), # }G{ ), # }T{ , -, ), # }M{ , -, ), # }F{ *, /, , -, ), # }2.4 预测分析表构造填表规则与本题完整结果预测分析表是二维矩阵行由非终结符编号列由终结符含#编号。填表时对每个产生式A - α执行两步操作。第一步遍历FIRST(α)去掉ε把产生式填入M[A][每个终结符]对应的格子里。第二步如果α可空也就是FIRST(α)里有ε则遍历FOLLOW(A)把同样的产生式填入M[A][FOLLOW中每个终结符]对应的格子里。如果某个格子被填了两次且产生式不同就说明文法不是LL(1)的这也是一种自动化的冲突检测方式。用上面手算的First和Follow逐条填入得到完整预测表非终结符-*/()i#E----E-TG-E-TG-GG-TGG--TG---G-ε-G-εT----T-FM-T-FM-MM-εM-εM-*FMM-/FM-M-ε-M-εF----F-(E)-F-i-注意M行在列和-列各有一个M-ε这是因为FOLLOW(M)包含了和-。当输入符号是或-时M要选择推导为空把控制权交还给上层非终结符。很多人在这一行上出错把和-对应的格子留空导致分析ii这类输入时在M这一步直接卡死。至此手算部分完成。这套分析表在代码里既可以硬编码成二维数组也可以写算法根据First和Follow动态构建。推荐动态构建因为文法一旦微调硬编码的表就得手动改动态构建则自动更新。3. 核心实现思路预测表、分析栈与主循环的三块拼图3.1 数据结构选型用什么表示文法、分析栈和预测表写LL(1)分析器代码组织的核心是三个数据结构。第一个是非终结符表第二个是终结符表第三个是预测分析表本身。本题的终结符有8个、-、*、/、(、)、i、#非终结符有5个E、G、T、M、F。分析表用二维数组存储行数等于非终结符个数5列数等于终结符个数8。C语言里最直接的做法是定义两个字符数组做索引映射char nonTerm[5] {E, G, T, M, F}; char term[8] {, -, *, /, (, ), i, #};通过查找元素在数组中的下标把字符符号翻译成行列索引。有一个容易被忽视的坑不要直接用ASCII码当索引比如table[E][i] 产生式编号这样会分配大量无用空间而且char类型做下标还有可能越界。标准做法是写一个查找函数返回符号对应的行列下标找不到就返回-1。分析栈在C语言里用数组模拟最直观栈底放#栈顶放在数组末尾char stack[MAX_STACK]; int top 0;初始化时先把#压入再把开始符号E压入。这样栈顶永远是当前正在处理的符号每次读取栈顶时取stack[top]就行。为什么初始时要压#因为分析结束的标志是栈里有#且输入串也读到#双#相遇才算结束少一个都会导致死循环或者提前退出。3.2 自动构建预测分析表核心算法与冲突检测预测表的动态构建算法流程如下对每条产生式求FIRST(右部)如果右部第一个符号是终结符直接加入如果是非终结符递归取它的FIRST如果整个右部可空则确定ε属于这条产生式的FIRST。然后根据产生式的FIRST集填表如果产生式可空再根据左部的FOLLOW集补填。这里最关键的递归函数是判断「某个符号串能否推导出ε」。判断条件是符号串里每个符号都必须可空只要有一个不可空的整个串就不可空。这个逻辑在代码里体现为一个循环逐个检查当前符号的FIRST集是否含ε遇到不含ε的符号就提前终止。构建完整之后要立刻做一个自检遍历每个产生式尝试填写表格如果某个格子已经填了一条产生式又来了另一条就报告冲突。这一步对应着LL(1)文法的判定能在文法不满足条件时及时发现而不是等到运行时才暴露分析失败。提示在Code::Blocks里新建控制台项目时把源文件保存为 .c 后缀。如果默认创建的是 .cpp编译器会走C模式个别字符串处理函数的隐式转换可能会触发不必要的警告。3.3 栈驱动分析主循环状态输出与边界条件分析器的主循环是整个程序的发动机。逻辑结构如下栈初始# E 输入串i i * i # 循环条件栈顶非 # 或 输入指针未到末尾 若栈顶是终结符 与当前输入符号比较 相等则弹出栈顶输入指针后移 不等则报告语法错误 若栈顶是非终结符 查预测表 table[非终结符][当前输入符号] 若表项为空 - 语法错误 若表项是产生式 弹出栈顶 若产生式右部不是 ε 将右部符号从右往左依次压栈 打印当前栈、剩余输入和所用产生式这里有两个细节值得特别说明。第一产生式右部为ε时要特殊处理只弹栈不压任何符号。很多实现把ε当作普通符号压栈结果栈里出现ε后续分析在ε上永远匹配不上卡死在循环里。第二压栈顺序必须从右往左也就是从产生式右部的最后一个符号开始压。比如产生式G - TG右部第一个符号要最后压栈这样分析时才能在栈顶最先被处理。主循环的终止条件也容易写错。正确条件是「栈顶和输入指针都到达#」也就是栈中只剩#且输入串也恰好读完。写成「栈空」是不对的因为初始栈里就有##不会真的被弹出除非输入串末尾也是#并且代码里写了弹出#的动作。稳妥的做法是循环条件写成while(1)在循环体里判断栈顶是否为#且当前输入符为#满足则break并打印分析成功。3.4 输出设计让每一步转移都看得见实验要求里通常会写「输出分析过程」所以主循环里每一步都要打屏。建议至少输出三列信息当前栈内容、剩余输入串、采用哪条产生式。栈内容的打印顺序建议从栈底到栈顶这样和人类阅读习惯一致也方便和手写推导过程对照。剩余输入串则是从当前指针到字符串末尾。产生式输出格式用「左部 - 右部」的写法例如步骤 栈内容 剩余输入 产生式 1 #E ii*i# E-TG 2 #GT ii*i# T-FM这种逐步输出对调试和实验报告截图都非常有价值老师批改时一眼就能判断分析流程是否正确不需要自己从头推演中间状态。4. 完整代码走读Code::Blocks下能直接编译的LL(1)分析器4.1 头文件、宏定义与全局数据结构资源包里的主文件是单个 .c 文件编译环境是Code::Blocks自带的MinGW GCC不需要额外配置。文件开头部分是全局数据定义。#include stdio.h #include stdlib.h #include string.h #define MAX_STACK 100 #define MAX_LEN 100 // 非终结符表 char nonTerm[] {E, G, T, M, F}; int nonTermNum 5; // 终结符表注意顺序与预测表列索引一致 char term[] {, -, *, /, (, ), i, #}; int termNum 8; // 预测分析表-1 表示该格为空即语法错误 int table[5][8]; // 产生式右部用字符串存储 char* prodRight[] { TG, // 0: E-TG TG, // 1: G-TG -TG, // 2: G--TG ε, // 3: G-ε FM, // 4: T-FM ε, // 5: M-ε *FM, // 6: M-*FM /FM, // 7: M-/FM (E), // 8: F-(E) i // 9: F-i }; // 每个产生式的左部非终结符在 nonTerm 中的下标 int prodLeft[] {0, 1, 1, 1, 2, 3, 3, 3, 4, 4};这段代码里有几个设计点直接关系到后续算法的复杂度。第一个是「产生式右部用字符串存储ε用特殊字符串表示」这样压栈时只需判断strcmp(prodRight[i], ε)是否为0可空判断一目了然。第二个是「左部只存下标不存字符」查找符号在表里的位置时直接O(1)取值不用每次都遍历nonTerm数组。第三个是「预测表先初始化为-1」-1表示空项运行时遇到-1直接报语法错误避免把空项当成产生式编号使用。4.2 FIRST集求解子程序迭代稳定化处理FIRST集在代码里用布尔二维数组存储多出的一列标记该非终结符是否可空。由于FIRST集之间存在间接依赖比如FIRST(F)里裹着FIRST(T)的内容而FIRST(T)又反过来依赖FIRST(F)单次递归可能拿不到完整结果所以实际实现用的是迭代稳定化方法初始化所有FIRST集为空反复扫描全部产生式更新FIRST集直到某一轮没有任何变化为止。// first[i][j] 1 表示非终结符 i 的 FIRST 集包含终结符 term[j] // first[i][termNum] 1 表示非终结符 i 可推导出 ε int first[5][9]; // 求某个产生式右部的 FIRST 集结果存入 dest 数组 void getFirstOfRight(int* dest, char* right) { int len strlen(right); for (int k 0; k len; k) { int tIdx getTermIdx(right[k]); if (tIdx ! -1) { dest[tIdx] 1; break; // 以终结符开头立即停止 } int ntIdx getNonTermIdx(right[k]); if (ntIdx ! -1) { for (int c 0; c termNum; c) if (first[ntIdx][c]) dest[c] 1; if (first[ntIdx][termNum] 0) break; // 非终结符不可空迭代停止 continue; // 可空继续看下一个符号 } } // 整个右部所有符号都可空则 ε 属于 FIRST(右部) int allEpsilon 1; for (int k 0; k len; k) { int ntIdx getNonTermIdx(right[k]); if (ntIdx -1 || first[ntIdx][termNum] 0) { allEpsilon 0; break; } } if (allEpsilon) dest[termNum] 1; }这段逻辑的关键在两层判断上。遇到终结符直接停止是因为终结符没有任何展开能力遇到不可空的非终结符停止是因为这个符号已经贡献了它能贡献的全部终结符后面符号的FIRST被它挡住了。只有当前符号可空才继续扫描右部下一个符号这和手算FIRST集的思路完全一致。外层再包一个循环重复调用getFirstOfRight定时检查所有FIRST集是否有变化没有变化就退出循环。这样写虽然比单次递归多一点代码但逻辑清晰不容易漏掉间接依赖。4.3 FOLLOW集求解子程序逐条规则迭代更新FOLLOW集用同样的布尔数组格式存储。初始化时把#放进开始符号E的FOLLOW集里然后循环执行更新规则直到收敛。核心更新逻辑如下// follow[i][j] 1 表示非终结符 i 的 FOLLOW 集包含终结符 term[j] // follow[i][termNum] 1 表示 FOLLOW 集含 #用 term 数组里的 # 来标记 int follow[5][9]; // 每一轮迭代对每个产生式 A - α B β套用两条规则 void calcFollowOnePass() { for (int p 0; p 10; p) { char* right prodRight[p]; int len strlen(right); int leftIdx prodLeft[p]; // A 的下标 // 右部最后一个符号是非终结符直接吸收 FOLLOW(A) int lastNt getNonTermIdx(right[len-1]); if (lastNt ! -1) { for (int c 0; c termNum; c) if (follow[leftIdx][c]) follow[lastNt][c] 1; } // 对右部中每个位置处理 后跟符号串 的 FIRST for (int k 0; k len; k) { int ntIdx getNonTermIdx(right[k]); if (ntIdx -1) continue; // 终结符跳过 // 计算 B 后面符号串的 FIRST int betaFirst[9] {0}; getFirstOfRight(betaFirst, right k 1); // 规则一FIRST(β) 去掉 ε 后加入 FOLLOW(B) for (int c 0; c termNum; c) if (betaFirst[c]) follow[ntIdx][c] 1; // 规则二β 可空FOLLOW(A) 整体加入 FOLLOW(B) if (betaFirst[termNum]) { for (int c 0; c termNum; c) if (follow[leftIdx][c]) follow[ntIdx][c] 1; } } } }这里最需要注意的就是数组索引范围。follow[i][termNum]这一列用来标记#因为#只出现在输入串结尾不会作为普通终结符出现在表列里但FOLLOW集又必须记录它。我在代码里把#直接放进了term数组所以FOLLOW集的最后一列和终结符数组里#的索引是同一位置填表时转换行列下标就能直接对上。外层同样做循环迭代直到所有FOLLOW集不再变化。本题实际三轮迭代就稳定了性能上没有压力。这种迭代法比手写递归清晰得多尤其适合处理多产生式交叉引用的情况。4.4 填表与主循环可运行的核心代码填表过程把前面算好的FIRST和FOLLOW转换成预测表。逻辑是遍历每个产生式对产生式右部的FIRST去ε填表如果右部可空则对左部FOLLOW填表void buildTable() { memset(table, -1, sizeof(table)); for (int p 0; p 10; p) { int left prodLeft[p]; int frst[9] {0}; getFirstOfRight(frst, prodRight[p]); // 第一步右部FIRST的非ε终结符 for (int c 0; c termNum; c) { if (frst[c]) fillCell(left, c, p); } // 第二步右部可空再用左部FOLLOW补填 if (frst[termNum]) { for (int c 0; c termNum; c) { if (follow[left][c]) fillCell(left, c, p); } } } }fillCell里有一行关键的冲突检查void fillCell(int row, int col, int prodIdx) { if (table[row][col] ! -1 table[row][col] ! prodIdx) { printf(判定失败文法不满足LL(1)条件 [%c][%c] 有冲突\n, nonTerm[row], term[col]); exit(1); } table[row][col] prodIdx; }这一步把冲突检测前置到构建阶段比运行时才报错友好得多。表填完之后可以先打印一遍确认内容和手算结果核对无误再跑输入串。主循环的完整代码如下void analyze(char* input) { char stack[MAX_STACK]; int top 0; stack[top] #; stack[top] E; int ip 0; // 输入指针 while (1) { char topSym stack[top]; char curIn input[ip]; // 打印当前状态 printf(栈: ); for (int i 0; i top; i) printf(%c, stack[i]); printf( 输入: %s\n, input ip); if (topSym # curIn #) { printf(分析成功\n); return; } int tIdx getTermIdx(topSym); if (tIdx ! -1) { // 栈顶是终结符 if (topSym curIn) { top--; ip; } else { printf(语法错误栈顶终结符 %c ≠ 输入 %c\n, topSym, curIn); return; } } else { // 栈顶是非终结符 int row getNonTermIdx(topSym); int col getTermIdx(curIn); int prod table[row][col]; if (prod -1) { printf(语法错误非终结符 %c 在输入 %c 下无产生式\n, topSym, curIn); return; } char* right prodRight[prod]; printf(采用产生式: %c-%s\n, nonTerm[row], right); top--; // 弹出左部非终结符 if (strcmp(right, ε) ! 0) { int len strlen(right); for (int k len-1; k 0; k--) { stack[top] right[k]; // 右部从右往左压栈 } } } } }主循环里可空处理是重点。产生式右部是ε时只弹出左部符号不压入任何新符号这样栈顶永远是「下一次要处理的实际符号」。很多学生第一次写LL(1)时会把ε直接压栈分析过程立刻就乱了。提示预测表构建完成后别急着跑完整分析先打印一遍五行八列的表手动核对G行在)和#列是否都有G-εM行在和-列是否都有M-ε。这两处是本题最容易漏填的地方。4.5 实验报告的结构与测试用例设计资源包里的实验报告包含题目原文、文法分析、First集与Follow集推导过程、预测分析表、流程图说明、运行测试、实验结论几个部分。测试用例建议采用以下四组合法串 i最短输入验证单因子分析合法串 ii*i混合加减乘除优先级验证G的推导和M的ε归约合法串 (ii)*i带括号输入验证F-(E)这条产生式是否正确调用非法串 ii*缺少因子验证错误分支能否正确报错且不崩溃每组测试在报告中附一段运行截图。每行输出里的「栈内容、剩余输入、产生式」都是依据老师不需要跟代码推演逻辑看输出序列就能判定算法实现正确与否。5. 避坑排查这个实验里最常翻车的五个细节5.1 运行到一半报非终结符无产生式但手算表看起来没问题现象输入合法串ii分析到某一步突然报语法错误提示某个非终结符在当前输入符号下无产生式。原因预测表填充时只填了FIRST集对应的列没填FOLLOW集对应的列。比如G-ε这条产生式的FIRST集是ε没有实际终结符如果只用第一步填表G行就全是空的。正确做法是产生式右部可空时用FOLLOW(G)里的符号补填。解决确认buildTable函数里第二步的FOLLOW补填逻辑加上了。填完表后打印整个预测表检查G行在)列和#列是否出现G-εM行在列和-列是否出现M-ε。5.2 栈里出现ε分析卡死或产生式无限循环现象程序不报错但一直重复输出同一条产生式或者栈内容快速膨胀最终栈溢出崩溃。原因压栈时把产生式右部的ε字符串当成普通符号压入了栈。比如strcmp判断漏写或者把ε这个字符串本身压进去。后续栈顶是ε匹配终结符规则不行匹配非终结符规则也不行分析流程直接乱套。解决压栈前用strcmp严格判断是否为ε是则不压栈。这个判断必须在弹出左部符号之后执行保证栈里只保留真正的终结符和非终结符。5.3 输入串末尾的换行符导致分析失败现象测试printf里写死的串一切正常用标准输入读串时总是报语法错误打印输入串发现末尾多了一个\n。原因用gets或fgets读入时会保留换行符而输入串的结束标志是#\n在输入串里相当于一个无法识别的符号查预测表时行列下标直接对不上。解决读入后立刻清理输入串末尾的换行符把\n替换为\0。同时检查是否要求用户末尾输入#号如果用户忘了写#程序也应该给出明确提示而不是直接崩。5.4 输入串合法但栈底#被弹出导致打印不出分析成功现象分析到输入串末尾时栈里只剩一个#程序还在继续循环最后因为查表越界崩溃。原因终止条件只判断了输入指针到末尾没判断栈顶是否为#。双#相遇才是结束条件只有输入串结束而栈顶还是非终结符时说明分析还没完成需要继续推导或直接报错。解决终止条件写成topSym # curIn #用栈内容加输入指针双条件判断。不能用栈是否为空来判断因为栈底#本身就是栈的一部分它永远不会被弹出。5.5 实验报告手算表和程序输出表对不上现象报告里手算的预测表和程序跑出来的预测表有几项不一致又说不清谁对谁错。原因手算Follow集时漏算ε产生式传导。本题最典型的是FOLLOW(F)少了*、/、、-四个符号或FOLLOW(M)少了和-。手算一旦漏掉某个FOLLOW预测表里对应的格子就会缺项。解决用程序直接打印预测表逐行列对照报告。如果还有出入回看Follow集推导重点检查「β可空时FOLLOW(A)是否进入FOLLOW(B)」这一步。吃不准就在纸上把每个非终结符出现的位置圈出来逐个推一遍。6. 进阶验证用栈轨迹反向校验算法实现正确性实验代码跑通之后别急着交作业还有一个有效的验证方法拿「栈轨迹」反向推演确认分析器和预测表没有隐藏bug。具体操作是把主循环里每次迭代的栈顶符号和当前输入符号整理成一张轨迹表结合输入串做人工验证。对ii*i#第一行栈顶E、输入i查表得E-TG第二行栈顶T、输入i查表得T-FM第三行栈顶F、输入i查表得F-i。用这个方法走一遍会发现一个关键事实LL(1)分析过程的每一步都是确定的栈顶符号和输入符号唯一决定下一步动作不存在任何分支选择。另一个实用的验证技巧是手工画最左推导树。LL(1)分析器的每次产生式应用本质上就是最左推导的一步展开。把分析过程记录成节点连起来就是输入串的语法分析树。例如ii*i的最左推导序列是E TG FMG iMG i*FMG i*iMG ... ii*i把程序的输出和这个推导序列逐行对比能直接确认栈操作是否符合定义预测表是否在每一步都选择了正确产生式。我在每次做LL(1)语法分析实验时都会强制走一遍「轨迹打印最左推导对照」的过程。虽然多花几分钟但能省掉调试时的无数猜测尤其是那种「看起来跑对了但输入一变就崩」的隐性问题。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网