本文借助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:
上面的栗子基本包含了本文所用到的所有操作,另外说明几点:
transitions
列表中既可以是字典,也可以是列表,甚至是两者的结合,但都需要按照[trigger , source , destination]
的顺序写add_transition()
函数增加转移函数,输入参数也需要按照上述顺序GraphMachine
函数中的initial
参数指定初始状态,并标注为不同的颜色,然而我们在画状态转移图时标注的往往不是初始状态而是接收状态,因此我一般将initial
参数赋为接收状态- 更多使用方式请参见transitions/README.md
∇ 逆波兰表示法RPN
逆波兰表示法,是一种是由波兰数学家扬·武卡谢维奇于1920年引入的数学表达式形式,在逆波兰记法中,所有操作符置于操作数的后面,因此也被称为后缀表示法。逆波兰记法不需要括号来标识操作符的优先级。
首先阐述一下逆波兰表达式是如何得到的,我们采取如下算法:
-
需要两个栈,一个存数,一个存符号,下面分别称之为数栈和符号栈
-
顺序读入中缀表达式(正常的运算式)
-
遇到数(完整的数,不是逐位)时压入数栈
-
遇到运算符时,首先从符号栈栈顶开始检查,如果栈顶符号优先级>(=)读入运算符,则将栈顶符号弹出压入数栈中,然后检查符号栈下一个符号,循环往复,直到符号栈栈顶符号优先级<读入运算符时,将读入运算符压入符号栈
⚠️由于运算符基本是左结合的,因此我们可以认为左边(符号栈内)的运算符优先级>右边(读入)的同级运算符
-
当读入【(】时,直接压入符号栈,且只有读入【)】时才可以弹出
-
当读入【)】时,将符号栈内的符号依次弹出压入数栈,直到遇到第一个【(】,注意【(】直接弹出不压入数栈
-
读完运算式后,依次弹出符号栈并压入数栈,那么数栈【栈底→栈顶】即为逆波兰表达式
那么逆波兰表达式有什么优点呢,既然是关于运算式的那必然涉及到计算。对于逆波兰表达式,我们只需顺序读入,遇到数字压入栈中,遇到运算符则弹出栈顶的两个数字直接计算再压入栈中,最终的栈里只会剩余一个数,即为运算结果。
ℵ 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
- 虽然【(】的优先级最高,但由于【(】在遇到【)】之前不能弹出,因此我们将其优先级置为最低
- python的list自带
pop()
函数,不仅弹出最后一个元素,同时返回其值,这与C++的<stack>不同 - NFA起点是’start’,接收状态为序号最大的状态,颜色与其他状态不同
rpn
为数栈,ops
为符号栈
(aa|b)*a(a|bb)*
「Reference」
- 逆波兰表示法. (2021, September 24). Retrieved from 维基百科, 自由的百科全书