首先:正则两种状态机的时间复杂度
来源:请问一下大家正则表达式的时间复杂度
NFA构造O(n),匹配O(nm)
DFA构造O(2^n),最小化O(kn'logn')(N'=O(2^n)),匹配O(m)
n=regex长度,m=串长,k=字母表大小,n'=原始的dfa大小
理论模型是有穷自动机
,具体的实现为正则引擎(Regex Engine)
分两类确定型有穷自动机(Definite Finite Automata,DFA,其状态都是确定)
和非确定型有穷自动机(Non-definite Finite Automate,NFA,其状态在某个时刻是不确定的)
。DFA和NFA是可以证明存在等价关系,不过这是两种不同的自动机。在这里,我才不会深入讨论它们的原理。简单地说,DFA 的时间复杂度是线性的。它更稳定,但功能有限。NFA 的时间复杂度相对不稳定。 根据正则表达式的不同,时间有时长,有时短。NFA 的优点是它的功能更强大,所以被 Java、.NET、Perl、Python、Ruby 和 PHP 用来处理正则表达式。 NFA 是怎样进行匹配的呢?我用下面的字符串和表达式作为例子。
text="A lovely cat."
regex="cat"
NFA 匹配是基于正则表达式的。也就是说,NFA 将依次读取正则表达式的匹配符,并将其与目标字符串进行匹配。如果匹配成功,它将转到正则表达式的下一个匹配符。否则,它将继续与目标字符串的下一个字符进行比较。 让我们一步一步地来看一下上面的例子。
- 首先,提取正则表达式的第一个匹配符:c。然后,将它与字符串的第一个字符 A 进行比较。不匹配,所以转到下一个。第二个字符是 l,也不匹配。继续转到下一个,也就是 o。匹配失败。于是,继续读取text的字符直到c,匹配成功猴,读取正则表达式的第二个字符: a。
- 正则表达式的第二个匹配符是:a。将它与字符串的第九个字符 a进行比较。又匹配了。于是继续读取正则表达式的第三个字符 t。
- 正则表达式的第三个匹配符是 t。让我们继续与字符串的第十个字符比较。匹配成功。接着,尝试读取正则表达式的下一个字符,发现没有字符了,因此匹配结束。
回溯(backtracking)
为了应对NFA状态不确定,可能会匹配出错误的结果,因此需要“尝试失败-重新选择”的过程,现在,我已经解释了 NFA 是如何进行字符串匹配的——回溯法。我们将使用下面的例子,以便更好的解释回朔法。
text="xyyyyyyz"
regex="xy{1,10}z"
这是一个比较简单的例子。正则表达式以 x 开始,以 z 结束。它们之间有以 1-10 个 y 组成的字符串。NFA 的匹配过程如下:
- 首先,读取正则表达式的第一个匹配符 x,并将其与字符串的第一个字符 x 进行比较。两者匹配,所以,接下来是移动到正则表达式的第二个字符。
- 读取正则表达式的第二个匹配符
y{1,10}
,将它与字符串的第二个字符 y 进行比较。它们又匹配了。y{1,10}
代表 1-10 个 y,基于 NFA 的贪婪特性(即,尽可能地进行匹配),此时它不会读取正则表达式的下一个匹配符,而是仍然使用y{1,10}
与字符串的第三个字符 y 进行比较。它们匹配了。于是继续用 y{1,10} 与字符串的第n个字符 y 进行比较,直到第七个,它们不匹配。 回溯就出现在这里 - 那么回溯是如何进行的?回溯后,字符串中已被读取的第七个字符 y 将被放弃。指针将返回到字符串的第三个字符。接着,正则表达式的下一个匹配符 z 会被用来与待匹配字符串当前指针的下一个字符 z 进行对比。两者是匹配的。这时,字符串最后一个字符已经被读取。匹配结束。
正则表达式的三种模式
- 贪婪模式
- 这个就是最简单的匹配模式,也是最先接触的匹配模式
- 勉强模式
- 在正则表达式中添加一个
?
标志,贪婪模式将变成勉强模式。此时,它将尽可能少地匹配。然而,勉强模式下回溯仍可能出现。
- 在正则表达式中添加一个
- 独占模式
- 如果添加
+
标志,则原来的贪婪模式将变成独占模式。也就是说,它将匹配尽可能多的字符,但不会回溯。
- 如果添加
text="xyyyyyyz"
regex="xy{1,10}?z"
正则表达式的第一个字符 x 与字符串的第一个字符 x 相匹配。正则表达式的第二个运算符 y{1,10}?
匹配了字符串的第二个字符 y 。由于最小匹配的原则,正则表达式将读取第三个运算符 z,并与字符串第三个字符 y 进行比较。两者不匹配。因此,程序进行回溯并将正则表达式的第二个运算符 y{1,10}?
与字符串的第三个字符 y 进行比较。还是匹配成功了。之后继续匹配,正则表达式的第三个匹配符 c 与字符串的第七个字符 z 正相匹配。匹配结束。
这三种模式,在Python,Java这些常见的开发工具中是比较常用的,然而在C++中并不是合适,我们开发团队经过一次次的修改、优化,在性能调优上才有了质的飞跃。
优化:
python中re模块的四种方法:
这四个方法是从某个字符串中寻找特定子串或判断某个字符串是否符合某个模式的常用方法。
1、match
re.match(pattern, string[, flags])
从首字母开始开始匹配,string如果包含pattern子串,则匹配成功,返回Match对象,失败则返回None,若要完全匹配,pattern要以$结尾。
2、search
re.search(pattern, string[, flags])
若string中包含pattern子串,则返回Match对象,否则返回None,注意,如果string中存在多个pattern子串,只返回第一个。
3、findall
re.findall(pattern, string[, flags])
返回string中所有与pattern相匹配的全部字串,返回形式为数组。
4、finditer
re.finditer(pattern, string[, flags])
返回string中所有与pattern相匹配的全部字串,返回形式为迭代器。
若匹配成功,match()/search()返回的是Match对象,finditer()返回的也是Match对象的迭代器,获取匹配结果需要调用Match对象的group()、groups或group(index)方法。
group()、groups()与group(index)的区别,
>>> import re
>>> s = '23432werwre2342werwrew'
>>> p = r'(\d*)([a-zA-Z]*)'
>>> m = re.match(p,s)
>>> m.group()
'23432werwre'
>>> m.group(0)
'23432werwre'
>>> m.group(1)
'23432'
>>> m.group(2)
'werwre'
>>> m.groups()
('23432', 'werwre')
>>> m = re.findall(p,s)
>>> m
[('23432', 'werwre'), ('2342', 'werwrew'), ('', '')]
>>> p=r'(\d+)'
>>> m=re.match(p,s)
>>> m.group()
'23432'
>>> m.group(0)
'23432'
>>> m.group(1)
'23432'
>>> m.groups()
('23432',)
>>> m=re.findall(p,s)
>>> m
['23432', '2342']
综上:
- group():母串中与模式pattern匹配的子串;
- group(0):结果与group()一样;
- groups():所有group组成的一个元组,group(1)是与patttern中第一个group匹配成功的子串,group(2)是第二个,依次类推,如果index超了边界,抛出IndexError;
- findall():返回的就是所有groups的数组,就是group组成的元组的数组,母串中的这一撮组成一个元组,那一措组成一个元组,这些元组共同构成一个list,就是findall()的返回结果。另,如果groups是只有一个元素的元组,findall的返回结果是子串的list,而不是元组的list了。