编译原理高频错题解析:FIRST/FOLLOW集、NFA确定化与LL(1)分析表避坑指南

发布时间:2026/10/2 8:15:17
编译原理高频错题解析:FIRST/FOLLOW集、NFA确定化与LL(1)分析表避坑指南 简介本资源是南京邮电大学《编译原理》课程配套的习题解答汇编面向计算机科学与技术、软件工程等专业本科生及考研复习者聚焦编译系统核心概念的理解与解题训练。内容覆盖翻译程序分类编译、汇编、解释、编译程序八大部分词法/语法/语义分析、中间代码生成、优化与目标代码生成等、文法与语言建模、正规表达式推导、短语结构文法判定、状态图构造与句子识别等重点难点每道习题均附详细推导过程与规范答案。资源为单个PDF文件大小606KB排版清晰、公式准确、逻辑严谨便于打印研读或碎片化学习。已有489人下载学习适合作为课堂作业自查、期末备考强化及考研真题拓展训练的权威参考材料。1. 南邮《编译原理》习题解答收集.pdf不是答案速查表而是能帮你把“推导树画歪”“FIRST集算错”“DFA化简卡死”三类高频翻车现场拉回正轨的实战手札如果你正在啃龙书、王生原《编译原理》或清华第三版第二章——尤其是做到“消除左递归”“LL(1)分析表构造”“NFA→DFA子集构造”这几节时反复在草稿纸上画出语法树又撕掉、FIRST/FOLLOW集交叉验证三遍仍对不上、状态转换矩阵填到一半发现漏了ε-闭包——那这份南邮内部流传多年的《编译原理》习题解答收集.pdf就是你此刻最该打开的“后悔药”。它不是按章节罗列标准答案的教辅而是用真实作业批注痕迹还原出学生从“抄定义”到“真懂推导”的完整认知断点比如P142第1题用两种方法消除直接左递归答案里并列写出“重复法”和“改写法”的等价变形过程并在括号里手写标注“注意改写法引入E’后FOLLOW(E’)必须含#和)才能填分析表”再如P74第8题NFA确定化不仅给出子集构造表还在状态{A,B}行旁批“此处M({A,B},b)M(A,b)∪M(B,b){B}∪{A,B}{A,B}初学者常漏掉B自身转移”。它覆盖南邮课程全部七次作业从源程序/目标程序基础辨析到第七次LL(1)分析表符号串a*bb全流程解析所有解法均严格遵循教材定义如清华第三版对“简单短语”“句柄”的判定逻辑且每道题都暗藏一个易被忽略的边界条件——比如P39第12题构造文法{aⁿbᵐcᵖ}答案给出两种方案一种用四个非终结符分层生成S→ABC→aABC→…另一种用单符号S递归S→aS|bS|cS|ε并在下方小字注明“后者虽简洁但无法保证a/b/c顺序仅当题目未限定顺序时可用”。适合刚学完词法分析想动手推导、正在准备期末考前突击、或带实验课需要快速核对关键步骤的从业者。别把它当PDF存着要打印出来在“P142第5题LL(1)分析表”那页折个角——那里有你调试语法分析器时最需要的对照基准。2. 从源程序到目标代码编译流程八模块拆解与作业题映射实战2.1 编译程序八模块功能定位为什么P14第3题的答案不能只背名词南邮作业P14第3题要求列出编译程序的八个组成部分及其功能。表面看是记忆题实则是整门课的骨架图。很多同学背下“词法分析→语法分析→语义分析→中间代码生成→代码优化→目标代码生成→错误处理→符号表管理”就止步了但作业解答里每个模块都配了对应题号的实例锚点。比如“词法分析程序”功能描述后紧跟P38第1题的字符串运算T₁{11,010}, T₂{0,01,1001}计算T₂T₁时本质就是在模拟词法分析器对输入字符流的切分与拼接——T₂中每个字符串如0与T₁中每个字符串如11首尾连接生成新字符串011这正是词法单元token组合成单词word的底层逻辑。再如“信息表管理程序”解答没空谈概念而是指向P39第15题推导语法树时的符号表操作当句型baabaab中出现多个a时词法分析阶段已为每个a分配唯一标识符ID语法树节点需通过符号表索引其类型如变量/常量和作用域否则无法判断“简单短语a”是否真的可规约。这种题干与模块的强绑定逼你理解每个模块的输入输出接口词法分析输出的是token_type, lexeme二元组语法分析接收的正是这个序列而符号表管理程序的输出如变量地址偏移量会直接喂给目标代码生成模块。所以复习时别孤立背模块名要拿着P38第8题的句型推导过程反向标注每一步调用了哪个模块——SaAb→aBcAb→aidtcAb→aidtcBcAb其中“aBcAb”到“aidtcAb”的转换就触发了语义分析模块对idt是否为合法标识符的查表动作。2.2 翻译程序家族关系图P14第2题的“关系”二字藏着考试陷阱P14第2题问源程序、目标程序、翻译程序等概念及相互关系标准答案引用教材P4图1.3但南邮解答在此处加了血泪经验批注“考试若问‘汇编程序是否属于翻译程序’答‘是’得1分若问‘解释程序是否生成目标程序’答‘否’得2分但若问‘编译程序与解释程序的根本区别’只答‘前者生成目标代码后者不生成’会被扣分——必须强调‘编译程序是整体翻译后执行解释程序是边翻译边执行’”。这个细节直指常见误区把“是否生成文件”当作区分标准。实际上现代JIT编译器如Java HotSpot既生成目标代码又边执行但仍是编译程序。南邮解答用P38第3题的句型验证来具象化对句型aidtccb编译程序会先完成全部语法/语义分析确认其符合G[S]规则后才生成目标码而解释程序遇到aidtc时若发现B未定义会立即报错中断不会继续分析后续ccb。因此关系图的核心是数据流方向——所有翻译程序汇编/编译/解释都以源程序为输入、以某种形式的执行结果为输出但汇编程序输出机器码编译程序输出中间表示或机器码解释程序输出运行时状态。作业中P74第6题构造自动机时要求判断“该自动机是非确定的”其依据正是解释程序对输入字符串eefe的处理方式NFA可同时走多条路径模拟解释器对同一语句的多种语义解读可能而DFA必须唯一确定对应编译器生成的确定性目标码。2.3 语法分析与语义分析的分水岭P14第4题例子里的“类型检查”如何落地P14第4题用赋值语句x:y举例说明语法与语义分析差异标准答案说“语法分析管结构语义分析管意义”。但南邮解答在“x与y类型要一致”后补了一行关键操作“类型检查发生在语义分析阶段需查询符号表获取x、y的声明类型如int/float若类型不兼容则触发错误处理模块生成‘type mismatch’错误信息并记录位置”。这直接关联到P142第5题LL(1)分析表的构造逻辑。例如分析串a*bb时步骤13到14的转换#E’T’F’b → #E’T’F’触发F’→ε此时语义分析模块必须检查F’所代表的因子此处为b是否在符号表中声明为数值型否则即使语法分析成功语义分析也会在后续步骤报错。更隐蔽的坑在P39第15题推导baabaab的句柄时解答指出“句柄a是简单短语但若a在符号表中被声明为函数名而非变量则此a不可作为句柄规约需回溯”。这意味着语法分析树的构建必须与符号表状态同步更新——每次规约产生式如A→a时语义分析模块要将a的属性类型、作用域写入符号表每次归约到非终结符如S→AB时要合并A、B的语义属性。所以做P142第2题间接左递归消除时Z::AZ|b和A::ZA|a的循环依赖不仅是语法问题更是语义分析器在构建符号表时可能陷入无限递归的预警信号。3. 文法与语言从正规式到上下文无关文法的推导链路与作业验证3.1 正规文法→正规表达式双向转换P74第18题的代数求解法详解P74第18题要求根据文法S::cC|a, A::cA|aB, B::aB|c, C::aS|aA|bB|cC|a构造等价正规表达式。南邮解答没有直接套用“消去非终结符”模板而是展示代数求解全过程这对理解文法本质至关重要。第一步解B::aB|c按正规方程BaBc右移得Bac此处为Kleene闭包第二步代入A::cA|aB得AcAaac解出Acaac第三步将A、B代入C::aS|aA|bB|cC|a得Cc(aSacaacbaca)最后代入S::cC|a得到SccaScc*(acaacbaca)a整理为S(cca)(cc(acaac|bac|a)|a)。这个过程暴露了关键细节文法中的|符号对应正规表达式的“或”运算而相邻符号如cC对应连接运算递归产生式如A::cA对应星号闭包。作业中P39第11题L(G){0ⁿ1|n≥1}的推导正是逆向应用此逻辑由S::0S|01可写为S0S01解得S0010⁺1。而P39第12题(5)构造{aⁿbᵐcᵖ}的文法两种方案的取舍也源于此——方案①用S→ABC分层生成确保a/b/c严格分段方案②用S→aS|bS|cS|ε虽简洁但产生的字符串如abc、bca、cab都合法违背了题目隐含的“a先于b、b先于c”的顺序约束故考试中若未明确说明顺序方案①才是安全选择。3.2 文法分类判定P41第24题短语结构文法辨析的四层过滤法P41第24题要求判断8个文法规则属于短语结构文法0型、上下文有关文法1型、上下文无关文法2型还是正规文法3型。南邮解答提炼出四层过滤口诀比死记定义更易操作第一层查α→β中α是否含非终结符若α为空如ε→a或全为终结符如a→b则为0型短语结构文法如题中第3题aA::aB属此列αaA含非终结符但右侧有上下文a第二层查|α|≤|β|是否恒成立若存在α→β且|α||β|如Aa→a则必为0型若所有产生式满足|α|≤|β|则可能是1型如第3题aA::aaA中|aA|2≤|aaA|3第三层查α是否为单个非终结符若所有α均为单非终结符如S→aB则进入2型候选若存在α含多个符号如aA→aB则排除2型如第3题、第4题第四层查β是否符合正规文法模式对2型候选再检β若β为终结符非终结符如aB或纯终结符如a且所有非终结符在右右线性或左左线性则为3型。如第1题S::aB, B::cB|bC, C::cβ均为终结符非终结符或纯终结符且非终结符在右故为3型正规文法。此法在P74第12题NFA最小化中同样适用当判断两个状态是否等价时需检查它们对所有输入符号的转移是否都导向等价状态组——这本质是1型文法中“上下文有关”的思想迁移状态i与j等价当且仅当对任意输入aM(i,a)与M(j,a)所属的状态组相同即转移结果的“上下文”一致。3.3 句型推导与语法树构建P39第15题baabaab的句柄定位实战P39第15题要求对句型baabaab给出推导语法树并求短语、简单短语、句柄。南邮解答的树形图虽为文字描述但标注了关键剪枝点“S→AB→Aa→bB a→b a a b其中最后一个a是句柄”。这里藏着三个易错点短语定义陷阱短语是某子树的所有叶子节点组成的符号串但必须是“某棵子树”的全部叶子。baabaab中“ba”是S→AB子树的叶子b来自Aa来自B错B推导出a需经B→a故“ba”跨了A、B两棵子树不是短语真正短语是aB→a子树、baA→bB→ba子树、baaA→bB→b a a错A→bBB→aB→a a故“baa”对应A→bB→b(aB)→b(a a)是A子树的叶子、baabA→bB→b(aB)→b(a a b)错B→aB→a a无b故“baab”非法正确短语是a、ba、baa、baab、baabaabS整棵树。简单短语判定简单短语是短语中长度最短的且其根节点直接产生该短语。baabaab中a是B→a直接产生故为简单短语ba是A→bB→b a但A不直接产生ba需经B故ba不是简单短语。句柄唯一性句柄是最左简单短语即最左边的、可被某产生式直接规约的短语。此处最左a位置1是B→a产生故为句柄。若误将第二个a位置3当句柄则后续规约A→bB失败因B已规约为aA只剩b无法匹配。此分析直接指导P142第1题左递归消除E::EAT含左递归其句柄是E本身故改写为E→TEE→ATE|ε使句柄变为T规避了E→EAT的无限循环。4. 自动机理论NFA确定化、DFA最小化与LL(1)分析表的三位一体验证4.1 NFA→DFA子集构造P74第8题状态爆炸的压缩技巧P74第8题给定NFA M({A,B},{a,b},M,{A},{B})其中M(A,a){A,B}, M(A,b){B}, M(B,a)∅, M(B,b){A,B}要求构造DFA。南邮解答的子集构造表看似标准但关键在状态命名策略将{A}记为0{B}记为1{A,B}记为2而非笼统称“状态集合”。这样在填表时I₀[A]{A}I₀ₐM({A},a){A,B}2I₀_bM({A},b){B}1I₁[B]{B}I₁ₐM({B},a)∅空集记为ΦI₁_bM({B},b){A,B}2I₂[A,B]{A,B}I₂ₐM({A,B},a)M(A,a)∪M(B,a){A,B}∪∅2I₂_bM({A,B},b)M(A,b)∪M(B,b){B}∪{A,B}2。最终DFA状态集K{0,1,2}终态Z{1,2}因原NFA终态为{B}故含B的状态均为终态。压缩技巧在于当某状态Iₓ对所有输入符号的转移都指向自身如I₂ₐI₂_b2则该状态为吸收态无需再展开其子集。这避免了P74第12题中因盲目展开导致的状态数激增——原NFA有3个状态子集构造理论最多2³8个状态但实际只需3个0,1,2因Φ状态无后继可直接丢弃。4.2 DFA最小化P74第12题等价状态合并的矩阵标记法P74第12题要求将NFA确定化后的DFA最小化。南邮解答采用矩阵标记法比分区迭代更直观列出所有状态对i,jij初始标记所有终态与非终态对如0与10与2对未标记对i,j检查是否存在输入符号a使M(i,a)与M(j,a)为已标记对若存在则标记i,j。对P74第12题DFA状态0[1],1[0],2[0,1]终态Z{[0],[0,1]}{0,2}状态对0,1M(0,a)[0,1]2M(1,a)[0]02,0未标记M(0,b)ΦM(1,b)[1]1Φ与1是否等价因Φ无定义视为不同故0,1标记。状态对1,2M(1,a)[0]0M(2,a)[0,1]20,2为终态对已标记故1,2标记。仅剩0,2未标记且M(0,a)2M(2,a)2M(0,b)ΦM(2,b)2Φ与2不同但Φ,2未在状态对中因Φ非有效状态故0,2保持未标记可合并。最终最小DFA仅2个状态{0,2}与{1}。此法在P142第5题LL(1)分析表验证中复用当检查E→E与E→ε是否冲突时需验证FIRST(E)∩FOLLOW(E)是否为空这本质是判断两个集合E的首符集与E的后继集是否有交集与DFA最小化中判断状态对是否等价逻辑同源。4.3 LL(1)分析表构造与符号串解析P142第5题a*bb的26步推演解密P142第5题要求构造LL(1)分析表并分析a*bb。南邮解答的26步推演表是黄金范本但需读懂其设计逻辑栈顶符号与输入符号的匹配分析栈初始为#E输入a*bb#查表得E→TE故压入ET当栈顶为F时输入a触发F→PF压入FP当栈顶为P时输入a触发P→a弹出P压入a随后a与输入a匹配弹出。ε产生式的触发时机F→ε在输入bb#时触发步骤6因F的FOLLOW集含且当前输入为*故查表选ε同理T→ε在输入b#时触发步骤15因T的FOLLOW集含。错误检测点若某步查表为空如栈顶E输入*但表中E行列为∅则报错。abb全程无空项故成功。此过程揭示LL(1)核心约束对每个非终结符A的每个产生式A→αFIRST(α)与FOLLOW(A)当α⇒*ε时必须互斥。P142第6题(1)的FOLLOW(B){d,c}因B→ε且A→BC故FOLLOW(B)包含FOLLOW(A){d}及FIRST(C)-{ε}{c}确保B→ε与B→b不冲突FIRST(b){b}∩{d,c}∅。5. 避坑指南编译原理作业中高频踩坑的5个血泪现场与当场修复方案5.1 坑1FIRST集计算遗漏ε-推导链导致LL(1)分析表填错现象P142第5题中计算FIRST(T)时得到{(,a,b,∧}但实际应为{(,a,b,∧,ε}导致步骤9查表T→T失败因输入b#时T需选ε但表中T行列为T。原因T::T|ε而T::FTT可推导出ε故T⇒ε进而T⇒ε。计算FIRST(T)必须考虑T→T→FT→F...→ε的完整链即若T→α且α⇒ε则ε∈FIRST(T)。解决FIRST集计算分三步①终结符a的FIRST{a}②非终结符A的FIRST所有A→α中α首符号的FIRST并集③若α⇒ε则将ε加入FIRST(A)。对T::T|ε先算FIRST(T){(,a,b,∧}因T→FTF→PF→(E)等再因T→ε故FIRST(T){(,a,b,∧,ε}。务必对每个含ε的产生式回溯检查其右侧是否可全推ε。5.2 坑2FOLLOW集传播漏掉“继承式”传递使分析表多重入口现象P144第9题改写LL(1)文法后FOLLOW(S){a,#}但若漏算A→Sa中S的FOLLOW则FOLLOW(S)缺a导致S→bB{aB}与S→bB冲突。原因FOLLOW集传播有三类规则①S为开始符号则#∈FOLLOW(S)②A→αBβ则FIRST(β)-{ε}⊆FOLLOW(B)③A→αB或A→αBβ且β⇒*ε则FOLLOW(A)⊆FOLLOW(B)。P144第9题中A→Saβ为空故FOLLOW(A)⊆FOLLOW(S)而FOLLOW(A){c}因B→Ac但解答中FOLLOW(S){a,#}显然漏了c。解决画FOLLOW依赖图S←A←B故FOLLOW(B)→FOLLOW(A)→FOLLOW(S)。计算时先标出所有“继承点”如A→Sa中的S再按拓扑序传播。对S→bB{aB}B后无符号故FOLLOW(B)⊇FOLLOW(S){a,#}又因B→Acc∈FOLLOW(A)故FOLLOW(A)⊇{c}最终FOLLOW(S){a,#,c}。5.3 坑3NFA确定化时ε-闭包未迭代计算导致状态缺失现象P74第6题中NFA有ε转移但子集构造时仅算M(I,a)未算ε-closure(M(I,a))导致状态{A,Z}漏掉Z的ε后继。原因NFA含ε转移时M(I,a)的结果需取ε-闭包即从M(I,a)出发经任意条ε边可达的所有状态。若只算M(I,a){A}却未算ε-closure({A}){A,Z}因A有ε→Z则状态缺失。解决ε-闭包计算必须迭代设S₀初始集S₁S₀∪{所有从S₀经ε可达的状态}若S₁≠S₀则令S₀S₁继续直到收敛。P74第6题中M(S,0){A}ε-closure({A}){A,Z}因A→Z via ε故I₀₀{A,Z}非{A}。5.4 坑4语法树推导中混淆“最左推导”与“规范推导”句柄定位错误现象P39第15题(2)baabaab推导过程写为S→AB→bB→ba→baa→baab→baabaab得出句柄为baab。原因规范推导最右推导要求每次替换最右非终结符而上述推导替换了最左B。句柄定义基于规范推导的逆过程最左规约故必须用最右推导反向找句柄。正确推导S→AB→Ab→aBb→aaBb→aaaBb→...错误应S→AB→Aa→bBa→bba→bbab→...仍错。南邮解答的推导链S→AB→Aa→bB a→b a a b实为最左推导但句柄判定仍正确因其基于实际语法树结构而非推导顺序。解决放弃推导顺序执念直接画语法树S为根左子A推导出bBB推导出a右子B推导出a。故叶子为b,a,a即baa但句型是baabaab说明B还推导出更多。正确树S→ABA→bBB→aBB→aB→aBB→a故叶子b,a,a,a,a,b——即baabaab。最左简单短语是第一个a位置1故句柄为a。5.5 坑5文法分类时误判“上下文有关”将含终结符前缀的产生式当1型现象P41第24题第3题aA::aB认为因左侧含a终结符故为1型文法。原因1型文法要求|α|≤|β|且α→β中α至少含一个非终结符但aA::aB中αaA含非终结符A|α|2≤|β|2看似满足。然而1型定义要求α→β中α的替换必须依赖上下文即αγAδ→γβδ其中γ、δ为任意符号串。aA::aB中γaδεA→B符合γAδ→γβδ故确为1型。但第1题S::aB中αS为单非终结符属2型。解决判断口诀升级若产生式形如X→αX为单非终结符则为2型若形如γXδ→γβδγ、δ非空则为1型若形如α→β且|α|≤|β|无其他限制则为0型。aA::aB中γaδε但δ为空严格说γXδ中δ可为空故仍属1型。考试中若选项含“1型”则选之。6. 进阶验证用Python脚本自动化校验FIRST/FOLLOW集与LL(1)分析表一致性6.1 FIRST/FOLLOW集自动计算脚本三步验证你的手算结果手动计算FIRST/FOLLOW集极易出错尤其当文法含多层递归如P142第5题E→TET→FTF→PF时。我写了一个轻量Python脚本输入文法产生式列表输出各非终结符的FIRST/FOLLOW集并高亮冲突项。以P142第5题文法为例# grammar.py from typing import Set, Dict, List, Tuple # 定义文法非终结符 - [产生式右侧列表] grammar { E: [[T, E]], E: [[, E], [ε]], T: [[F, T]], T: [[*, F, T], [ε]], F: [[P, F]], F: [[*, F], [ε]], P: [[(, E, )], [a], [b], [∧]] } # 终结符集合从产生式右侧提取 terminals {, *, (, ), a, b, ∧, ε, #} def compute_first_sets() - Dict[str, Set[str]]: first {nt: set() for nt in grammar} # 初始化终结符的FIRST为其自身 for nt in grammar: for prod in grammar[nt]: if prod and prod[0] in terminals and prod[0] ! ε: first[nt].add(prod[0]) # 迭代计算直到稳定 changed True while changed: changed False for nt in grammar: for prod in grammar[nt]: if not prod: # ε产生式 if ε not in first[nt]: first[nt].add(ε) changed True else: # 计算prod的FIRST prod_first set() for symbol in prod: if symbol in terminals: prod_first.add(symbol) break else: # 非终结符 prod_first.update(first[symbol] - {ε}) if ε not in first[symbol]: break else: # 所有symbol都可推ε prod_first.add(ε) # 合并到nt的FIRST old_size len(first[nt]) first[nt].update(prod_first) if len(first[nt]) old_size: changed True return first def compute_follow_sets(first: Dict[str, Set[str]]) - Dict[str, Set[str]]: follow {nt: set() for nt in grammar} start_symbol E follow[start_symbol].add(#) # 开始符号后跟# changed True while changed: changed False for nt in grammar: for prod in grammar[nt]: # 在prod中找非终结符X计算FOLLOW(X) for i, symbol in enumerate(prod): if symbol in grammar: # symbol是非终结符 # case 1: X后有符号β if i 1 len(prod): beta prod[i1:] # 计算FIRST(β) beta_first set() for j, s in enumerate(beta): if s in terminals: beta_first.add(s) break else: beta_first.update(first[s] - {ε}) if ε not in first[s]: break else: beta_first.add(ε) # FOLLOW(X) FIRST(β) - {ε} old_size len(follow[symbol]) follow[symbol].update(beta_first - {ε}) if len(follow[symbol]) old_size: changed True # case 2: β⇒*ε则FOLLOW(nt) ⊆ FOLLOW(X) if ε in beta_first: old_size len(follow[symbol]) follow[symbol].update(follow[nt]) if len(follow[symbol]) old_size: changed True # case 3: X在prod末尾FOLLOW(nt) ⊆ FOLLOW(X) else: old_size len(follow[symbol]) follow[symbol].update(follow[nt]) if len(follow[symbol]) old_size: changed True return follow if __name__ __main__: first compute_first_sets() follow compute_follow_sets(first) print(FIRST sets:) for nt, s in first.items(): print(f {nt}: {s}) print(\nFOLLOW sets:) for nt, s in follow.items(): print(f {nt}: {s})运行此脚本输出与南邮解答完全一致FIRST(T)包含{(, a, b, ∧, ε}FOLLOW(E)为{#, )}。若手算结果不同脚本会立刻暴露哪一环出错——比如若漏了T→ε的ε传播脚本中first[T]将不含ε从而在后续follow计算中引发连锁错误。6.2 LL(1)分析表冲突检测一键定位“多重入口”风险点LL(1)文法的核心是分析表无冲突即对每个非终结符A和输入符号atable[A本文还有配套的精品资源点击获取