dfa转正则表达式_从0到1打造正则表达式执行引擎(二)

本文原文地址https://blog.csdn.net/xindoo/article/details/106458165

在上篇博客从0到1打造正则表达式执行引擎(一)中我们已经构建了一个可用的正则表达式引擎,相关源码见https://github.com/xindoo/regex,但上文中只是用到了NFA,NFA的引擎建图时间复杂度是O(n),但匹配一个长度为m的字符串时因为涉及到大量的递归和回溯,最坏时间复杂度是O(mn)。与之对比DFA引擎的建图时间复杂度O(n^2),但匹配时没有回溯,所以匹配复杂度只有O(m),性能差距还是挺大的。

DFA和NFA

我们已经多次提到了NFA和DFA,它俩究竟是啥?有啥区别?
首先,NFA和DFA都是有限状态机,都是有向图,用来描述状态和状态之间的关系。其中NFA全称是非确定性有限状态自动机(Nondeterministic finite automaton),DFA全称是确定性有限状态自动机(Deterministic finite automaton)。

二者的差异主要在于确定性和非确定性,何为确定性? 确定性是指面对同一输入,不会出现有多条可行的路径执行下一个节点。有点绕,看完图你就理解了。

68b79105feb8ea1a12af1702de11f94d.png


图示分别是一个NFA和DFA,上图之所以是NFA是因为它有节点具备不确定性,比如0节点,在输入"a"之后它分别可以到0 1 2 节点。还有,上图有$epsilon$边,它可以在没有输入的情况下跳到下一个节点,这也带来了不确定性。相反,下图DFA中,每个节点对某一特定的输入都只有最多一条边。

总结下NFA和DFA的区别就是,有ε边或者某个节点对同一输入对应多个状态的一定是NFA。

DFA和NFA存在等价性,也就是说任何NFA都可以转化为等价的DFA。由于NFA的非确定性,在面对一个输入的时候可能有多条可选的路径,所以在一条路径走不通的情况下,需要回溯到选择点去走另外一条路径。但DFA不同,在每个状态下,对每个输入不会存在多条路径,就不需要递归和回溯了,可以一条路走到黑。DFA的匹复杂度只有O(n),但因为要递归和回溯NFA的匹配复杂度达到了O(n^2)。 这也是为什么我们要将引擎中的NFA转化为DFA的主要原因。

NFA转DFA

算法

NFA转DFA的算法叫做子集构造法,其具体流程如下。

  • 步骤1: NFA的初始节点和初始节点所有ε可达的节点共同构成DFA的初始节点,然后对初始DFA节点执行步骤2。
  • 步骤2: 对当前DFA节点,找到其中所有NFA节点对输入符号X所有可达的NFA节点,这些节点沟通构成的DFA节点作为当前DFA节点对输入X可达的DFA节点。
  • 步骤3: 如果步骤2中找到了新节点,就对新节点重复执行步骤2。
  • 步骤4: 重复步骤2和步骤3直到找不DFA新节点为止。
  • 步骤5: 把所有包含NFA终止节点的DFA节点标记为DFA的终止节点。

语言描述比较难理解,我们直接上例子。 我们已经拿上一篇网站中的正则表达式 a(b|c)* 为例,我在源码https://github.com/xindoo/regex中加入了NFA输出的代码, a(b|c)* 的NFA输出如下。

from to input0-> 1  a1-> 8  Epsilon8-> 9  Epsilon8-> 6  Epsilon6-> 2  Epsilon6-> 4  Epsilon2-> 3  b4-> 5  c3-> 7  Epsilon5-> 7  Epsilon7-> 9  Epsilon7-> 6  Epsilon

绘图如下:

62298447439a181ff8b2a4a70cec519c.png


我们在上图的基础上执行步骤1 得到了节点0作为DFA的开始节点。

2627e98031f99e5546bd877ba0fa7464.png


然后对DFA的节点0执行步骤1,找到NFA中所有a可达的NFA节点(1#2#4#6#8#9)构成NFA中的节点1,如下图。

88d97ccb7184dca08811dc6d5cceea75.png


我以dfa1为出发点,发现了a可达的所有NFA节点(2#3#4#6#7#9)和b可达的所有节点(2#4#5#6#7#9),分别构成了DFA中的dfa2和dfa3,如下图。

adbfe626c96e8b00bec60a333933c870.png

5e59c256371665fc07f2863c77f9c238.png


然后我们分别在dfa2 dfa3上执行步骤三,找不到新节点,但会找到几条新的边,补充如下,至此我们就完成了对 a(b|c)* 对应NFA到DFA的转化。

adf5f6118db4ee46079153e7029fb50c.png


可以看出DFA图节点明显少于NFA,但NFA更容易看出其对应的正则表达式。之前我还写过DFA生成正则表达式的代码,详见文章https://blog.csdn.net/xindoo/article/details/102643270

代码实现

代码其实就是对上文流程的表述,更多细节见https://github.com/xindoo/regex。

 private static DFAGraph convertNfa2Dfa(NFAGraph nfaGraph) {DFAGraph dfaGraph = new DFAGraph();Set<State> startStates = new HashSet<>();// 用NFA图的起始节点构造DFA的起始节点 步骤1 startStates.addAll(getNextEStates(nfaGraph.start, new HashSet<>()));if (startStates.size() == 0) {startStates.add(nfaGraph.start);}dfaGraph.start = dfaGraph.getOrBuild(startStates);Queue<DFAState> queue = new LinkedList<>();Set<State> finishedStates = new HashSet<>();// 如果BFS的方式从已找到的起始节点遍历并构建DFAqueue.add(dfaGraph.start);while (!queue.isEmpty()) {  // 步骤2 DFAState curState = queue.poll();for (State nfaState : curState.nfaStates) {Set<State> nextStates = new HashSet<>();Set<String> finishedEdges = new HashSet<>();finishedEdges.add(Constant.EPSILON);for (String edge : nfaState.next.keySet()) {if (finishedEdges.contains(edge)) {continue;}finishedEdges.add(edge);Set<State> efinishedState = new HashSet<>();for (State state : curState.nfaStates) {Set<State> edgeStates = state.next.getOrDefault(edge, Collections.emptySet());nextStates.addAll(edgeStates);for (State eState : edgeStates) {// 添加E可达节点if (efinishedState.contains(eState)) {continue;}nextStates.addAll(getNextEStates(eState, efinishedState));efinishedState.add(eState);}}// 将NFA节点列表转化为DFA节点,如果已经有对应的DFA节点就返回,否则创建一个新的DFA节点DFAState nextDFAstate = dfaGraph.getOrBuild(nextStates);if (!finishedStates.contains(nextDFAstate)) {queue.add(nextDFAstate);}curState.addNext(edge, nextDFAstate);}}finishedStates.add(curState);}return dfaGraph;}
public class DFAState extends State {public Set<State> nfaStates = new HashSet<>();// 保存对应NFAState的id,一个DFAState可能是多个NFAState的集合,所以拼接成Stringprivate String allStateIds;public DFAState() {this.stateType = 2;}public DFAState(String allStateIds, Set<State> states) {this.allStateIds = allStateIds;this.nfaStates.addAll(states);//这里我将步骤五直接集成在创建DFA节点的过程中了for (State state : states) {if (state.isEndState()) {this.stateType = 1;}}}public String getAllStateIds() {return allStateIds;}
}

另外我在DFAGraph中封装了有些NFA节点列表到DFA节点的转化和查找,具体如下。

public class DFAGraph {private Map<String, DFAState> nfaStates2dfaState = new HashMap<>();public DFAState start = new DFAState();// 这里用map保存NFAState结合是已有对应的DFAState, 有就直接拿出来用public DFAState getOrBuild(Set<State> states) {String allStateIds = "";int[] ids = states.stream().mapToInt(state -> state.getId()).sorted().toArray();for (int id : ids) {allStateIds += "#";allStateIds += id;}if (!nfaStates2dfaState.containsKey(allStateIds)) {DFAState dfaState = new DFAState(allStateIds, states);nfaStates2dfaState.put(allStateIds, dfaState);}return nfaStates2dfaState.get(allStateIds);}
}

DFA引擎匹配过程

dfa引擎的匹配也可以完全复用NFA的匹配过程,所以对之前NFA的匹配代码,可以针对DFA模式取消回溯即可(不取消也没问题,但会有性能影响)。

   private boolean isMatch(String text, int pos, State curState) {if (pos == text.length()) {if (curState.isEndState()) {return true;}for (State nextState : curState.next.getOrDefault(Constant.EPSILON, Collections.emptySet())) {if (isMatch(text, pos, nextState)) {return true;}}return false;}for (Map.Entry<String, Set<State>> entry : curState.next.entrySet()) {String edge = entry.getKey();// 如果是DFA模式,不会有EPSILON边if (Constant.EPSILON.equals(edge)) {for (State nextState : entry.getValue()) {if (isMatch(text, pos, nextState)) {return true;}}} else {MatchStrategy matchStrategy = MatchStrategyManager.getStrategy(edge);if (!matchStrategy.isMatch(text.charAt(pos), edge)) {continue;}// 遍历匹配策略for (State nextState : entry.getValue()) {// 如果是DFA匹配模式,entry.getValue()虽然是set,但里面只会有一个元素,所以不需要回溯if (nextState instanceof DFAState) {return isMatch(text, pos + 1, nextState);}if (isMatch(text, pos + 1, nextState)) {return true;}}}}return false;}

因为DFA的匹配不需要回溯,所以可以完全改成非递归代码。

    private boolean isDfaMatch(String text, int pos, State startState) {State curState = startState;while (pos < text.length()) {boolean canContinue = false;for (Map.Entry<String, Set<State>> entry : curState.next.entrySet()) {String edge = entry.getKey();MatchStrategy matchStrategy = MatchStrategyManager.getStrategy(edge);if (matchStrategy.isMatch(text.charAt(pos), edge)) {curState = entry.getValue().stream().findFirst().orElse(null);pos++;canContinue = true;break;}}if (!canContinue) {return false;}}return curState.isEndState();}

DFA和NFA引擎性能对比

我用jmh简单做了一个非严格的性能测试,随手做的 看看就好,结果如下:

Benchmark                   Mode  Cnt       Score   Error  Units
RegexTest.dfaNonRecursion  thrpt    2  144462.917          ops/s
RegexTest.dfaRecursion     thrpt    2  169022.239          ops/s
RegexTest.nfa              thrpt    2   55320.181          ops/s

DFA的匹配性能远高于NFA,不过这里居然递归版还比非递归版快,有点出乎意料, 详细测试代码已传至Github https://github.com/xindoo/regex,欢迎查阅。

参考资料

  • nfa转dfa

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/418031.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AOP的学习

注意&#xff1a;不惊动原始设计 一、部署依赖 AOP开发默认导入 <dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.4</version></dependency> 二、具体步骤 Aspect 告诉Sprin…

选择题_一级造价师选择题的分值是多少

一级造价工程师考试的选择题分为单选题和多选题&#xff0c;每科单选题为60题&#xff0c;每题一分&#xff1b;多选题为20题&#xff0c;每题两分&#xff0c;总分100分。一级造价师选择题的分值一级造价工程师考试共有《建设工程造价管理》、《建设工程计价》、《建设工程技术…

2017年什么命_所谓“佛系”,真的不是什么都不做!

文 | 新宇姐姐 猫咪de生活观在这匆忙的世道中&#xff0c;人心浮躁&#xff0c;急不可耐。这喧嚣的世界车水马龙&#xff0c;我们为了生活&#xff0c;三点一线的忙碌奔波。城市很繁华&#xff0c;容易让人迷失&#xff1b;尘世很奔波&#xff0c;容易让人疲倦。不由得让我们想…

flask 接口 让别人能访问_flask搭建一个前后端分离的系统

我们通常说三端&#xff0c;pc端&#xff0c;android端和ios端。如果前后端不分离&#xff0c;相当是要做三套系统。如果前后端分离的话&#xff0c;可以共用一个后端&#xff0c;前端各自做自己的&#xff0c;不用管后端。前后端不分离在前后端不分离的应用模式中&#xff0c;…

SpringMVC下5中参数类型传递

1、普通参数 //普通参数:请求参数名与形参名不同RequestMapping("/commonParamDifferentName")ResponseBodypublic String commonParamDifferentName(RequestParam("name") String username, int age){System.out.println("普通参数传递:username:&qu…

centos 没有nmtui命令_Linux free 命令

在Linux/Unix 系统上&#xff0c;free是一个很受系统管理员欢迎的命令&#xff0c;它是一个功能强大的工具&#xff0c;他能以一种易读的方式把内存使用情况展示出来free 展示了系统中空闲和已经使用了的物理内存、交换内存以及内核的缓冲区和页面缓存&#xff0c;所有展示的信…

spring中注解来创建bean

一、 核心配置文件中的组件来扫描对应包下面的所有类来匹配。 当我们使用ApplicationContext实例化的对象调用getBean函数获取bean的时候&#xff0c;内部时根据Component内的值来确定创建哪个对象&#xff1a; Component中的值就类似之前bean配置中id的作用一样。 二、纯注解…

django设置超级管理员_Django的简介与安装

Django的简介与安装Django 是用 Python 开发的一个免费开源的 Web 框架&#xff0c;可以用来快速搭建优雅的高性能网站。它采用的是“MVC”的框架模式&#xff0c;即模型 M、视图 V 和控制器 C。 Django 最大的特色&#xff0c;在于将网页和数据库中复杂的关系&#xff0c;转化…

SpringMVC的准备工作

一、 先通过Maven创建一个webapp项目 创建完成之后在pom文件中设置一些基本的信息&#xff1a; 1、打包方式 <packaging>war</packaging> 以war包的形式打包 2、相关依赖 现阶段只有3个&#xff1a;SpringMVC、thymeleaf与Spring整合、ServletAPI <dependencie…

请领导批阅文件怎么说_领导说“谢谢”,该怎么回?低情商才说不客气,高情商的都这样说!...

职场中&#xff0c;当我们顺手帮了领导个小忙或者领导跟我们要工作资料&#xff0c;我们发给领导的时候&#xff0c;领导可能会顺口说“收到&#xff0c;谢谢”&#xff0c;这样的时候&#xff0c;其实领导就是表示收到了。我们如何回复&#xff0c;才不至于失了礼貌&#xff0…

vue的简单学习

vue和thymeleaf &#xff1a; 若先显示框架后加载数据则用vue 若后端处理好之后发送给前端页面显示用thymeleaf (弹幕) 对于前后端分离的认识&#xff1a; 使用vue 将前端页面、框架等建立好&#xff0c;只需要后端发送数据即可 但thymeleaf就需要在服务器启动之前将数据保存到…

Selleck --- 01Cookie

1. //1.设置Cookie function setCookie(name,value,days) {var date new Date();date.setDate(date.getDate() days);document.cookie name "" value ";expires" date; } console.log(setCookie("pwd","5566778899",1)); //获取c…

JS~~~ 前端开发一些常用技巧 模块化结构 命名空间处理 奇技淫巧!!!!!!...

前端开发一些常用技巧 模块化结构 &&&&& 命名空间处理 奇技淫巧&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;2016-09-29 17:16:39 #####背景&#xff1a; 前端开发过程中&#xff0c;创建js对象…