项目地址:Regex in Python
前两篇已经完成的写了一个基于NFA的正则表达式引擎了,下面要做的就是更近一步,把NFA转换为DFA,并对DFA最小化
DFA的定义对于NFA转换为DFA的算法,主要就是将NFA中可以状态节点进行合并,进而让状态节点对于一个输入字符都有唯一的一个跳转节点
所以对于DFA的节点就含有一个nfa状态节点的集合和一个唯一的标识和对是否是接收状态的flag
class Dfa(object): STATUS_NUM = 0 def __init__(self): self.nfa_sets = [] self.accepted = False self.status_num = -1 @classmethod def nfas_to_dfa(cls, nfas): dfa = cls() for n in nfas: dfa.nfa_sets.append(n) if n.next_1 is None and n.next_2 is None: dfa.accepted = True dfa.status_num = Dfa.STATUS_NUM Dfa.STATUS_NUM = Dfa.STATUS_NUM + 1 return dfa NFA转换为DFA将NFA转换为DFA的最终目标是获得一张跳转表,这个和之前C语言编译的语法分析表有点像
这个函数就是NFA转换为DFA的全部算法了,主要逻辑就是:
先利用之前的closure算法,计算出可以合并的NFA节点,然后生成一个DFA的节点
然后对这个DFA集合进行遍历
之后对于每个输入字符进行move操作,然后对得到的move集合再进行一次closure操作,这样就可以得到下一个DFA状态节点(这里还要进行一个判重的操作,就是可能当前DFA状态节点可能已经生成过了)
然后将这两个节点的对应关系放入跳转表中
这时候的DFA如果其中含有的NFA存在一个可接收的状态节点,那么当前的DFA的当然也是可接受状态了
def convert_to_dfa(nfa_start_node): jump_table = list_dict(MAX_DFA_STATUS_NUM) ns = [nfa_start_node] n_closure = closure(ns) dfa = Dfa.nfas_to_dfa(n_closure) dfa_list.append(dfa) dfa_index = 0 while dfa_index < len(dfa_list): dfa = dfa_list[dfa_index] for i in range(ASCII_COUNT): c = chr(i) nfa_move = move(dfa.nfa_sets, c) if nfa_move is not None: nfa_closure = closure(nfa_move) if nfa_closure is None: continue new_dfa = convert_completed(dfa_list, nfa_closure) if new_dfa is None: new_dfa = Dfa.nfas_to_dfa(nfa_closure) dfa_list.append(new_dfa) next_state = new_dfa.status_num jump_table[dfa.status_num][c] = next_state if new_dfa.accepted: jump_table[new_dfa.status_num]['accepted'] = True dfa_index = dfa_index + 1 return jump_table DFA最小化DFA最小化本质上是也是对状态节点的合并,然后分区
先根据是否为接收状态进行分区
再根据DFA跳转表的跳转关系对分区里的节点进行再次分区,如果当前DFA节点跳转后的状态节点也位于同一个分区中,证明它们可以被归为一个分区
重复上面的算法
Dfa分区定义DfaGroup和之前的定义大同小异,都是有一个唯一的标识和一个放DFA状态节点的list
class DfaGroup(object): GROUP_COUNT = 0 def __init__(self): self.set_count() self.group = [] def set_count(self): self.group_num = DfaGroup.GROUP_COUNT DfaGroup.GROUP_COUNT = DfaGroup.GROUP_COUNT + 1 def remove(self, element): self.group.remove(element) def add(self, element): self.group.append(element) def get(self, count): if count > len(self.group) - 1: return None return self.group[count] def __len__(self): return len(self.group) Minimize DFApartition是最小化DFA算法最重要的部分
会先从跳转表中找出当前DFA对应跳转的下一个状态节点
first是用来比较的DFA节点
如果next节点的下一个状态和first节点的下一状态不在同一分区下的话,说明它们不可以在同一个分区
就重新创建一个新分区
所以其实DFA最小化做的就是合并相同的下一个跳转状态的节点
def partition(jump_table, group, first, next, ch): goto_first = jump_table[first.status_num].get(ch) goto_next = jump_table[next.status_num].get(ch) if dfa_in_group(goto_first) != dfa_in_group(goto_next): new_group = DfaGroup() group_list.append(new_group) group.remove(next) new_group.add(next) return True return False 创建跳转表再分完区之后节点和节点间的跳转就变成了区和区间的跳转了
遍历DFA集合
从之前的跳转表中找到相应的节点和相应的跳转关系
然后找出它们对应的分区,即转换为分区和分区之间的跳转
def create_mindfa_table(jump_table): trans_table = list_dict(ASCII_COUNT) for dfa in dfa_list: from_dfa = dfa.status_num for i in range(ASCII_COUNT): ch = chr(i) to_dfa = jump_table[from_dfa].get(ch) if to_dfa: from_group = dfa_in_group(from_dfa) to_group = dfa_in_group(to_dfa) trans_table[from_group.group_num][ch] = to_group.group_num if dfa.accepted: from_group = dfa_in_group(from_dfa) trans_table[from_group.group_num]['accepted'] = True return trans_table 匹配输入字符串利用跳转表进行对输入字符串的匹配的逻辑非常简单
遍历输入的字符串
拿到当前状态对应的输入的跳转关系
进行跳转或者完成匹配
def dfa_match(input_string, jump_table, minimize=True): if minimize: cur_status = dfa_in_group(0).group_num else: cur_status = 0 for i, c in enumerate(input_string): jump_dict = jump_table[cur_status] if jump_dict: js = jump_dict.get(c) if js is None: return False else: cur_status = js if i == len(input_string) - 1 and jump_dict.get('accepted'): return True return jump_table[cur_status].get('accepted') is not None 总结正则表达式 -> NFA -> DFA -> DFA最小化 -> 进行匹配