news 2026/11/4 7:43:49

编译原理试卷拆解:从正规式到DFA与四元式中间代码

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
编译原理试卷拆解:从正规式到DFA与四元式中间代码

简介:常州工学院编译原理试卷A是一份面向计算机专业本科生复习与教师命题参考的课程试卷,覆盖正规表达式与最简DFA构造、逆波兰表示、文法二义性判定及语言描述、LL(1)文法的First/Follow集与预测分析表构造,以及条件语句翻译为四元式序列等核心考点。压缩包内共1个doc文件,约55KB,为单份试卷文档,适合打印练习或课堂测验使用。该资源已有393人学习,可作为期末复习、考研专业课梳理和编译原理阶段测试的对照材料。通过逐题演算,读者可系统检查词法分析、语法分析与语义分析等环节的掌握情况,并借助完整题目训练正规式设计、分析表构建和中间代码生成能力。

1. 翻一份编译原理试卷,多数人盯错了地方

翻一份编译原理试卷,多数人会先盯着最后那道 15 分的语句翻译题,觉得前面那些正规表达式、FIRST/FOLLOW 只是套公式的计算活。反过来才对:那些看起来像算术题的部分,才是整条编译前端流水线的地基。一份典型的期末卷面通常考五件事——正规表达式到最简 DFA、逆波兰与四元式、文法二义性、LL(1) 预测分析表、条件语句的中间代码翻译——它们正好串起词法识别、语法判定、分析表构造、中间代码生成这条主干。搞清这五步之间谁依赖谁,比背下每种题的解法有用得多。下面就把这份试卷拆开,讲清每道题背后在考什么机制、为什么必须这么设计,以及手工推导时最容易在哪一步翻车。

2. 正规表达式到最简 DFA:子集构造为什么能消灭“不确定”

2.1 从题目文字到正规式:1(0|1)*11 漏掉了什么

试卷第一题问的是:在字母表 {0,1} 上,写出“不以 0 开头但以 11 结尾”的所有字符串的正规表达式。直觉会写成1(0|1)*11——第一个字符固定是 1,中间任意,最后两个是 1。这个式子确实只生成长度不小于 3 的串,问题在于它把11自己漏掉了:这个串以 1 开头、以 11 结尾,却没法从1(0|1)*11里推出来,因为它要求开头那个 1 和结尾两个 1 是三个不同的位置。

正确的写法是11 | 1(0|1)*11,或者等价地写成1(1 | (0|1)*11):第一个 1 之后,要么直接接一个 1 收尾(总长 2),要么中间塞任意串再接 11。

同一题的第二问“包含 01 子串”就直白得多:(0|1)*01(0|1)*。它和第一问的差别值得留意——第一问约束的是“首字符”和“尾字符”两个端点,端点约束在写正规式时最容易丢;第二问约束的是“存在某个子串”,用前后各一个(0|1)*夹住就行。

正规式的写法从来不唯一,11|1(0|1)*11和1(1|(0|1)*11)描述的是同一个语言。所以出题人说“最简”,考的其实不是式子长短,而是后面那个 DFA 的状态数最少——这是两个不同的最简概念。

2.2 Thompson 构造与 ε-闭包:不确定性是从哪来的

正规式到 DFA 之间通常垫一步 NFA。常见的实现思路是 Thompson 构造:给每种基本正规式配一个小片段,连接靠首尾相接,选择靠新起一个状态分叉出两条 ε 边,闭包靠把片段的首尾用 ε 边接回去。整个过程是纯结构递归,NFA 的状态数和正规式长度成线性关系,这是它最大的价值——不会爆炸。

代价是引入了 ε 边。ε 边意味着“不吃任何输入也能走”,于是 NFA 在某一时刻不再处于单一状态,而是同时处于一个状态集合。这就是“不确定”的准确含义:不是随机,而是并行的多条路径。

处理它的工具是 ε-闭包:ε-closure(S)等于 S 本身,加上从 S 出发只沿 ε 边走能到达的所有状态。这个操作必须做传递闭包,而不是只看一步——A 沿 ε 到 B、B 沿 ε 到 C,那么 C 也在闭包里。手工推导时最常犯的错就是只展开一层,状态一多就漏。

2.3 子集构造:把一个状态集合当成一个新状态

子集构造是这套流程里最漂亮的一步:既然 NFA 跑起来本来就是一个状态集合,那就干脆把这个集合直接定义成 DFA 的一个状态。转移函数写成 δ(S, a) = ε-closure(∪ move(s, a)),其中 s 遍历 S 里的每个状态。代码大致长这样:

def subset_construct(nfa_states, alphabet, eps, move, start, finals): """eps: {state: set(state)} 表示 ε 边;move: {(state, ch): set(state)} 表示符号边""" def eclose(states): stack, seen = list(states), set(states) while stack: # 栈式传递闭包,不是只展开一层 s = stack.pop() for t in eps.get(s, ()): if t not in seen: seen.add(t) stack.append(t) return frozenset(seen) # frozenset 才能当 dict 的键 start_set = eclose({start}) dfa, work, trans, dfa_final = {start_set: 0}, [start_set], {}, set() while work: cur = work.pop() if cur & finals: # 集合里只要含一个 NFA 接受态就是接受态 dfa_final.add(dfa[cur]) for ch in alphabet: nxt = eclose(set().union(*[move.get((s, ch), set()) for s in cur])) if not nxt: # 空集不建状态,留成“不完整 DFA” continue if nxt not in dfa: dfa[nxt] = len(dfa) work.append(nxt) trans[(dfa[cur], ch)] = dfa[nxt] return dfa, trans, dfa_final

eclose用显式栈做闭包,避免递归层数受状态数影响;frozenset是因为集合本身不可哈希,不能当状态编号字典的键;if not nxt: continue那一行是刻意的取舍——空集不建状态,得到的是一个“不完整 DFA”,遇到非法输入直接报词法错误。这在真实词法分析器里是标准做法,但到了最小化那一步就必须补一个死状态,否则等价类划分会算错,这是后话。

拿第一题验证一下。起点读 1 进“已合法开头”,读 0 直接进死路;读到第一个 1 之后,再看一个 1 就进入接受态。最后得到的转移表是:

状态读 0读 1含义
→q0dq1起始,还没读到开头那个 1
q1q1q2开头合法,但末尾不是 11
*q2q1q2末尾两位是 11,接受
ddd以 0 开头的非法串

q2 读 1 仍停在 q2,因为末尾两位还是 11;读 0 退回 q1,因为末尾变成了 10。q1 的语义是“已经合法开头,但当前不满足结尾条件”,所以它读 0 停在原地、读 1 前进。死状态 d 是不可省的,它承载了“这个串已经废了”这个信息。

2.4 最小化:状态为什么可以合并,又凭什么不能再少

最小化的本质是找一个等价关系:两个状态等价,当且仅当对所有可能的输入后缀,它们要么都接受、要么都不接受。这个定义是“对所有后缀”的全称量化,所以不能用枚举验证,只能用归纳法证明——Hopcroft 划分就是干这个的。

初始划分只有两块:接受态集合和是否定态集合。然后反复检查,如果同一块里的两个状态读到某个字符后落进了不同的块,就把它们拆开,直到这一轮不再产生新块。道理很直白:如果一个状态读 a 后能接受、另一个读 a 后不能,它们就不可能等价;这条判据一路归纳下去,最终得到的就是“任意长度后缀行为一致”的划分。

手工验证最小性有个笨但可靠的办法:对每两个状态各找一条区分串。在上面那张表里,q1 和 q2 用空串就能区分(一个接受一个不接受);d 和 q1 用1区分(q1 读到接受态,d 读到 d,而 d 再接什么都出不来);q0 和 d 用1区分(q0 读 1 到 q1,再读 1 接受;d 读 1 还是 d)。四类状态两两可区分,所以 4 个状态已经是最少。

这一步在纸面上是纯计算,在工程上却是实打实的性能问题。词法分析器一般实现成状态转移表加一个大 switch 或者二维数组,表里的每个状态都要占一行,状态数从几十降到十几个,表就小一半,缓存也更友好。

3. 逆波兰与四元式:中间代码为什么必须“无括号”

3.1 调度场算法:运算符栈里到底存了什么

第二题要求把A+B*(C-D)+E/(C-D)写成逆波兰表示。逆波兰(后缀式)的核心性质是:只要知道每个运算符要几个操作数,从左往右扫一遍就能算完,永远不需要括号,也不需要回退。

把中缀转后缀的标准做法是调度场算法。它维护两个容器:输出队列装已经排好序的操作数和运算符,运算符栈装“还没确定什么时候该出栈”的运算符。一条运算符能不能出栈,取决于栈顶运算符的优先级是否不低于当前运算符——>=这个等号就是左结合性的落点,因为同级运算符要先出栈,才能保证a-b-c被算成(a-b)-c:

def to_rpn(tokens): prec = {'+': 1, '-': 1, '*': 2, '/': 2} out, ops = [], [] for t in tokens: if t.isalnum(): out.append(t) # 操作数直接进输出队列 elif t == '(': ops.append(t) # 左括号当哨兵,优先级视为最低 elif t == ')': while ops and ops[-1] != '(': out.append(ops.pop()) ops.pop() # 弹出并丢弃左括号 else: while ops and ops[-1] != '(' and prec[ops[-1]] >= prec[t]: out.append(ops.pop()) # 左结合用 >=,右结合改成 > ops.append(t) while ops: out.append(ops.pop()) # 收尾:把栈里剩的全部吐出 return out

prec表决定了运算符优先级,左括号当哨兵是为了挡住“(还没有配对时被弹栈”的情况,循环末尾的 while 负责处理表达式结束时栈里残留的运算符。如果语言里有右结合运算符(比如幂运算或者赋值),只需要把>=放宽成>,逻辑不变。

对A+B*(C-D)+E/(C-D)走一遍,得到A B C D - * + E C D - / +。注意两个加法是左结合的,第一个+必须在第二个+入栈之前出栈,否则表达式就变成了A+(B*(C-D)+(E/(C-D))),虽然这里加法结合性不影响结果,但对减法和除法就是致命错误。

后缀式还有一个经常被忽略的性质:它的最大栈深度等于表达式语法树的深度。这个数字在寄存器分配里直接对应需要的临时寄存器数量,Sethi-Ullman 编号就是为了求这个最少寄存器数而设计的。

3.2 三元式与四元式:序号引用为什么是后悔药

同一道题还要写三元式或四元式序列。三元式的格式是(op, arg1, arg2),结果不写出来,而是用这条指令的序号隐式表示——第 (1) 条的结果就叫 (1)。四元式多一个字段:(op, arg1, arg2, result),结果显式写成 T1、T2 这样的临时变量。

这个多出来的字段是设计上的关键分水岭。三元式的引用全部是“第几条指令”,一旦中间代码要插入删除、重排顺序,所有引用都得跟着改,改漏一处就是错。四元式用临时变量名引用,指令随便挪位置,引用关系不变。做过指令调度或者局部优化的人都明白这个差别意味着什么:优化本来就是不停地增删和搬家,如果每搬一次都要重算一遍引用,优化器根本没法写。

A+B*(C-D)+E/(C-D)的四元式序列是:

序号oparg1arg2result
1-CDT1
2*BT1T2
3+AT2T3
4/ET1T4
5+T3T4T5

第 4 条直接用 T1,是手写时主动做的公共子表达式复用:(C-D)算一次就够。而如果让程序从后缀式机械生成,就会得到六条指令、算两遍C-D——这正好说明“有后缀式不等于有优化”,公共子表达式消除是单独一趟要做的活。

3.3 if-then-else 的四元式:回填与短路求值

第五题要把if x>0 and y>0 then z:=x+y else x:=x+2; y:=y+3翻译成四元式。这题真正考的不是表达式翻译,而是控制流的跳转目标怎么定。

麻烦在于:生成第一条指令的时候,你根本不知道 else 分支从第几条开始。机器码是顺序生成的,未来的地址在现在这一刻还不存在。常见的解法是回填:先在这些位置填一个占位符,同时把“所有等着填这个目标的地方”串成一条链记下来,等目标地址确定,顺链子一次性补上。条件为假就跳走的jf

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 7:43:19

基于Claude模型的智能虾塘养殖系统实践

1. 项目背景与核心价值去年夏天我在水产养殖场第一次见到自动化投喂系统时,就被这个结合了IoT和AI的解决方案深深吸引。传统养虾行业长期面临投喂不精准、人力成本高、水质难控制等痛点,而"OpenClaw人人养虾"项目正是用Anthropic的Claude模型搭…

作者头像 李华
网站建设 2026/11/4 7:42:26

Archery 使用手册:SQL 审核工单、回滚与 binlog 排查实战

简介:《archery使用手册》是一份面向SQL审核、查询平台使用者和MySQL运维人员的实操文档,围绕Archery的审核规则、工单流程、SQL优化与实例管理展开,适合开发、测试及DBA在数据库规范落地和日常排障中参考。压缩包共1个doc文件,大…

作者头像 李华
网站建设 2026/11/4 7:42:19

系统集成项目中的网络规划与设备选型实战指南

1. 系统集成项目管理的核心挑战在IT基础设施建设项目中,系统集成往往是最具挑战性的环节。记得去年负责某大型制造企业的ERP升级项目时,网络设备与原有生产系统的兼容性问题就让整个团队熬了三个通宵。这种痛点的根源在于:系统集成不是简单的…

作者头像 李华
网站建设 2026/11/4 7:41:26

SQL Server新手入门:从安装到建表查数据的完整实践地图

简介:这是一份面向数据库初学者与SQL Server入门者的系统性学习笔记,帮助读者快速掌握关系型数据库核心概念与SQL Server管理实践。内容覆盖数据库对象操作(CREATE/DROP/ALTER)、C/S架构原理、系统数据库功能(master/m…

作者头像 李华
网站建设 2026/11/4 7:41:25

橡胶制造业能源管理系统:MyEMS架构与能效优化实践

1. 项目背景与行业痛点橡胶制品制造业作为典型的高能耗行业,其生产过程中存在三大能源管理痛点:首先,密炼、硫化等核心工序的能耗占生产成本高达30%-40%,但多数企业仍依赖人工抄表统计,数据滞后严重;其次&a…

作者头像 李华
网站建设 2026/11/4 7:41:24

Redis高并发秒杀系统架构设计与实战优化

1. 优惠券秒杀的业务挑战与技术选型去年双十一期间,我们电商平台的200元无门槛优惠券在3秒内被抢购一空,服务器却因为瞬时高并发险些崩溃。这种秒杀场景对系统架构提出了极高要求:既要保证超卖问题零容忍,又要应对突发流量冲击。R…

作者头像 李华