非确定有限自动机
彻底理解NFA的形式定义、子集构造证明与DFA等价的全部细节
按 空格/→ 演示下一步
全部页面点击任意一页,跳回舞台从这页播放
非确定有限自动机
彻底理解NFA的形式定义、子集构造证明与DFA等价的全部细节
什么是「非确定」
上一页我们给非确定有限自动机下了定义,但它名字里的「非确定」到底什么意思?先放下术语——想象你走进一个迷宫,每到岔路口都可以分身,同时走所有方向。
每个岔路同时派分身,任何一条通到出口就算成功
NFA vs DFA 对比
NFA 与 DFA 等价但本质不同——一个并行探索,一个线性推进。
- 同一输入可触发多条转移
- 并行维护多个活跃状态
- 可用 ε-转移不读字符跳转
- 同一字符可对应多个目标
- 同一输入只能选一条转移
- 任意时刻只有一个当前状态
- 必须消耗字符才能转移
- 每个字符对应唯一目标
NFA的形式化定义
前面我们看清 NFA 比 DFA 「松」在哪里:同一输入可去多个状态、某些转移可以不写。这份「松」落到纸面,就是五元组 M=(Q,Σ,δ,q0,F)。下面逐个拆开分量。
Q 是全部路口、Σ 是合法转向、δ 给出路口读信号后可能的多个去处、q0 是出发路口、F 是目的路口集合
NFA的状态转移图表示
看图先找三要素:无源箭头(起点)、双圈(接收)、带符号的箭头(转移)。
ε转移的概念
看 NFA 转移图时,每条边上都标着输入字符。但 NFA 还有种特殊箭头——上面什么都不标。这不是漏写,而叫 ε 转移:不用读任何字符就能切换状态。
推开连通门就到隔壁房,不走走廊、不花体力——对应「不读字符、状态已切换」
带ε转移的NFA示例
圆点标亮 NFA 当前所处的状态集合,按动画步序逐次点亮。
ε闭包与状态可达
上页我们看到ε箭头不消耗字符。站在q时,其实可以沿ε箭头免费滑到所有能到的地方。这个「免费可达的全部去处」,就叫ε闭包。
在航站楼可免费换乘所有可达航站楼,不消耗登机次数(即输入字符)
为什么要扩展转移函数
δ 一次只能处理一个字符,但判断「整个字符串是否被接受」需要读完整串。能不能把 δ「打包升级」,让它一次处理整串?
一页一页翻,每翻一页页码可能跳;读完一章要记下所有可能停下的页
δ*的递归定义
把 δ 从「读一个字符」升级到「读整条字符串」,靠两条递归规则搭出来。
扩展转移函数的计算示例
用 Python 把 δ* 的递推定义直译成代码,在字符串 aab 上逐字符演示。
δ(q,a) 返回集合是 NFA 的非确定根源(L4);代码把 δ* 递推定义逐行落地:基础步(L16)给种子、归纳步(L20)做并集;最终在 aab 上逐字符输出 δ* 中间值(L28)。
NFA与DFA的等价性
我们能用扩展转移函数 δ* 跑通 NFA 了。但 NFA 多出的那份『非确定性』,真的能让它接受 DFA 接受不了的字符串吗?这一页给反转:不会。
每读一字符按 NFA 转移规则更新整张清单;只要清单里出现 NFA 终态,就算命中
子集构造法步骤
把NFA的状态集合"折叠"成DFA状态——核心算法五步走。
NFA转DFA实例
左:原NFA接受ab结尾;右:子集构造得到的等价DFA。
NFA与DFA优缺点对比
NFA看起来更灵活强大,但表达能力上两者完全等价。这一页只看工程取舍。
- 转移函数多值,同一输入可到多个状态
- 支持 ε 转移,构造直观灵活
- 状态数通常更少,描述更简洁
- 模拟需回溯或并行追踪,效率较低
- 转移函数单值,每输入唯一下一状态
- 无 ε 转移,结构受正则语言约束
- 最坏状态数指数膨胀(2^n)
- 可单线程顺序扫描,无需回溯
正则表达式与NFA的联系
前页我们把 NFA 翻成了 DFA。现在请出正则表达式——一串符号,像 a|b、(ab)*。它和 NFA 长得不一样,但有一个深刻结论:两者描述语言的能力完全等价,可以双向无损翻译。
乐谱、录音、MIDI 描述同一曲子,形态不同但可无损互转
Thompson构造法
Thompson 构造法按正则语法递归拆分,每类操作对应一条规则,最终拼出整个 NFA。
文本搜索NFA应用示例
纵轴是模拟器一步:每读完一字符,把当前可达集R先做δ再做ε闭包,得到下一轮R。
核心要点回顾
- ✓NFA与DFA识别能力等价,非确定性不增表达能力
- ✓子集构造最坏情况下指数膨胀,换取构造简洁
- ✓ε转移不增表达力,但显著提升建模灵活性
- ✓δ*以递归方式统一单步与多步的语义
- ✓NFA是连接正则表达式与自动机的桥梁
课后思考
先自己想,再看参考答案;问题没有标准答案,重点是思考路径。
参考答案因为ε闭包只是把所有'无输入可达'的状态预先打包,并未给NFA增加任何DFA做不到的判定能力。
参考答案Thompson构造能把正则直接拼成NFA,编译期成本几乎为零;模拟时一次扫描并行尝试多条路径,避免子集构造的指数膨胀。
参考答案关键是构造一个让'部分匹配的可能位置'指数增长的语言——比如(a|b)*a(a|b)^k匹配倒数第k位是a的串,DFA必须记住最近k位才能判定。