1.背景正则表达式是计算机科学中的一个概念,很多语言都有实现。正则表达式使用某些元字符来搜索、匹配和替换与指定字符串相匹配的字符串。用于构造正则表达式语法的元字符由普通字符、标准字符、定界字符(量词)和定位符(边界字符)组成。公式,程序对这个公式进行语法分析,构建语法分析树,然后根据分析树结合正则表达式引擎生成执行程序(这个执行程序称为状态机,也称为状态自动机).来匹配字符。这里的正则表达式引擎是一组构建状态机的核心算法。目前有两种实现正则表达式引擎的方式:DFA(DeterministicFinalAutomata)和NFA(NondeterministicFiniteAutomaton)。相比之下,构造DFA自动机的成本远大于NFA自动机,但DFA自动机的执行效率高于NFA自动机。假设一个字符串的长度为n,如果使用DFA自动机作为正则表达式引擎,则匹配时间复杂度为O(n);如果使用NFA自动机作为正则表达式引擎,由于NFA自动机在匹配过程中有很多分支和回溯,假设NFA的状态数为s,则匹配算法的时间复杂度为O(ns)。NFA自动机的优点是它支持更多的功能。例如:捕获组、环顾四周、拥有优先级量词等高级函数。这些函数都是基于子表达式独立匹配的,所以在编程语言中,所使用的正则表达式库都是基于NFA实现的。那么NFA自动机是怎么匹配的呢?接下来,用下面的例子来说明:text="aabcab"regex="bc"NFA自动机读取正则表达式的每个字符,并用它来匹配目标字符串。如果匹配成功,则替换正则表达式的下一个字符,否则继续匹配目标字符串的下一个字符。分解过程:1)读取正则表达式的第一个匹配字符,与字符串的第一个字符进行比较,b不匹配a;继续改变字符串的下一个字符,即a,不匹配;继续改变下一个,即b,匹配;2)同理,读取正则表达式的第二个匹配字符,与字符串的第四个字符进行比较,c匹配c,匹配;继续读取正则表达式的下一个字符,但是已经没有字符可以匹配了,结束。这就是NFA自动机的匹配过程。虽然在实际应用中,遇到的正则表达式比这更复杂,但是匹配的方法是一样的。3.NFA自动机的回溯NFA自动机实现的比较复杂的正则表达式在匹配过程中往往会引起回溯问题。大量回溯会长时间占用CPU,带来系统性能开销。比如下面这个例子:text="abbc"regex="ab{1,3}c"上面这个例子,匹配的目的比较简单。匹配以a开头并以c结尾且中间有1-3个b字符的字符串。NFA自动机的解析过程如下:1)读取正则表达式的第一个匹配字符a,与字符串的第一个字符a进行比较,a匹配a;2)读取正则表达式A的第一个字符a匹配字符b{1,3}与字符串的第二个字符b比较匹配。但是因为b{1,3}表示1-3个b串,而NFA自动机具有贪心的特性,所以此时不??会继续读取正则表达式的下一个匹配字符,仍然使用b{1,3}与字符串的第三个字符b进行比较,结果仍然是匹配。3)继续用b{1,3}与字符串的第4个字符c进行比较,如果不匹配则回溯,将已经读取到的字符串的第4个字符c吐出。指针返回到第三个字符b的位置。4)那么回溯发生后匹配过程如何继续进行呢?程序会读取正则表达式的下一个匹配字符c,与字符串中的第四个字符c进行比较,结果匹配,结束。4.如何避免回溯问题?既然回溯会给系统带来性能开销,那么我们该如何处理呢?如果仔细看上面的案例,你会发现NFA自动机的贪心特性是导火索,它与正则表达式的匹配方式密切相关。1.贪心模式(Greedy),顾名思义,就是在数量上匹配,如果使用+,?,*or(min,max)等量词,正则表达式会匹配尽可能多的内容。比如上面的例子:text="abbc"regex="ab{1,3}c"表示在贪心模式下,NFA自动机读取最大的匹配范围,即匹配3个b字符。匹配失败会导致回溯。如果匹配结果为“abbbc”,则匹配成功。text="abbbc"regex="ab{1,3}c"2.惰性模式(Reluctant)在这种模式下,正则表达式会尽可能少地重复匹配的字符,如果匹配成功,则继续匹配其余字符串。例如,添加一个“?”在上面例子中的字符之后可以启用惰性模式。text="abc"regex="ab{1,3}?c"匹配结果为"abc"。在这种模式下,NFA自动机首先选择最小的匹配范围,即匹配1个b字符,从而避免了回溯问题。另外,关注公众号Java技术栈,后台回复:面试,可以拿到我整理的Java系列面试题及答案,很全。3、独占模式(Possessive)和贪心模式一样,独占模式会最大限度匹配更多的内容;不同的是,在排他模式下,如果匹配失败,则匹配结束,不会有回溯问题。还是上面的例子,在字符后面加一个“+”,开启独占模式。text="abbc"regex="ab{1,3}+c"导致不匹配,匹配结束,不会出现回溯问题。所以综上所述,避免回溯的方法是:使用惰性模式或者独占模式。上面提到,“Split()方法使用正则表达式来实现其强大的切分功能,但正则表达式的性能非常不稳定,使用不当会导致回溯问题。”比如使用split的方式提取域名,检查请求参数是否符合规定。split在匹配组的时候遇到特殊字符会遇到很多回溯。解决办法是在正则表达式后面加一个待匹配的字符和“+”来解决回溯问题:\\?(([A-Za-z0-9-~_=%]++\\&{0,1})+)五。正则表达式优化1.少用贪心模式:多贪心模式会造成回溯问题,可以使用独占模式避免回溯。2.减少分支选择:分支选择类型“(X|Y|Z)”的正则表达式会降低性能,开发中尽量少用。如果一定要用,可以通过以下方式进行优化:1)考虑选择的顺序,将比较常用的选项放在前面,这样匹配起来更快;2)可以尝试抽取常见的模式,例如,将“(abcd|abef)”替换为“ab(cd|ef)”,后者匹配速度更快,因为NFA自动机会尝试匹配ab,如果不是找到,它不会再尝试任何选项;3)如果是Simplebranchselectiontype,可以用tripleindex代替“(X|Y|Z)”。如果你测试一下,你会发现三重索引的效率比“(X|Y|Z)”要高。3.减少捕获嵌套:捕获组是指将正则表达式中子表达式匹配的内容保存到一个数组中,并编号或显式命名,以备后用。一般一个()就是一个捕获组,捕获组可以嵌套。非捕获组是指参与匹配但不进行组编号的捕获组,其表达式一般由(?:exp)组成。在正则表达式中,每个捕获组都有一个数字,数字0代表匹配到的全部内容。你可以看下面的例子:publicstaticvoidmain(String[]args){Stringtext="
