Regex 我们如何匹配a^n b^n?
这是教育正则表达式系列文章的第二部分。它展示了如何使用lookaheads和嵌套引用来匹配非规则语言anbn。嵌套引用首先在中介绍: 其中一个典型的非冲突因素是:Regex 我们如何匹配a^n b^n?,regex,regex-lookarounds,capturing-group,nested-reference,Regex,Regex Lookarounds,Capturing Group,Nested Reference,这是教育正则表达式系列文章的第二部分。它展示了如何使用lookaheads和嵌套引用来匹配非规则语言anbn。嵌套引用首先在中介绍: 其中一个典型的非冲突因素是: L={anbn:n>0} 这是所有非空字符串的语言,由一定数量的a,后跟相等数量的b。这种语言中的字符串示例有ab,aabb,aaabbb 此语言可以通过以下方式显示为非规则语言。它实际上是一个原型,可以由S生成→ aSb | ab 尽管如此,现代的正则表达式实现显然不仅仅识别常规语言。也就是说,它们不是形式语言理论定义的“规则”。P
L={a
nb
n:n>0}
这是所有非空字符串的语言,由一定数量的a
,后跟相等数量的b
。这种语言中的字符串示例有ab
,aabb
,aaabbb
此语言可以通过以下方式显示为非规则语言。它实际上是一个原型,可以由S生成→ aSb | ab
尽管如此,现代的正则表达式实现显然不仅仅识别常规语言。也就是说,它们不是形式语言理论定义的“规则”。PCRE和Perl支持递归正则表达式,.NET支持平衡组定义。甚至更少的“花哨”特性,例如反向引用匹配,意味着正则表达式不是规则的
但这种“基本”功能到底有多强大?例如,我们可以用Java正则表达式识别L
?我们是否可以将lookarounds和嵌套引用结合起来,并使用一种模式来匹配字符串,例如ab
,aabb
,aaabbb
,等等
工具书类
- -及
- 答案是,不用说,是的!您完全可以编写一个Java正则表达式模式来匹配anbn。它对断言使用正向前瞻,对“计数”使用嵌套引用
这个答案不是立即给出模式,而是引导读者完成推导过程。在缓慢构造解决方案时,会给出各种提示。在这方面,希望这个答案不仅仅包含另一个整洁的正则表达式模式。希望读者也能学会如何“在正则表达式中思考”,以及如何将各种结构和谐地组合在一起,以便他们将来能够自己推导出更多的模式
由于其简洁性,用于开发解决方案的语言将是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);
输出为():
这正是我们想要的输出:我们匹配a+
,前提是它位于字符串的开头,并且紧跟b+
课程:您可以使用lookarounds中的模式进行断言
步骤2:以前瞻模式捕获(和f r e-s p c i n g模式) 现在让我们假设,即使我们不希望
b+
成为比赛的一部分,我们还是希望它进入第1组。另外,由于我们预期会有一个更复杂的模式,所以让我们使用x
修饰符,以便使正则表达式更具可读性
在前面的PHP代码段的基础上,我们现在有以下模式:
$r2 = '/ ^ a+ (?= (b+) ) /x';
# │ └──┘ │
# │ 1 │
# └────────┘
# lookahead
testAll($r2, $tests);
现在输出为():
请注意,例如,aaa | b
是join
-ing每个组用“|”捕获的内容的结果。在这种情况下,组0(即模式匹配的内容)捕获了aaa
,组1捕获了b
课程:您可以捕获周围环境中的内容。您可以使用自由间距来增强可读性
步骤3:将前瞻重构为“循环”
在引入计数机制之前,我们需要对模式进行一次修改。目前,前瞻在+
重复“循环”之外。到目前为止这还不错,因为我们只是想断言在我们的a+/code>之后有一个b+/code>,但我们最终真正想做的是断言,对于我们在“循环”中匹配的每个a
,都有一个相应的b
现在我们不必担心计数机制,只需按照以下步骤进行重构:
- 首先将
a+
重构为(?:a)+
(请注意,(?:…)
是一个非捕获组)
- 然后在该非捕获组内移动前瞻
- 请注意,我们现在必须先“跳过”
a*
,然后才能“看到”b+
,因此相应地修改模式
因此,我们现在有以下几点:
$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
# │ │ └──┘ │ │
# │ │ 1 │ │
# │ └───────────┘ │
# │ lookahead │
# └───────────────────┘
# non-capturing group
输出与前面()相同,因此在这方面没有变化。重要的是,现在我们正在+
循环的每次迭代中进行断言。在我们当前的模式下,这是不必要的,但接下来我们将使用自引用为第1组“计数”
课程:您可以在非捕获组内捕获。环顾四周可以重复
步骤4:这是我们开始计数的步骤
下面是我们要做的:我们将重写第1组,以便:
- 在
+
的第一次迭代结束时,当第一次a
匹配时,它应该捕获b
- 在第二次迭代结束时,当另一个
a
匹配时,它应该捕获bb
- 在第三次迭代结束时,它应该捕获
bbb
- 在第n次迭代结束时,第1组应捕获bn
- 如果没有足够的
b
捕获到组1中,那么断言就会失败
因此,组1现在是(b+)
,必须重写
aaa 0
aaab 1 aaa|b
aaaxb 0
xaaab 0
b 0
abbb 1 a|bbb
$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
# │ │ └──┘ │ │
# │ │ 1 │ │
# │ └───────────┘ │
# │ lookahead │
# └───────────────────┘
# non-capturing group
$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb'
);
$r4 = '/ ^ (?: a (?= a* (\1? b) ) )+ /x';
# │ │ └─────┘ | │
# │ │ 1 | │
# │ └──────────────┘ │
# │ lookahead │
# └──────────────────────┘
# non-capturing group
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....
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
$r5 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
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!!!
$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
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);
}
function is_anbn($s) {
return (preg_match('/^(a+)(b+)$/', $s, $groups)) &&
(strlen($groups[1]) == strlen($groups[2]));
}
$rRecursive = '/ ^ (a (?1)? b) $ /x';
/^(a(?1)?b)$/
^
(?<A>a)+
(?<B-A>b)+ (?(A)(?!))
(?<C-B>c)+ (?(B)(?!))
...
(?<Z-Y>z)+ (?(Y)(?!))
$
^
(?=(a(?-1)?b)) a+
(?=(b(?-1)?c)) b+
...
(?=(x(?-1)?y)) x+
(y(?-1)?z)
$