How can we match a^n b^n with Java regex?
This is the second part of a series of educational regex articles. It shows how lookaheads and nested references can be used to match the non-regular languge anbn. Nested references are first introduced in: How does this regex find triangular numbers?
一种典型的非正规语言是:
L = { a nb n: n > 0 }
这是所有非空字符串的语言,由若干个
这种语言可以通过抽运引理证明是非正则的。它实际上是一种原型的上下文无关语言,可以由上下文无关语法
尽管如此,现代的regex实现显然不仅识别常规语言。也就是说,根据形式语言理论的定义,它们不是"规则的"。PCRE和Perl支持递归regex,.NET支持平衡组定义。甚至更少的"花哨"特性,例如backreference匹配,意味着regex不是常规的。
但这种"基本"功能到底有多强大?例如,我们能用Java正则表达式识别EDCOX1 6吗?我们是否可以将查找和嵌套引用结合起来,并有一个模式可以与
- Perlfaq6:我可以使用Perl正则表达式来匹配平衡文本吗?
- msdn-正则表达式语言元素-平衡组定义
- pcre.org-pcre手册页
- regular-expressions.info-查找、分组和回溯引用
java.util.regex.Pattern
链接的问题
- lookaround是否影响哪些语言可以与正则表达式匹配?
- .NET Regex平衡组与PCRE递归模式
答案是,不用说,是的!当然,您可以编写一个Java正则表达式来匹配ANNN。它对断言使用正的先行,对"计数"使用一个嵌套的引用。好的。
这个答案不是立即给出模式,而是引导读者完成推导模式的过程。在缓慢构造解决方案时,给出了各种提示。在这方面,希望这个答案包含的不仅仅是另一个整洁的regex模式。希望读者也能学会如何"在regex中思考",以及如何将各种结构和谐地结合在一起,这样他们就可以在将来自己获得更多的模式。好的。
用于开发解决方案的语言将是PHP,因为它简洁。最终的测试一旦模式完成,将在Java中完成。好的。步骤1:展望断言
让我们从一个更简单的问题开始:我们希望在字符串的开头匹配
下面是我们的模式和一个简单的测试工具:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function testAll($r, $tests) { foreach ($tests as $test) { $isMatch = preg_match($r, $test, $groups); $groupsJoined = join('|', $groups); print("$test $isMatch $groupsJoined "); } } $tests = array('aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb'); $r1 = '/^a+(?=b+)/'; # └────┘ # lookahead testAll($r1, $tests); |
输出是(在ideone.com上看到的):好的。
1 2 3 4 5 6 | aaa 0 aaab 1 aaa aaaxb 0 xaaab 0 b 0 abbb 1 a |
这正是我们想要的输出:我们匹配
教训:您可以在查找中使用模式来进行断言。好的。步骤2:在先行模式下捕获(和F R E-S P A C I N G模式)
现在让我们假设,尽管我们不希望
在前面的PHP代码段的基础上,我们现在有以下模式:好的。
1 2 3 4 5 6 7 | $r2 = '/ ^ a+ (?= (b+) ) /x'; # │ └──┘ │ # │ 1 │ # └────────┘ # lookahead testAll($r2, $tests); |
现在输出(如ideone.com上所示):好的。
1 2 3 4 5 6 | aaa 0 aaab 1 aaa|b aaaxb 0 xaaab 0 b 0 abbb 1 a|bbb |
请注意,例如,
教训:你可以在环顾四周的时候捕捉到。您可以使用自由间距来增强可读性。好的。第3步:将展望重构为"循环"
在介绍计数机制之前,我们需要对模式进行一次修改。目前,lookahead在
现在我们不必担心计数机制,只需按以下方式进行重构:好的。
- 首先将
a+ 重构为(?: a )+ (注意,(?:…) 是非捕获组) - 然后在这个非捕获组中向前看
- 注意,我们现在必须"跳过"
a* ,然后才能"看到"b+ ,因此相应地修改模式。
- 注意,我们现在必须"跳过"
现在我们有以下内容:好的。
1 2 3 4 5 6 7 | $r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x'; # │ │ └──┘ │ │ # │ │ 1 │ │ # │ └───────────┘ │ # │ lookahead │ # └───────────────────┘ # non-capturing group |
输出和以前一样(在ideone.com上看到),所以在这方面没有变化。重要的是,现在我们在
教训:您可以在非捕获组内捕获。可以重复观察。好的。第4步:这是我们开始计数的步骤
下面是我们要做的:我们将改写组1,使:好的。
- 在
+ 的第一次迭代结束时,当第一个a 匹配时,应捕获b 。 - 在第二次迭代结束时,当另一个
a 匹配时,它应该捕获bb 。 - 在第三次迭代结束时,它应该捕获
bbb 。 - …
- 在第n次迭代结束时,第1组应捕获bn
- 如果没有足够的
b 捕获到第1组中,那么断言就是失败了。
因此,现在的组1(EDOCX1)(13)必须重写为类似于
这里有一个小问题,这个模式缺少"基本情况",也就是说,在没有自引用的情况下,它可以匹配。基本情况是必需的,因为组1开始时"未初始化";它尚未捕获任何内容(甚至不是空字符串),因此自引用尝试将始终失败。好的。
有很多种方法可以解决这个问题,但现在我们只需要让自引用匹配成为可选的,即
1 2 3 4 5 6 7 8 9 10 11 | $tests = array( 'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb' ); $r4 = '/ ^ (?: a (?= a* (\1? b) ) )+ /x'; # │ │ └─────┘ | │ # │ │ 1 | │ # │ └──────────────┘ │ # │ lookahead │ # └──────────────────────┘ # non-capturing group |
现在输出(如ideone.com上所示):好的。
1 2 3 4 5 6 7 8 9 | aaa 0 aaab 1 aaa|b # (*gasp!*) aaaxb 0 xaaab 0 b 0 abbb 1 a|b # yes! aabb 1 aa|bb # YES!! aaabbbbb 1 aaa|bbb # YESS!!! aaaaabbb 1 aaaaa|bb # NOOOOOoooooo.... |
小精灵!看来我们已经接近解决方案了!我们使用自引用成功地使第1组"计数"!但是等等…第二个和最后一个测试用例有问题!!没有足够的
教训:初始化自引用组的一种方法是使自引用匹配成为可选的。好的。第4步?:了解出了什么问题
问题是,由于我们选择了自引用匹配,"计数器"可以在没有足够的
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 | a a a a a b b b ↑ # Initial state: Group 1 is"uninitialized". _ a a a a a b b b ↑ # 1st iteration: Group 1 couldn't match \1 since it was"uninitialized", # so it matched and captured just b ___ a a a a a b b b ↑ # 2nd iteration: Group 1 matched \1b and captured bb _____ a a a a a b b b ↑ # 3rd iteration: Group 1 matched \1b and captured bbb _ a a a a a b b b ↑ # 4th iteration: Group 1 could still match \1, but not \1b, # (!!!) so it matched and captured just b ___ a a a a a b b b ↑ # 5th iteration: Group 1 matched \1b and captured bb # # No more a, +"loop" terminates |
小精灵!在第四次迭代中,我们仍然可以匹配
但是要注意,除了第一次迭代,您总是可以只匹配自引用
教训:小心回溯。regex引擎将尽可能多地进行回溯,直到给定的模式匹配为止。这可能会影响性能(即灾难性的回溯)和/或正确性。好的。第五步:自救!
"修正"现在应该是显而易见的:把可选的重复和所有格量词结合起来。也就是说,不要简单地使用
在非常非正式的术语中,这是
?+
- (optional)"It doesn't have to be there,"
- (possessive)"but if it is there, you must take it and not let go!"
?
- (optional)"It doesn't have to be there,"
- (greedy)"but if it is you can take it for now,"
- (backtracking)"but you may be asked to let it go later!"
??
- (optional)"It doesn't have to be there,"
- (reluctant)"and even if it is you don't have to take it just yet,"
- (backtracking)"but you may be asked to take it later!"
在我们的设置中,
1 2 3 4 5 6 7 | $r5 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ /x'; # │ │ └──────┘ │ │ # │ │ 1 │ │ # │ └───────────────┘ │ # │ lookahead │ # └───────────────────────┘ # non-capturing group |
现在输出是(在ideone.com上看到的):好的。
1 2 3 4 5 6 7 8 9 | aaa 0 aaab 1 a|b # Yay! Fixed! aaaxb 0 xaaab 0 b 0 abbb 1 a|b aabb 1 aa|bb aaabbbbb 1 aaa|bbb aaaaabbb 1 aaa|bbb # Hurrahh!!! |
VORE!!!!问题解决了!!!!我们现在正按照我们想要的方式正确地计数!好的。
教训:学习贪婪、不情愿和占有欲重复之间的区别。可选的所有格可以是一个强大的组合。好的。第六步:收尾
所以我们现在得到的是一个重复匹配
为了完成这项工作,我们只需要附加到模式
下面是最终确定的模式,以及附加的测试用例,包括一个长度为10000个字符的测试用例:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 | $tests = array( 'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb', '', 'ab', 'abb', 'aab', 'aaaabb', 'aaabbb', 'bbbaaa', 'ababab', 'abc', str_repeat('a', 5000).str_repeat('b', 5000) ); $r6 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ \1 $ /x'; # │ │ └──────┘ │ │ # │ │ 1 │ │ # │ └───────────────┘ │ # │ lookahead │ # └───────────────────────┘ # non-capturing group |
它找到4个匹配项:
所以这个模式在PHP中起作用,但是最终的目标是编写一个在Java中工作的模式。好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public static void main(String[] args) { String aNbN ="(?x) (?: a (?= a* (\\1?+ b)) )+ \\1"; String[] tests = { "", // false "ab", // true "abb", // false "aab", // false "aabb", // true "abab", // false "abc", // false repeat('a', 5000) + repeat('b', 4999), // false repeat('a', 5000) + repeat('b', 5000), // true repeat('a', 5000) + repeat('b', 5001), // false }; for (String test : tests) { System.out.printf("[%s]%n %s%n%n", test, test.matches(aNbN)); } } static String repeat(char ch, int n) { return new String(new char[n]).replace('\0', ch); } |
该模式按预期工作(在ideone.com上看到)。好的。现在我们得出结论…
需要指出的是,展望中的
也应该说,虽然有一个与anbn匹配的regex模式是很好的,但这在实践中并不总是"最佳"的解决方案。更好的解决方案是简单地匹配
在PHP中,它可能看起来像这样(在ideone.com中可以看到):好的。
1 2 3 4 | function is_anbn($s) { return (preg_match('/^(a+)(b+)$/', $s, $groups)) && (strlen($groups[1]) == strlen($groups[2])); } |
本文的目的并不是要说服读者,Regex几乎可以做任何事情;它显然不能,甚至可以做的事情,如果它导致一个更简单的解决方案,至少应该考虑部分委托给宿主语言。好的。
正如上面提到的,虽然本文的stackoverflow必须标记为
既然我们提出了PHP,就需要说PCRE支持递归模式和子例程。因此,以下模式适用于
1 | $rRecursive = '/ ^ (a (?1)? b) $ /x'; |
目前Java的正则表达式不支持递归模式。好的。更多的奖励材料!匹配anbncn!!
所以我们已经看到了如何匹配anbnc,它是非正规的,但仍然是上下文无关的,但是我们也可以匹配anbnc,它甚至不是上下文无关的吗?好的。
答案当然是肯定的!鼓励读者自行解决这一问题,但下面提供解决方案(在IDENo.com上在爪哇上实现)。好的。
^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $ 好的。< /块引用>好啊。考虑到没有提到支持递归模式的PCRE,我只想指出描述所讨论语言的最简单和最有效的PCRE示例:
1 /^(a(?1)?b)$/如问题中所述-使用.NET平衡组,类型anbncdn&hellip;zn的模式可以很容易地匹配为
1
2
3
4
5
6
7 ^
(?<A>a)+
(?<B-A>b)+ (?(A)(?!))
(?<C-B>c)+ (?(B)(?!))
...
(?<Z-Y>z)+ (?(Y)(?!))
$例如:http://www.ideone.com/usooe
编辑:
对于具有递归模式的通用语言,也有一个PCRE模式,但是需要有一个前瞻性。我不认为这是上述内容的直接翻译。
1
2
3
4
5
6
7 ^
(?=(a(?-1)?b)) a+
(?=(b(?-1)?c)) b+
...
(?=(x(?-1)?y)) x+
(y(?-1)?z)
$例如:http://www.ideone.com/9guwf