Regular Expression Matching First Non-Repeated Character
DR
EDCOX1〔0〕与文本中包含的第一个非重复字符不匹配(它总是在第一个非重复字符之前或之前返回字符,或者在没有重复字符的情况下,在字符串结束之前返回字符)。我的理解是Re.Sqh()如果没有匹配,应该返回任何一个。我只感兴趣理解为什么这个正则表达式不是用Python EDCOX1 1模块来工作的,而不是用任何其他方法来解决这个问题。
全背景
问题描述来自https://www.codeeval.com/open_challenges/12/。我已经用一个非regex方法解决了这个问题,但是我重新讨论了它,以扩展我对Python的
我的整个程序看起来像这样
1 2 3 4 5 6 7 8 | import re import sys with open(sys.argv[1], 'r') as test_cases: for test in test_cases: print(re.search("(?P<letter>.)(?!.*(?P=letter))", test.strip() ).group() ) |
一些输入/输出对是:
1 2 3 4 5 6 7 8 9 | rain | r teetthing | e cardiff | c kangaroo | k god | g newtown | e taxation | x refurbished | f substantially | u |
根据我在https://docs.python.org/2/library/re.html上看到的内容:
(.) 创建一个与任何字符匹配的命名组,并允许以后将其作为\1 进行回退引用。(?!...) 是一种消极的前瞻,它将匹配限制在... 不匹配的情况下。.*\1 表示前面(.) 匹配的任何字符数(包括零)。re.search(pattern, string) 只返回regex模式产生匹配的第一个位置(如果找不到匹配,则返回none)。.group() 相当于.group(0) 返回整个匹配
我认为这些部分应该一起解决所述的问题,而且它确实像我认为的那样对大多数输入有效,但是在
1 2 3 4 5 6 7 8 9 | tooth | o # fails on consecutive repeated characters aardvark | d # but does ok if it sees them later aah | a # verified last one didn't work just because it was at start heh | e # but it works for this one hehe | h # What? It thinks h matches (lookahead maybe doesn't find"heh"?) heho | e # but it definitely finds"heh" and stops"h" from matching here hahah | a # so now it won't match h but will match a hahxyz | a # but it realizes there are 2 h characters here... hahxyza | h # ... Ok time for StackOverflow |
我知道lookbehind和negative lookbehind仅限于3个字符的最大固定长度字符串,即使它们的计算结果为固定长度字符串,也不能包含backreference,但我没有看到文档指定任何对negative lookbehind的限制。
我们以你的
从
1 2 | tooth ^ ° |
接下来,以
1 2 | tooth ^° |
接下来取第二个
1 2 | tooth ^ |
所以你的regex不匹配第一个未重复的字符,但是第一个,在字符串的末尾没有更多的重复。
塞巴斯蒂安的回答已经很好地解释了为什么你目前的尝试不起作用。
.NET由于您对.NET风格的解决方案感兴趣,因此解决方案变得微不足道:
1 | (?<letter>.)(?!.*?\k<letter>)(?<!\k<letter>.+?) |
演示链接
这是因为.NET支持可变长度的lookbehinds。您还可以使用python获得这个结果(见下文)。
因此,对于每个字母
- 如果在输入
(?!.*?\k 中进一步重复) - 如果在
(?.+?) 之前就已经遇到过(我们必须在返回时跳过正在测试的字母,因此是+ )。
Python
python regex模块还支持可变长度的lookbehinds,因此上面的regex将使用一个小的语法变化:您需要用
正则表达式为:
1 | (?<letter>.)(?!.*?\g<letter>)(?<!\g<letter>.+?) |
下面是一个例子:
1 2 3 4 5 6 7 | $ python Python 2.7.10 (default, Jun 1 2015, 18:05:38) [GCC 4.9.2] on cygwin Type"help","copyright","credits" or"license" for more information. >>> import regex >>> regex.search(r'(?<letter>.)(?!.*?\g<letter>)(?<!\g<letter>.+?)', 'tooth') <regex.Match object; span=(4, 5), match='h'> |
PCRE
好了,现在事情开始变糟了:由于PCRE不支持可变长度的lookbehinds,我们需要以某种方式记住在输入中是否已经遇到了给定的字母。
不幸的是,regex引擎不提供随机访问内存支持。就通用内存而言,我们能得到的最好的是一个堆栈——但这还不够,因为一个堆栈只允许我们访问其最顶层的元素。
如果我们接受把自己限制在一个给定的字母表中,我们就可以滥用捕获组来存储标志。让我们在一个由三个字母组成的有限字母表中看到这一点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | # Anchor the pattern \A # For each letter, test to see if it's duplicated in the input string (?(?=[^a]*+a[^a]*a)(?<da>)) (?(?=[^b]*+b[^b]*b)(?<db>)) (?(?=[^c]*+c[^c]*c)(?<dc>)) # Skip any duplicated letter and throw it away [a-c]*?\K # Check if the next letter is a duplicate (?: (?(da)(*FAIL)|a) | (?(db)(*FAIL)|b) | (?(dc)(*FAIL)|c) ) |
这是如何工作的:
- 首先,EDCOX1×8的锚点确保我们只处理输入字符串一次。
- 然后,对于每一个字母EDOCX1,9的字母表,我们将设置一个IS复制标志
dX :- 条件模式EDOCX1,11,在这里使用:
- 条件是EDOCX1,12,如果输入字符串包含EDCOX1,9,两次,那么这是正确的。
- 如果条件为真,那么then子句是
(? ,它是一个空捕获组,将匹配空字符串。) - 如果条件为假,则不会匹配
dX 组
- 接下来,我们懒散地跳过字母表中的有效字母:
[a-c]*? - 我们在与埃多克斯的最后一场比赛中淘汰了他们。
- 现在,我们试图匹配一个没有设置
dX 标志的字母。为此,我们将做一个有条件的分支:(?(dX)(*FAIL)|X) 。- 如果
dX 匹配(意味着X 是一个重复字符),我们将(*FAIL) 强制引擎回溯并尝试不同的字母。 - 如果
dX 不匹配,我们尝试匹配X 。在这一点上,如果成功的话,我们知道X 是第一个不重复的字母。
- 如果
- 条件模式EDOCX1,11,在这里使用:
模式的最后一部分也可以替换为:
1 2 3 4 5 | (?: a (*THEN) (?(da)(*FAIL)) | b (*THEN) (?(db)(*FAIL)) | c (*THEN) (?(dc)(*FAIL)) ) |
这是更优化的。它首先匹配当前字母,然后才检查它是否是副本。
小写字母
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | # Anchor the pattern \A # For each letter, test to see if it's duplicated in the input string (?(?=[^a]*+a[^a]*a)(?<da>)) (?(?=[^b]*+b[^b]*b)(?<db>)) (?(?=[^c]*+c[^c]*c)(?<dc>)) (?(?=[^d]*+d[^d]*d)(?<dd>)) (?(?=[^e]*+e[^e]*e)(?<de>)) (?(?=[^f]*+f[^f]*f)(?<df>)) (?(?=[^g]*+g[^g]*g)(?<dg>)) (?(?=[^h]*+h[^h]*h)(?<dh>)) (?(?=[^i]*+i[^i]*i)(?<di>)) (?(?=[^j]*+j[^j]*j)(?<dj>)) (?(?=[^k]*+k[^k]*k)(?<dk>)) (?(?=[^l]*+l[^l]*l)(?<dl>)) (?(?=[^m]*+m[^m]*m)(?<dm>)) (?(?=[^n]*+n[^n]*n)(?<dn>)) (?(?=[^o]*+o[^o]*o)(?<do>)) (?(?=[^p]*+p[^p]*p)(?<dp>)) (?(?=[^q]*+q[^q]*q)(?<dq>)) (?(?=[^r]*+r[^r]*r)(?<dr>)) (?(?=[^s]*+s[^s]*s)(?<ds>)) (?(?=[^t]*+t[^t]*t)(?<dt>)) (?(?=[^u]*+u[^u]*u)(?<du>)) (?(?=[^v]*+v[^v]*v)(?<dv>)) (?(?=[^w]*+w[^w]*w)(?<dw>)) (?(?=[^x]*+x[^x]*x)(?<dx>)) (?(?=[^y]*+y[^y]*y)(?<dy>)) (?(?=[^z]*+z[^z]*z)(?<dz>)) # Skip any duplicated letter and throw it away [a-z]*?\K # Check if the next letter is a duplicate (?: a (*THEN) (?(da)(*FAIL)) | b (*THEN) (?(db)(*FAIL)) | c (*THEN) (?(dc)(*FAIL)) | d (*THEN) (?(dd)(*FAIL)) | e (*THEN) (?(de)(*FAIL)) | f (*THEN) (?(df)(*FAIL)) | g (*THEN) (?(dg)(*FAIL)) | h (*THEN) (?(dh)(*FAIL)) | i (*THEN) (?(di)(*FAIL)) | j (*THEN) (?(dj)(*FAIL)) | k (*THEN) (?(dk)(*FAIL)) | l (*THEN) (?(dl)(*FAIL)) | m (*THEN) (?(dm)(*FAIL)) | n (*THEN) (?(dn)(*FAIL)) | o (*THEN) (?(do)(*FAIL)) | p (*THEN) (?(dp)(*FAIL)) | q (*THEN) (?(dq)(*FAIL)) | r (*THEN) (?(dr)(*FAIL)) | s (*THEN) (?(ds)(*FAIL)) | t (*THEN) (?(dt)(*FAIL)) | u (*THEN) (?(du)(*FAIL)) | v (*THEN) (?(dv)(*FAIL)) | w (*THEN) (?(dw)(*FAIL)) | x (*THEN) (?(dx)(*FAIL)) | y (*THEN) (?(dy)(*FAIL)) | z (*THEN) (?(dz)(*FAIL)) ) |
这是关于regex101的演示,包括单元测试。
如果需要更大的字母表,可以扩展这个模式,但显然这不是一个通用的解决方案。它主要是出于教育目的,不应用于任何严重的应用。
对于其他风格,您可以尝试调整模式以用更简单的等价物替换PCRE功能:
\A 变为^ 。X (*THEN) (?(dX)(*FAIL)) 可改为(?(dX)(?!)|X) 。- 您可以扔掉
\k ,将最后一个未捕获的组(?: 、) 替换为命名组,如(? 、) ,并将其内容视为结果。
唯一需要但有点不寻常的构造是条件组
正则表达式对于任务来说不是最优的,即使使用RE的替代实现,RE不限制固定长度字符串(如Matthew Barnett的正则表达式)的后视。
最简单的方法是计算字母的出现次数并打印频率等于1的第一个字母:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import sys from collections import Counter, OrderedDict # Counter that remembers that remembers the order entries were added class OrderedCounter(Counter, OrderedDict): pass # Calling next() once only gives the first entry first=next with open(sys.argv[1], 'r') as test_cases: for test in test_cases: lettfreq = OrderedCounter(test) print(first((l for l in lettfreq if lettfreq[l] == 1))) |
regex不起作用的原因是它将不匹配后面跟着同一个字符的字符,但是没有什么可以阻止它匹配后面不跟着同一个字符的字符,即使前面跟着同一个字符。