答案是,不用说,是的!您当然可以编写一个 Java 正则表达式模式来匹配a n b n。它对断言使用肯定的前瞻,对“计数”使用一个嵌套引用。
这个答案不是立即给出模式,而是引导读者完成推导它的过程。随着解决方案的缓慢构建,给出了各种提示。在这方面,希望这个答案不仅仅包含另一个简洁的正则表达式模式。希望读者也能学习如何“用正则表达式思考”,如何将各种构造和谐地组合在一起,以便他们将来可以自己推导出更多的模式。
用于开发解决方案的语言将是 PHP,因为它简洁。模式最终确定后的最终测试将在 Java 中完成。
第 1 步:前瞻断言
让我们从一个更简单的问题开始:我们想要匹配a+
字符串的开头,但前提是它后面紧跟b+
. 我们可以使用^
来锚定我们的匹配,并且由于我们只想匹配a+
没有 的b+
,我们可以使用前瞻断言(?=…)
。
这是我们的带有简单测试工具的模式:
function testAll($r, $tests) {
foreach ($tests as $test) {
$isMatch = preg_match($r, $test, $groups);
$groupsJoined = join('|', $groups);
print("$test $isMatch $groupsJoined\n");
}
}
$tests = array('aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb');
$r1 = '/^a+(?=b+)/';
# └────┘
# lookahead
testAll($r1, $tests);
输出是(如在 ideone.com 上看到的):
aaa 0
aaab 1 aaa
aaaxb 0
xaaab 0
b 0
abbb 1 a
这正是我们想要的输出:我们匹配a+
,仅当它位于字符串的开头,并且仅当它紧跟在b+
.
教训:您可以在环视中使用模式来进行断言。
第 2 步:在前瞻(和自由间距模式)中捕获
现在假设即使我们不希望b+
成为比赛的一部分,但我们确实希望将其捕获到第 1 组中。另外,由于我们预计会有更复杂的模式,让我们使用x
修饰符来自由间距,所以我们可以使我们的正则表达式更具可读性。
基于我们之前的 PHP 片段,我们现在有以下模式:
$r2 = '/ ^ a+ (?= (b+) ) /x';
# │ └──┘ │
# │ 1 │
# └────────┘
# lookahead
testAll($r2, $tests);
现在的输出是(如在 ideone.com 上看到的):
aaa 0
aaab 1 aaa|b
aaaxb 0
xaaab 0
b 0
abbb 1 a|bbb
请注意, egaaa|b
是join
-ing 每个组使用 捕获的结果'|'
。在这种情况下,第 0 组(即模式匹配的内容)被捕获aaa
,第 1 组被捕获b
。
课程:您可以在环视中捕捉。您可以使用自由间距来增强可读性。
第 3 步:将前瞻重构为“循环”
在我们介绍我们的计数机制之前,我们需要对我们的模式进行一次修改。目前,前瞻在+
重复“循环”之外。到目前为止这很好,因为我们只是想断言有一个b+
跟随 our a+
,但我们最终真正想做的是断言对于a
我们在“循环”中匹配的每个,都有一个对应b
的与之对应。
我们暂时不用担心计数机制,只需按如下方式进行重构:
- 首先重构
a+
为(?: a )+
(注意(?:…)
是非捕获组)
- 然后在这个非捕获组内移动前瞻
- 请注意,我们现在必须“跳过”
a*
才能“看到” b+
,因此相应地修改模式
所以我们现在有以下内容:
$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
# │ │ └──┘ │ │
# │ │ 1 │ │
# │ └───────────┘ │
# │ lookahead │
# └───────────────────┘
# non-capturing group
输出与以前相同(如在 ideone.com 上所见),因此在这方面没有变化。重要的是,现在我们在“循环”的每次迭代中都做出断言。+
对于我们当前的模式,这不是必需的,但接下来我们将使用自引用为我们“计数”第 1 组。
课程:您可以在非捕获组内捕获。可以重复环顾四周。
第 4 步:这是我们开始计数的步骤
下面是我们要做的:我们将重写第 1 组,这样:
- 在 的第一次迭代结束时
+
,当第一次a
匹配时,它应该捕获b
- 在第二次迭代结束时,当另一个
a
匹配时,它应该捕获bb
- 在第三次迭代结束时,它应该捕获
bbb
- ...
- 在第n次迭代结束时,第 1 组应捕获b n
- 如果没有足够
b
的数据捕获到第 1 组,那么断言就会失败
因此,现在(b+)
的第 1 组将不得不重写为类似(\1 b)
. 也就是说,我们尝试将 a “添加”b
到上一次迭代中捕获的组 1 中。
这里有一个小问题,这个模式缺少“基本情况”,即它可以在没有自引用的情况下匹配的情况。需要一个基本情况,因为第 1 组开始“未初始化”;它还没有捕获任何东西(甚至没有一个空字符串),所以自引用尝试总是会失败。
有很多方法可以解决这个问题,但现在让我们将自引用匹配设为可选,即\1?
. 这可能会或可能不会完美地工作,但让我们看看它做了什么,如果有任何问题,那么当我们遇到它时,我们会越过那座桥。此外,我们将在此过程中添加更多测试用例。
$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb'
);
$r4 = '/ ^ (?: a (?= a* (\1? b) ) )+ /x';
# │ │ └─────┘ | │
# │ │ 1 | │
# │ └──────────────┘ │
# │ lookahead │
# └──────────────────────┘
# non-capturing group
现在的输出是(如在 ideone.com 上看到的):
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 组使用自我参考来“计数”!但是等等……第二个也是最后一个测试用例出了点问题!!s不够多b
,不知怎么算错了!我们将在下一步检查为什么会发生这种情况。
教训:“初始化”自引用组的一种方法是使自引用匹配成为可选的。
步骤 4½:了解出了什么问题
b
问题是,由于我们将自引用匹配设为可选,当's不足时,“计数器”可以“重置”回 0 。aaaaabbb
让我们仔细检查作为输入的模式的每次迭代都会发生什么。
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
啊哈!在我们的第 4 次迭代中,我们仍然可以匹配\1
,但我们无法匹配\1b
!由于我们允许自引用匹配是可选的\1?
,因此引擎会回溯并采用“不,谢谢”选项,这样我们就可以匹配和捕获b
!
但是请注意,除了第一次迭代之外,您始终可以只匹配 self-reference \1
。当然,这是显而易见的,因为它是我们刚刚在上一次迭代中捕获的,并且在我们的设置中,我们总是可以再次匹配它(例如,如果我们bbb
上次捕获,我们保证仍然会有bbb
,但可能或bbbb
这次可能不是)。
教训:当心回溯。正则表达式引擎将尽可能多地进行回溯,直到给定的模式匹配。这可能会影响性能(即灾难性回溯)和/或正确性。
第 5 步:自我控制来拯救!
“修复”现在应该很明显:将可选重复与所有格量词结合起来。也就是说,不要简单地?
使用,而是使用?+
(请记住,被量化为所有格的重复不会回溯,即使这种“合作”可能会导致整体模式的匹配)。
用非常非正式的术语来说,这就是?+
,?
并??
说:
?+
- (可选)“它不必在那里,”
- (占有欲)“但如果它在那里,你必须把它拿走,不能放手!”
?
??
在我们的设置中,\1
它不会在第一次出现,但之后的任何时候都会出现,并且我们总是想在那时匹配它。因此,将完全实现我们想要的。\1?+
$r5 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
现在输出是(如在 ideone.com 上看到的):
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!!!
瞧!!!问题解决了!!!我们现在正在正确地计算,正是我们想要的方式!
课程:了解贪婪、不情愿和所有格重复之间的区别。Optional-Posssive 可以是一个强大的组合。
第 6 步:收尾工作
所以我们现在拥有的是一个a
重复匹配的模式,对于每一个匹配的模式,在组 1 中a
都有一个对应的b
捕获。+
当没有更多时终止a
,或者如果断言失败,因为没有对应b
的 for一个a
。
要完成这项工作,我们只需要附加到我们的模式\1 $
。现在这是对第 1 组匹配的内容的反向引用,然后是行锚的末尾。锚确保字符串中没有任何多余b
的 ';换句话说,事实上我们有一个n b n。
这是最终的模式,带有额外的测试用例,包括一个 10,000 个字符长的测试用例:
$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 个匹配项:ab
、aabb
、aaabbb
和a 5000 b 5000。在ideone.com 上运行仅需 0.06 秒。
第 7 步:Java 测试
所以该模式适用于 PHP,但最终目标是编写一个适用于 Java 的模式。
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 上所见)。
现在我们得出结论……
需要说的是,a*
在前瞻中,实际上是“主+
循环”,都允许回溯。鼓励读者确认为什么这在正确性方面不是问题,以及为什么同时使两个所有格都可以工作(尽管在同一模式中混合强制和非强制所有格量词可能会导致误解)。
还应该说,虽然有一个匹配 n b n 的正则表达式模式很整洁,但这在实践中并不总是“最佳”解决方案。一个更好的解决方案是简单地匹配^(a+)(b+)$
,然后比较宿主编程语言中组 1 和组 2 捕获的字符串的长度。
在 PHP 中,它可能看起来像这样(见 ideone.com):
function is_anbn($s) {
return (preg_match('/^(a+)(b+)$/', $s, $groups)) &&
(strlen($groups[1]) == strlen($groups[2]));
}
本文的目的不是要让读者相信 regex 几乎可以做任何事情。它显然不能,即使它可以做的事情,如果它导致更简单的解决方案,至少应该考虑部分委托给宿主语言。
如顶部所述,虽然这篇文章必须标记[regex]
为 stackoverflow,但它可能不止于此。虽然学习断言、嵌套引用、所有格量词等当然是有价值的,但这里更大的教训可能是人们可以尝试解决问题的创造性过程,当你遇到问题时通常需要的决心和努力工作各种约束,各个部分的系统组合以构建工作解决方案等。
奖励材料!PCRE递归模式!
由于我们确实提出了 PHP,所以需要说 PCRE 支持递归模式和子例程。因此,以下模式适用于preg_match
(如在 ideone.com 上所见):
$rRecursive = '/ ^ (a (?1)? b) $ /x';
目前 Java 的正则表达式不支持递归模式。
更多奖励材料!匹配一个n b n c n !!
所以我们已经看到了如何匹配非常规但仍然与上下文无关的a n b n,但是我们是否也可以匹配甚至不是上下文无关的a n b n c n ?
答案当然是肯定的!鼓励读者尝试自己解决这个问题,但下面提供了解决方案(ideone.com 上的 Java 实现)。
^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $