编译原理习题与实验通关指南:从词法分析到面试考点
2026/9/18 13:15:19 网站建设 项目流程

写这题的时候,很多人的第一反应是“编译原理这课真难”。从我个人的经历来说,这门课的习题做得好不好,往往比上课听没听懂更能决定考试分数。我当年在学校学完一遍,回头找工作面试又被问了一轮,再后来带新人做项目发现很多人的基础薄弱,其实就差在这些经典习题的思路上。所以这篇文章我不打算讲大而全的理论,就围绕“编译原理习题”这个话题,把词法分析、语法分析、语义翻译、中间代码这些核心模块的做题套路、实验实现思路和面试常考点整理出来,希望能帮你少走点弯路。

坦白讲,编译原理这门课和别的计算机课程不太一样。数据结构、算法你刷刷题就能有手感,但编译原理的习题往往环环相扣,正则表达式没搞透,后面DFA、NFA就觉得费劲;语法分析没入门,后面的语义分析就不要想。很多学校用的是编译原理第三版教材,部分院校比如吉大、哈工大还有自己的一套课件讲义,题目风格差别也不小。但不管用的是哪一本教材,核心题型就那些,把每类题型的标准解法搞清楚,考试和面试都能应对。

1. 编译原理的知识骨架与习题目标

1.1 一个编译器的前端、中端、后端到底对应哪些题

先把这个大框架理一下。编译器的整体结构分成词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成这几个阶段。对应的习题也可以按这个层次分开刷:

  • 词法分析:考正规式、NFA转DFA、DFA最小化、根据词法规则设计状态转换图。
  • 语法分析:考自上而下分析(LL(1)文法、递归下降)和自下而上分析(算符优先、LR(0)、SLR(1)、LR(1)),其中LR系列是重中之重。
  • 语义分析与中间代码生成:考属性文法、语法制导翻译,生成逆波兰式、三元式、四元式。
  • 代码优化:考基本块划分、DAG、循环优化,比如强度削弱、删除公共子表达式。
  • 运行环境与目标代码:考符号表、静态作用域与动态作用域,偶尔考栈式存储分配。

我当年复习时最大的误区就是按教材章节一章一章推,结果前面文法分类还没搞明白,后面LR分析表又来了,越学越乱。后来我把习题按“从源码到目标代码的流水线”重新组织,一道题做完了知道自己处于哪个阶段,思路就顺了。

1.2 为什么说习题是理解编译原理的最快路径

上课听老师讲文法和推导,很多时候是在“听概念”,但只有做题时才会发现一个藏得很深的坑:你知道FIRST集和FOLLOW集的定义,但真正手动构造LL(1)分析表时,容易把空串是否属于某个FIRST集漏掉。这种细节不做题根本不会注意到。

拿一个常见的例子来说,求文法E -> TE'E' -> +TE' | ε的FIRST集也许不难,但到了表格构造时,什么时候该把+填入E'对应的表项,什么时候该根据FOLLOW(E') 在#)下填产生式子,就得把定义彻底弄明白。这其实就是考研、期末和面试的共同考点。

所以我建议的复习路径是:先花两天过一遍教材的课件讲义,然后直接上手刷题,遇到不会的再回头查定义。刷题能帮你把“我以为我会了”变成“我真的会了”。

2. 核心题型拆解与解题模板

2.1 词法分析:正规式、NFA转DFA、DFA最小化

词法分析这块的习题几乎每份试卷都有。最常见的一类题是:给定一个正规式,构造等价的最小化DFA。步骤很固定:

  1. 根据正规式构造NFA(Thompson结构法)。
  2. 用子集构造法把NFA转成DFA。
  3. 对DFA进行最小化,合并等价状态。

我拿一个非常经典的题目举例:正规式(a|b)*abb,描述的是所有以abb结尾的a、b串。先用Thompson结构法画出NFA,然后从初态出发做ε-闭包计算,一步步得到DFA的状态,最后把不可区分的状态合并,得到比较精简的DFA。很多教材答案都给了这个例子的完整过程,关键是每一步的闭包计算不要跳步

这里有个特别容易翻车的点:子集构造法里,必须先求ε-闭包。比如状态集合是{0},求ε-闭包({0})时可能包含多个通过空串转移到的状态;然后对每一个输入符号计算move再求闭包。很多同学图省事,直接把NFA的状态当作DFA的状态来写转换函数,这在ε转移较多或者多初态的NFA里会出错。

DFA最小化的口诀:先划分终态和非终态,再看每个组内状态对每个输入符号的转移是否仍然落在同一个组内,不停划分,直到任何组不能再细分。比如(a|b)*abb的DFA,最终最小化结果是5个状态,再合并一个状态就变成我们常见课本答案里的那个结构。做这类题时,建议画一个状态转移表辅助判断,别只盯着状态图看。

2.2 语法分析:LL(1)文法的判断与预测分析表

语法分析是编译原理习题的重头戏,可以说占了半壁江山。第一类高频题是LL(1)文法判断与分析表构造。判断一个文法是不是LL(1),需要做三件事:

  1. 消除左递归、提取左公因子(如果必要的话)。
  2. 求所有非终结符的FIRST集和FOLLOW集。
  3. 对每个产生式判断:若α =>* ε,则要求FIRST(α) ∩ FOLLOW(A) = ∅;同时对同一非终结符的不同产生式,要求它们的FIRST集合不相交。

有一点必须提醒:千万不要把FIRST集和FOLLOW集搞混。FIRST(A) 是从 A 出发能推导出的第一个终结符集合,FOLLOW(A) 是所有句型中可能紧跟 A 之后的终结符集合。求FOLLOW集时,规则是:先给开始符号的FOLLOW加#;对形如A -> αBβ的产生式,把FIRST(β)中除空串外的东西加入FOLLOW(B);对形如A -> αBA -> αBββ =>* ε的产生式,把FOLLOW(A)加入FOLLOW(B)

笔试时大家喜欢用表格法求集合,我建议至少做一次手写推导,别只依赖表。只有手写过一遍,你才知道哪里容易漏掉循环依赖的传递。

构造预测分析表时,对每个产生式A -> α

  • a ∈ FIRST(α),在表M[A, a]中填入该产生式。
  • ε ∈ FIRST(α),则对b ∈ FOLLOW(A),在表M[A, b]中填入产生式。

如果同一个表项被填入了两个产生式,这个文法就不是LL(1)了。这个“表项冲突”就是判断的关键。

2.3 语法分析:LR(0)、SLR(1)、LR(1)项目集族构造

LR系列的题比LL(1)复杂得多,但套路也更固定。LR(0)项目集构造的核心是“项目+闭包+转移”:

  • 项目就是带圆点的产生式,比如E -> E·+T
  • 对项目集 I,闭包操作就是:如果A -> α·Bβ在 I 中,则把所有B -> ·γ加入 I。
  • 转移操作就是:读入一个文法符号 X,把圆点越过 X 的项目组成新的项目集。

SLR(1)分析表在LR(0)的基础上,用FOLLOW集解决一部分冲突。比如I_k中有项目A -> α·,则归约动作只填在所有FOLLOW(A)对应的终结符上,而不是填满整个行。如果此时表项仍然有冲突,就得考虑 LR(1) 或 LALR 了。

我觉得很多朋友卡在LR这块,并不是不会构造项目集,而是不知道LR(0)的自动机状态图画出来以后该怎么用。其实分析表的横表头是终结符和#,纵表头是状态;移进和归约操作写进对应“状态+终结符”的表项,GOTO表填状态+非终结符。考试时只要按这个表头抄好,不太容易丢分。

有一个经验供参考:考试和面试中,SLR(1)比纯LR(1)出现频率高得多。因为LR(1)项目集的规模可能很大,手算要耗时很久。大部分学校只要求能说明LR(1)和SLR(1)的差异,比如“LR(1)带搜索符、能力更强、表更大”,实际手算题目往往止步在SLR。

2.4 中间代码生成:逆波兰式、四元式、三元式

语义分析阶段最喜欢考的是“将表达式或语句翻译成中间代码”。这里有个基本功——逆波兰式就是后缀表达式,对中缀表达式转后缀,用栈的经典算法就够。但如果是带数组、赋值语句和控制流语句的翻译,最好学会按语法制导定义逐步写。

举个例子,给语句if (a > b) x = a + b * c; else x = a - b;生成四元式,要关心真值出口和假值出口怎么回填。很多同学一看到“回填”两个字就紧张,其实做题时只要把每个四元式的标号编好,jnzjj>等跳转指令的目标标号先用占位符表示,最后再统一填上,基本不会错。

中端到后端的翻译题还有一个大考点:从表达式生成DAG。DAG适合做局部优化,因为公共子表达式会在图里只出现一次。题目一般会让画出DAG,然后从DAG重写中间代码。我自己刷题时总结的思路是:从底层叶子节点向上,重复的节点合并,运算结果作为新的节点挂在父运算下,这样后续生成代码时,类似a + b被计算两次的地方就被自然优化掉了。

3. 词法分析实验:如何用Java手工构造一个词法分析器

3.1 实验目标与整体设计

很多学校的编译原理实验都要求“用高级语言实现词法分析”。搜索热词中出现过“java+编译原理”和“编译原理词法分析实验”,我猜很多同学是被卡在这一步的。其实词法分析实验并不难,核心就是写一个状态转换图的遍历程序:读源码文件,按字符一个一个扫描,遇到关键边界就识别出一个token。

一个完整的词法分析器至少包含这几部分:

  • token类型定义,比如关键字、标识符、常数、运算符、界符。
  • 状态转换逻辑,比如从“字母开头”进入标识符状态,从“数字开头”进入数字状态。
  • 错误处理,遇到非法字符要能记录行号和报错信息。
  • 输出结构,每个token至少应该有类型、值、行号。

我建议用Java来做,主要原因有两条:一是Java的I/O和字符串处理顺手,适合快速原型;二是很多学校的课件和实验平台对Java支持较好,调试也方便。如果你不熟练,用C或Python也完全可以,核心思路是一样的。

3.2 状态图到代码的转换:一个可直接落地的例子

用一个最简单的例子说明状态图怎么翻译成代码。假设我们要识别:

  • 关键字:ifelsewhileint
  • 标识符:以字母开头,后跟字母或数字。
  • 无符号整数:数字串。
  • 运算符:+-*/===
  • 界符:;,(){}

扫描时用一个state变量记录当前状态,一个StringBuilder缓存当前词素,一个char类型记录当前首字符类型。从初态出发,遇到字母就进标识符状态,遇到数字就进数字状态,遇到运算符就单独识别。到达终态或遇到分隔符时,根据状态决定是关键字还是标识符。

一个简化版的Java核心循环可以这么写:

public class Lexer { private String input; private int pos; private int line = 1; public Lexer(String input) { this.input = input; this.pos = 0; } public Token nextToken() { skipWhitespace(); if (pos >= input.length()) { return new Token(TokenType.EOF, "", line); } char c = input.charAt(pos); if (Character.isLetter(c)) { return readIdentifierOrKeyword(); } if (Character.isDigit(c)) { return readNumber(); } return readOperatorOrDelimiter(); } private Token readIdentifierOrKeyword() { StringBuilder sb = new StringBuilder(); while (pos < input.length() && Character.isLetterOrDigit(input.charAt(pos))) { sb.append(input.charAt(pos++)); } String value = sb.toString(); TokenType type = Keyword.isKeyword(value) ? TokenType.KEYWORD : TokenType.IDENTIFIER; return new Token(type, value, line); } private Token readNumber() { StringBuilder sb = new StringBuilder(); while (pos < input.length() && Character.isDigit(input.charAt(pos))) { sb.append(input.charAt(pos++)); } return new Token(TokenType.NUMBER, sb.toString(), line); } // 运算符、界符的处理与错误处理省略 }

这里有一个细节值得留意:识别标识符时,不要在每个字符处都判断关键字,等凑完整整个词素再查表。如果扫描到if的前半段i就急着匹配关键字,会把ifx拆成ifx两个token,那就不对了。这也是词法分析实验最常见的bug之一。

3.3 实验中的边界与状态处理要点

下面几个点是我踩过坑之后总结出来的,写实验报告时也可以重点提:

  • 空白字符处理:空格、\t\n要跳过,但遇到\n行号要加一。很多同学报错信息里行号总是差几行,就是因为忘了在跳过空白时累计行号。
  • 最长匹配:识别==时不能看到第一个=就返回一个赋值号token,必须再看下一个字符。如果下一个也是=,就返回等于号,否则返回赋值号。
  • 错误恢复:遇到@#这样的非法字符,不要直接崩溃,记录错误后继续扫描,这样做测试的时候能一次拿到多个错误信息。
  • 文件结束:别忘了输出EOF。很多自动评测脚本会检查是否输出了EOF,漏掉会扣分。

我写实验时还会把每个token打印成(类型, 值, 行号)的格式,比如(KEYWORD, int, 1),这样测试数据一看就知道哪里错了,也方便老师检查。

3.4 如何准备一份能演示的实验与测试用例

如果要交实验报告,建议准备三类测试样例:

我不会只准备一个“全对”的正向样例,因为老师很可能临时换输入文件来测你的程序,如果只支持特定格式,一测就露馅。我一般会准备:

  1. 一个正常的demo程序,包含变量声明、算术表达式、循环语句。比如:
int x = 10; while (x > 0) { x = x - 1; } if (x == 0) { println(x); }
  1. 一个“边界样例”,包含==>=<=/* 注释 */(如果你实现了注释跳过)以及各种行分隔符。

  2. 一个“错误样例”,包含非法字符@_$、数字开头后接字母(如123abc)、未闭合的注释等,看看程序能不能报出错的行号。

我在测试时很容易发现,数字和字母连在一起(比如123abc)如果不加处理,会被识别成数字123然后跟着标识符abc,但很多词法规范里这应该是一个非法token。这是一个很能体现细节处理的测试点。

4. 常见错误与排查技巧实录

4.1 求FIRST集和FOLLOW集时最容易掉的坑

抛开复杂文法不谈,FIRST集和FOLLOW集求错多半是下面几个原因:

  • 先求FIRST集没彻底,回头求FOLLOW集时漏加空串传递。比如A -> B,B -> ε,你以为FIRST(A) 已经考虑完了,但FOLLOW传递时其实依赖“B能否推出空串”。
  • 把最能反映“可以推出空串”的情况漏掉,导致后续判断LL(1)失败
  • FOLLOW集合初值没加#。开始符号的FOLLOW集一定包含#,这是很多人丢分的地方。

我自己的检查方法是:求完一个FOLLOW集之后,手动代入几个简单句型验证。比如文法E -> TE',从最左推导得到E => T E',那么FOLLOW(E')肯定包含FOLLOW(E),如果结果里没有,一定哪里求错了。

4.2 LR分析表构造中的移进、规约冲突与解决思路

LR自动机状态多了以后,有时候自己都分不清是从哪个状态跳过去的,表更是一填就乱。我建议按三步走排查

  1. 先检查闭包计算:项目集I里只要圆点后是非终结符,就要把所有对应产生式加进去,别漏。
  2. 再检查转移表:对每个文法符号X,只移动圆点紧跟着X的项目。有的同学会把所有项目都移动一遍,这是错的。
  3. 最后检查分析表:如果同一状态在某个终结符下既有移进又有规约,说明有移进-规约冲突。先用SLR的FOLLOW规则排除,如果排除不了,就不是SLR(1)文法。

举一个经典的冲突例子:文法S -> L = R,S -> R,L -> * R,L -> i,R -> L。它的LR(0)项目集里会出现在某个状态下既有S -> L· = R的移进项目,又有R -> L·的规约项目,而FOLLOW(R) 是否包含=决定了它是否为SLR(1)文法。这类题在哈工大和吉大多年习题里都出现过,做会了相当于白拿10分。

4.3 调试实验代码时的高频bug:死循环、错位、吞字符

词法分析实验的代码bug其实非常有限,最常见的就是扫描指针推进条件写错,导致死循环或漏字符。

比如我在写readNumber时,如果循环里没有在pos++之后再break的边界条件,读到最后一位数字时会因为pos < input.length()不成立而正常退出,这还好;但如果用while (Character.isDigit(input.charAt(pos)))并且忘记判pos < input.length(),遇到文件结尾就会抛字符串越界异常。很多同学在这里绕很久,给代码加了一堆复杂的处理,其实只要在while条件里加上位置判断就行。

另外,不要在一个方法里做太多事。比如readIdentifierOrKeyword里既要做关键字判断又要处理错误,代码一复杂就容易把token的开始位置搞丢。我的习惯是一个方法只做一件扫描的事,token的类型判断单独用一个lookup函数去查。

调试时建议打开输入文件,对着源码一行一行模拟一遍程序运行。如果发现某个token的value和行号对不上,大概率是在处理换行和跳空格时多跳或少跳了一个字符。

4.4 习题答案和课件资源的正确使用姿势

关于“编译原理第三版答案”“吉林大学编译原理”“哈尔滨工业大学编译原理课件讲义”这类热词,我想多说两句。答案和课件确实有用,但关键是怎么用。

习题答案最大的价值是核对过程和最终结果,不是背诵。我见过太多人把答案抄了一遍,到考试换一个数字就不会了。你要做的是:拿到一道题,自己先做,做完再对答案;如果答案跟你的不一样,不一定是答案错,可能是你的FIRST集或项目集里少一步。这时候拿着答案反推自己的步骤,能找到自己知识体系中真正的盲区。

课件讲义方面,吉大和哈工大的课件之所以被搜得多,是因为它们的习题风格偏工程化,很多题目直接来自真实编译器构造场景。比如词法分析部分,哈工大的课件里会要求用正规式描述C语言的关键字和标识符,这种题比普通教材里的(a|b)*这类示例更贴近实际。如果你只是应付考试,用学校指定的教材就好;如果想深入一点,不妨搜一份其他学校的讲义来对照,特别要注意它们对“语法错误处理”和“中间代码生成”的处理方式,往往比教材写得更容易理解。

5. 编译原理面试题的高频问法与答题思路

5.1 编译器整体结构相关的问题

编译原理在面试中分为两种问法:一种是直接问基础概念,比如“编译器有哪些阶段”或“编译器和解释器的区别”;另一种是结合项目经历,比如“你写的解释器是如何处理变量作用域的”。

前一类问题,你只需要把阶段列出来,再提一嘴“前端、中端、后端”的划分即可。但如果你只背到这一步,面试官很容易加问:“那词法分析和语法分析谁先谁后?如果遇到非法token,编译器应该在哪里报错?”这个问题的关键点是:词法分析阶段就会报出“非法字符”或“无法识别的token”,而不是等语法分析。很多人误以为所有错误都是语法分析阶段报的,实际上词法阶段的错误更底层。

另一类高频问法是“为什么需要中间代码”,它的标准答案是“中间代码与机器无关,便于不同目标平台复用,便于做优化”。我建议你补充一个具体例子,比如a = b + c在x86、ARM、RISC-V上指令完全不同,但中间代码可以统一成一条四元式(+, b, c, a),这样前端只需做一次,后端再接各个平台的代码生成器就行。

5.2 语法分析理论相关的经典问答

语法分析这一块,面试官特别喜欢问“LL和LR的区别”以及“为什么LR比LL能力更强”。

  • LL是自上而下分析,从左到右扫描输入,最左推导;LR是自下而上分析,从左到右扫描输入,最右推导的逆过程。
  • LL(k)需要向前看k个符号,靠预测分析表驱动;LR依靠状态栈和项目集,能处理更多的文法。

如果用一句话回答面试官:“LR分析器相当于在语法树生成时‘拼装’节点,它能看到更多上下文,所以能识别更大的文法类。”这个回答比单纯背定义要好记,也更容易让面试官觉得你真懂。

还有一个常问的题目是“如何消除左递归”。记住统一的转换方法:对直接左递归A -> Aα | β,可以改写为A -> βA'A' -> αA' | ε。手写题里最常考的是算术表达式文法转LL(1):

E -> E + T | T T -> T * F | F F -> (E) | id

转成:

E -> T E' E' -> + T E' | ε T -> F T' T' -> * F T' | ε F -> (E) | id

这个例子在面试里出现的频率非常高,建议每个细节都背熟,包括FIRST集、FOLLOW集、预测分析表都要能手推。

5.3 项目实战类问题的回答技巧

如果你的简历里写了“手写过一个简单编译器”或“写过一个计算器解释器”,面试官大概率会追问:“你的词法分析是怎么实现的?遇到语法错误怎么报错?”

这时候要把话题往你熟悉的方向引。比如你说用Java写了词法分析器,可以提到状态转换图和DFA的思想,但不要说太多理论;面试官更想听的是你如何处理多行注释、如何处理嵌套括号、如何报告出错的行号。我在回答这类问题时通常会给一个迷你例子:假如输入是int x = @;,你的词法分析器输出什么?我会说:@是非法字符,在1号行报错,随后继续扫描;,输出界符token。这样能让面试官瞬间知道你掌握了错误恢复的思想,而不是只会跑通理想样例。

5.4 面向面试的快速复习清单

如果面试就在一周内,我建议按这个清单过一遍:

  • 能口述编译器七阶段(词法、语法、语义、中间代码、优化、目标代码、符号表管理等)并解释前端与后端划分。
  • 能手写消除左递归、提取左公因子,并说明为什么要这样处理。
  • 能手动构造一个简单文法的LL(1)预测分析表,并判断是否有冲突。
  • 能解释LR(0)项目集、SLR(1)的求解过程,并描述一个冲突例子。
  • 能把表达式a + b * c翻译成四元式序列。
  • 能解释DAG优化为什么能消除公共子表达式。
  • 能讲清楚词法分析器里状态转换图如何映射成代码。

这七条覆盖了编译原理面试题里超过八成的内容。剩下的和“运行环境”“垃圾回收”相关的题,往往在校招后端岗出现的几率不大,但如果你想面的是编译器或数据库内核相关岗位,那就得再深入一层了。

6. 面向考试和实验的复习提纲与资源建议

6.1 结合教材与讲义快速定位知识盲区

如果你用的是编译原理第三版这本国产经典教材,章节结构基本是这样的:前面讲文法和词法,中段讲语法分析,后段讲语义分析和代码生成。我特别推荐把每章的课后习题对照“哈工大课件”里的例题一起看,因为课件里会有大量课堂上老师手写的推导过程,虽然排版不一定精美,但每一步都写得比教材详细。

复习时不要试图把整本书读一遍,我建议直接做题。每章挑5-8道代表性题目,第一遍不查资料硬做,卡住了标记;第二遍集中查课件和答案解决标记的题;第三遍重做所有错题。三轮下来,我对同一个知识点的理解深度和第一遍完全不一样。

6.2 一个可以复用的习题刷题模板

我自己刷编译原理习题时,会用一个简单表格记录每道题的状态:

题号考察知识点初始掌握度(1-5)错误原因复做结果
3.7NFA转DFA2ε-闭包漏了状态通过
4.12LL(1)分析表3FOLLOW集少#通过
5.9LR(1)项目集1闭包不完整重做

这样能看出自己的薄弱点集中在哪,是词法分析还是语法分析。如果是某个知识点反复错,就去针对性找课件里的例题,再找教材的同类题加练。这个方法不用什么额外工具,一张纸一支笔就行,但效率提升非常明显。

6.3 期末突击与考研复习的侧重点差异

期末突击和考研复习的最大区别是:期末考的范围小、题型固定,重点在于“熟练”;考研范围广、题目深,重点在于“原理理解”。

期末突击我的经验是:先搞定词法分析中的DFA最小化和语法分析中的表格构造,因为这两类题最容易出大题,而且只要步骤完整,得分率很高。如果只有三天时间,我甚至建议直接刷近三年的期末真题,把每种题型做个两遍,及格甚至良好难度不大。

考研则不同。考研的编译原理题目往往会把“文法的二义性证明、SLR(1)分析表构造、语法制导翻译与中间代码生成”综合在一道大题里。这时候就不能只背步骤,还要理解每一步为什么这样设计。比如构造SLR分析表前,最好先判断文法有没有二义性、有没有冲突;生成四元式前,最好先画一下语法树。这些前置操作才是解题的关键。

6.4 关于实验报告的加分项

如果你还需要交实验报告,我有几个比较讨巧的建议:

  1. 写出“设计思路与状态转换图”,哪怕实现时用的是循环加判断,流程图也会让老师觉得你有理论支撑。
  2. 给出“测试用例与输出结果”的对比表,用输入源码、预期token流、实际token流做对照,能体现你的严谨性。
  3. 写一段“遇到的bug与解决方案”。比如我在做词法实验时遇到过一个“单行注释结束判断错误”的问题,最后通过提前预读一个字符解决。把这些写进报告,老师往往会给出不错的分数。

做实验报告不是要把源代码贴满,而是要体现“你理解程序为什么这样写”。编译原理这门课,特别讲究这个。

7. 实操中的小技巧与我的个人体会

最后分享几个我复习完编译原理、做完词法分析实验后印象很深的小经验。

第一点,一定要动手画状态图和LR自动机。不管脑子里怎么想,画图能帮你把状态转移看得更清楚。画LR自动机的时候,我习惯用不同颜色标出有关闭包的边和普通移进的边,考试时虽然不能用彩色笔,但手画的层次感好了很多。

第二点,做题时把第几步、考什么知识点写在草稿纸最上方。比如“此题考DFA最小化”,这个动作看起来无聊,但当你翻回头检查时,能立刻发现问题出在哪个阶段。我当初做NFA转DFA总错,就是因为懒得想步骤,直接瞎算。

第三点,面试时如果被问到不熟的题目,不要硬编。编译原理的概念很多,不懂装懂很容易被追问穿帮。你可以说“这块我了解得不够深,我目前的理解是……”,然后依然把你知道的框架、简化版思路说出来。面试官其实更看重你面对陌生问题时的分析方式,而不是背诵能力。

第四点,代码实现时,状态机的“状态”最好定义为枚举或常量。比如STATE_ID,STATE_NUM,STATE_OP,别直接用数字0、1、2。因为状态一多,数字很容易在写条件时弄混。用枚举以后,读代码和调试都舒服很多。

这些经验并不高深,但都是我实际做题、写代码、面试过程中踩坑得出来的。编译原理确实难,但它也是最值得花功夫的课程之一。很多人觉得编译器只有搞底层的人才会用到,实际上你写SQL、写正则表达式、用JSON解析库、甚至分析日志格式,背后都是这些思想的影子。

如果你正在复习这门课,我的建议很简单:别怕枯燥,把题一道一道做下去。做错没关系,错题才是最能提升的地方。坚持刷完一批典型题后,你会发现自己看代码的方式、理解程序设计的方式,都会变得不一样。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询