<编译原理> 正则表达式转NFA算法

本文借助transitions库基于逆波兰表示法(Reverse Polish Notation,RPN)实现了正则表达式转NFA,并画出相关的状态转移图。


∅ Transitions库介绍

好栗子胜千言。

Here is an example:

from transitions.extensions import GraphMachine

# The states
states = ['solid', 'liquid', 'gas', 'plasma']

# The transitions
transitions = [
                {'trigger': 'melt', 'source': 'solid', 'dest': 'liquid'},
                ['evaporate', 'liquid', 'gas'],
                ['sublimate', 'solid', 'gas']
              ]

# Initialize
machine = GraphMachine(states=states, transitions=transitions, initial='liquid', title='my STD')
machine.add_transition('ionize', 'gas', 'plasma')
machine.get_graph().draw('my_state_diagram.png', prog='dot')

Here is the output:

上面的栗子基本包含了本文所用到的所有操作,另外说明几点:

  1. transitions列表中既可以是字典,也可以是列表,甚至是两者的结合,但都需要按照[trigger , source , destination]的顺序写
  2. add_transition()函数增加转移函数,输入参数也需要按照上述顺序
  3. GraphMachine函数中的initial参数指定初始状态,并标注为不同的颜色,然而我们在画状态转移图时标注的往往不是初始状态而是接收状态,因此我一般将initial参数赋为接收状态
  4. 更多使用方式请参见transitions/README.md

∇ 逆波兰表示法RPN

逆波兰表示法,是一种是由波兰数学家扬·武卡谢维奇于1920年引入的数学表达式形式,在逆波兰记法中,所有操作符置于操作数的后面,因此也被称为后缀表示法。逆波兰记法不需要括号来标识操作符的优先级。

首先阐述一下逆波兰表达式是如何得到的,我们采取如下算法:

  1. 需要两个栈,一个存数,一个存符号,下面分别称之为数栈符号栈

  2. 顺序读入中缀表达式(正常的运算式)

  3. 遇到数(完整的数,不是逐位)时压入数栈

  4. 遇到运算符时,首先从符号栈栈顶开始检查,如果栈顶符号优先级>(=)读入运算符,则将栈顶符号弹出压入数栈中,然后检查符号栈下一个符号,循环往复,直到符号栈栈顶符号优先级<读入运算符时,将读入运算符压入符号栈

    ⚠️由于运算符基本是左结合的,因此我们可以认为左边(符号栈内)的运算符优先级>右边(读入)的同级运算符

  5. 当读入【(】时,直接压入符号栈,且只有读入【)】时才可以弹出

  6. 当读入【)】时,将符号栈内的符号依次弹出压入数栈,直到遇到第一个【(】,注意【(】直接弹出不压入数栈

  7. 读完运算式后,依次弹出符号栈并压入数栈,那么数栈【栈底→栈顶】即为逆波兰表达式

那么逆波兰表达式有什么优点呢,既然是关于运算式的那必然涉及到计算。对于逆波兰表达式,我们只需顺序读入,遇到数字压入栈中,遇到运算符则弹出栈顶的两个数字直接计算再压入栈中,最终的栈里只会剩余一个数,即为运算结果。


ℵ Regular Expression 2 NFA

这里的正则表达式我们认为仅有三种符号:Kleene闭包【*】,连接运算符【·】(一般省略),或运算符【|】(三者的优先级依次降低)。那么正则表达式转为NFA的规则基本可以由下图概括,

基本思想是先将正则表达式转为逆波兰表达式,然后用开始节点和最终节点的编号对[start node num , end node num]来表示正则表达式的运算结果,在用逆波兰表达式计算的同时生成列表transitions,最后绘制相应的状态转移图。


C☺DE

from transitions.extensions import GraphMachine

rpn = []
priority = {'|': 0, '*': 2, '.': 1, '(': -1}

# 补全正则表达式的连接运算符
def dotREG(s):
    i = 1
    while i < len(s):
        if s[i].islower() or s[i] == '(':
            if s[i - 1] == '*' or s[i - 1].islower() or s[i - 1] == ')':
                s = s[:i] + '.' + s[i:]
                i += 1
        i += 1
    return s

# 计算正则表达式的逆波兰表达式rpn
def REG2RPN(r):
    global rpn
    i = 0
    ops = []
    while i < len(r):
        if r[i].islower():
            rpn.append(r[i])
        elif r[i] in ['.', '|', '*']:
            while ops:
                if priority[ops[-1]] >= priority[r[i]]:
                    rpn.append(ops.pop())
                else:
                    break
            ops.append(r[i])
        elif r[i] == '(':
            ops.append('(')
        elif r[i] == ')':
            while ops[-1] != '(':
                rpn.append(ops.pop())
            ops.pop()
        i += 1

    while ops:
        rpn.append(ops.pop())


r = input("Input regular expression: ")
r = dotREG(r)
REG2RPN(r)
status = 0
crt_stt = []
transitions = []
for i in range(len(rpn)):
    if rpn[i].islower():
        crt_stt.append([status, status + 1])
        transitions.append([rpn[i], str(status), str(status + 1)])
        status += 2
    elif rpn[i] == '*':
        transitions.append(['ε', str(crt_stt[-1][0]), str(crt_stt[-1][1])])
        transitions.append(['ε', str(crt_stt[-1][1]), str(crt_stt[-1][0])])
    elif rpn[i] == '.':
        stt1 = crt_stt.pop()
        stt2 = crt_stt.pop()
        transitions.append(['ε', str(stt2[1]), str(stt1[0])])
        crt_stt.append([stt2[0], stt1[1]])
    elif rpn[i] == '|':
        stt1 = crt_stt.pop()
        stt2 = crt_stt.pop()
        crt_stt.append([status, status + 1])
        transitions.append(['ε', str(status), str(stt1[0])])
        transitions.append(['ε', str(status), str(stt2[0])])
        transitions.append(['ε', str(stt1[1]), str(status + 1)])
        transitions.append(['ε', str(stt2[1]), str(status + 1)])
        status += 2

transitions.append(['ε', 'start', str(crt_stt[0][0])])
states = list(map(str, range(status))).append('start')

NFA = GraphMachine(states=states,
                   transitions=transitions,
                   initial=str(crt_stt[0][1]), title='REG2NFA')
NFA.get_graph().draw('REG2NFA.png', prog='dot')

⊈ Few Tips

  1. 虽然【(】的优先级最高,但由于【(】在遇到【)】之前不能弹出,因此我们将其优先级置为最低
  2. python的list自带pop()函数,不仅弹出最后一个元素,同时返回其值,这与C++的<stack>不同
  3. NFA起点是’start’,接收状态为序号最大的状态,颜色与其他状态不同
  4. rpn为数栈,ops为符号栈
(aa|b)*a(a|bb)*


「Reference」

  • 逆波兰表示法. (2021, September 24). Retrieved from 维基百科, 自由的百科全书
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
编译原理是学习计算机科学的一门基础课程,主要涉及语言的识别和转化,而正则表达式则是其中一个非常重要的工具。在编译原理中,正则表达式通常用于描述一些模式,比如关键字、标识符等。因此,掌握正则表达式换过程对于理解编译原理课程非常重要。 正则表达式换过程主要包括以下几个部分:正则表达式NFANFADFA、DFA最小化。其中,NFA(非确定有限状态自动机)和DFA(确定有限状态自动机)都是描述正则表达式的模型。 正则表达式NFA: 首先,正则表达式中的基本元素是字符、括号和运算符。在换为NFA的过程中,需要设计出一些状态来描述不同的字符和运算符。 对于字符来说,我们可以为它们设计出一个状态,状态的入口边是字符,出口边为空。 对于括号和运算符来说,可以为它们设计出一些连接状态。例如在括号中的字符可以通过连接状态直接连接到后面的状态,或者通过其他运算符先连接到其他的状态再连接到后面的状态。 最后,需要定义一个起始状态和一个终止状态,起始状态与第一个字符状态相连,最后一个字符状态与终止状态相连。这样,我们就得到了一张NFA图。 NFADFA: 将一个NFA换成DFA图的主要目的是为了简化图结构,以便后续对文本进行识别。 首先,需要定义DFA的状态集合,每个集合都对应一个状态。因为DFA是完全确定的有限状态自动机,所以在DFA中只能有一个状态。 然后,需要将NFA图中的每个状态都映射为DFA图中的一个状态,以便对文本进行识别。当NFA图中有多个状态对应于DFA图中的同一状态,需要将它们合并,并将它们的出口边合并成一个出口边。 DFA最小化: 最后,对DFA进行最小化处理,以便减少状态数,提高运行效率。在最小化处理需要考虑不同状态之间的等价关系。 可以采用遍历算法,将DFA中的状态按照等价关系划分为若干个等价类,然后构造一个等价类访问表,每个表项对应一个状态集。 最小化后的DFA图是可以识别文本的,可以用于在编译器中进行文本匹配和词法分析等操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值