我们怎样才能将^ n b ^ n与Java正则表达式匹配?

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 = { anbn: n > 0 }

这是所有非空字符串的语言,由若干个a和等量的b组成。这种语言中的字符串示例有abaabbaaabbb

这种语言可以通过抽运引理证明是非正则的。它实际上是一种原型的上下文无关语言,可以由上下文无关语法S → aSb | ab生成。

尽管如此,现代的regex实现显然不仅识别常规语言。也就是说,根据形式语言理论的定义,它们不是"规则的"。PCRE和Perl支持递归regex,.NET支持平衡组定义。甚至更少的"花哨"特性,例如backreference匹配,意味着regex不是常规的。

但这种"基本"功能到底有多强大?例如,我们能用Java正则表达式识别EDCOX1 6吗?我们是否可以将查找和嵌套引用结合起来,并有一个模式可以与String.matches一起使用,以匹配字符串,如abaabbaaabbb等?

工具书类

  • Perlfaq6:我可以使用Perl正则表达式来匹配平衡文本吗?
  • msdn-正则表达式语言元素-平衡组定义
  • pcre.org-pcre手册页
  • regular-expressions.info-查找、分组和回溯引用
  • java.util.regex.Pattern

链接的问题

  • lookaround是否影响哪些语言可以与正则表达式匹配?
  • .NET Regex平衡组与PCRE递归模式


答案是,不用说,是的!当然,您可以编写一个Java正则表达式来匹配ANNN。它对断言使用正的先行,对"计数"使用一个嵌套的引用。好的。

这个答案不是立即给出模式,而是引导读者完成推导模式的过程。在缓慢构造解决方案时,给出了各种提示。在这方面,希望这个答案包含的不仅仅是另一个整洁的regex模式。希望读者也能学会如何"在regex中思考",以及如何将各种结构和谐地结合在一起,这样他们就可以在将来自己获得更多的模式。好的。

用于开发解决方案的语言将是PHP,因为它简洁。最终的测试一旦模式完成,将在Java中完成。好的。步骤1:展望断言

让我们从一个更简单的问题开始:我们希望在字符串的开头匹配a+,但前提是后面紧跟b+。我们可以使用^来锚定我们的匹配,因为我们只想匹配a+,而不使用b+,所以我们可以使用lookahead断言(?=…)。好的。

下面是我们的模式和一个简单的测试工具:好的。

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

这正是我们想要的输出:我们匹配a+,仅当它在字符串的开头时,并且仅当它紧跟b+时。好的。

教训:您可以在查找中使用模式来进行断言。好的。步骤2:在先行模式下捕获(和F R E-S P A C I N G模式)

现在让我们假设,尽管我们不希望b+成为比赛的一部分,但我们还是希望把它捕获到第1组。另外,正如我们预计会有一个更复杂的模式,让我们使用x修饰符来实现自由间距,这样我们可以使regex更可读。好的。

在前面的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

请注意,例如,aaa|bjoin的结果,每个组用'|'捕获了什么。在这种情况下,组0(即模式匹配的内容)捕获aaa,组1捕获b。好的。

教训:你可以在环顾四周的时候捕捉到。您可以使用自由间距来增强可读性。好的。第3步:将展望重构为"循环"

在介绍计数机制之前,我们需要对模式进行一次修改。目前,lookahead在+重复"循环"之外。到目前为止这还不错,因为我们只是想断言在我们的a+之后有一个b+,但我们最终真正想做的是断言,对于每个a我们在"循环"中匹配,有一个对应的b与之匹配。好的。

现在我们不必担心计数机制,只需按以下方式进行重构:好的。

  • 首先将a+重构为(?: a )+(注意,(?:…)是非捕获组)
  • 然后在这个非捕获组中向前看
    • 注意,我们现在必须"跳过"a*,然后才能"看到"b+,因此相应地修改模式。

现在我们有以下内容:好的。

1
2
3
4
5
6
7
$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
#          │     │      └──┘ │ │
#          │     │        1  │ │
#          │     └───────────┘ │
#          │       lookahead   │
#          └───────────────────┘
#           non-capturing group

输出和以前一样(在ideone.com上看到),所以在这方面没有变化。重要的是,现在我们在+循环的每一次迭代中都做出了断言。对于我们当前的模式,这是不必要的,但接下来我们将使用自引用使组1"计数"。好的。

教训:您可以在非捕获组内捕获。可以重复观察。好的。第4步:这是我们开始计数的步骤

下面是我们要做的:我们将改写组1,使:好的。

  • +的第一次迭代结束时,当第一个a匹配时,应捕获b
  • 在第二次迭代结束时,当另一个a匹配时,它应该捕获bb
  • 在第三次迭代结束时,它应该捕获bbb
  • 在第n次迭代结束时,第1组应捕获bn
  • 如果没有足够的b捕获到第1组中,那么断言就是失败了。

因此,现在的组1(EDOCX1)(13)必须重写为类似于(\1 b)的内容。也就是说,我们试图"添加"一个b到上一次迭代中捕获的组1中。好的。

这里有一个小问题,这个模式缺少"基本情况",也就是说,在没有自引用的情况下,它可以匹配。基本情况是必需的,因为组1开始时"未初始化";它尚未捕获任何内容(甚至不是空字符串),因此自引用尝试将始终失败。好的。

有很多种方法可以解决这个问题,但现在我们只需要让自引用匹配成为可选的,即\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组"计数"!但是等等…第二个和最后一个测试用例有问题!!没有足够的bs,不知何故算错了!我们将在下一步中研究为什么会发生这种情况。好的。

教训:初始化自引用组的一种方法是使自引用匹配成为可选的。好的。第4步?:了解出了什么问题

问题是,由于我们选择了自引用匹配,"计数器"可以在没有足够的b的情况下"重置"回0。让我们仔细检查一下在以aaaaabbb作为输入的模式每次迭代中会发生什么。好的。

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

小精灵!在第四次迭代中,我们仍然可以匹配\1,但是我们不能匹配\1b!由于我们允许自引用匹配与\1?可选,因此引擎会后退并采用"不谢谢"选项,这样我们就可以匹配并捕获b!好的。

但是要注意,除了第一次迭代,您总是可以只匹配自引用\1。当然,这是显而易见的,因为这是我们在上一次迭代中捕捉到的,并且在我们的设置中,我们总是可以再次匹配它(例如,如果我们上次捕获了bbb,我们可以保证仍然会有bbb,但这次可能会有,也可能不会有bbbb。好的。

教训:小心回溯。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不会第一次出现在那里,但在那之后的任何时候都会出现,我们总是想匹配它。因此,\1?+将完全满足我们的需要。好的。

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!!!!问题解决了!!!!我们现在正按照我们想要的方式正确地计数!好的。

教训:学习贪婪、不情愿和占有欲重复之间的区别。可选的所有格可以是一个强大的组合。好的。第六步:收尾

所以我们现在得到的是一个重复匹配a的模式,对于每个匹配的a,在第1组中有一个对应的b。当没有更多的a时,或者如果断言失败,因为a没有对应的b时,+终止。好的。

为了完成这项工作,我们只需要附加到模式\1 $中。现在,这是对第1组匹配内容的向后引用,后面是行尾锚。锚确保字符串中没有任何多余的b;换句话说,实际上我们有anbn。好的。

下面是最终确定的模式,以及附加的测试用例,包括一个长度为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个匹配项:abaabbaaabbb和a5000b5000。在ideone.com上运行只需要0.06秒。好的。第7步:Java测试

所以这个模式在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上看到)。好的。现在我们得出结论…

需要指出的是,展望中的a*,实际上是"主+"循环,都允许回溯。我们鼓励读者确认,为什么在正确性方面这不是一个问题,为什么同时使两个所有格也能起作用(尽管在同一模式中混合强制性和非强制性所有格量词可能会导致误解)。好的。

也应该说,虽然有一个与anbn匹配的regex模式是很好的,但这在实践中并不总是"最佳"的解决方案。更好的解决方案是简单地匹配^(a+)(b+)$,然后比较托管编程语言中组1和组2捕获的字符串的长度。好的。

在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必须标记为[regex],但它可能不止这个。当然,在学习断言、嵌套引用、所有格量词等方面是有价值的,也许这里更重要的一课是一个创造性的过程,通过这个过程,人们可以尝试解决问题,当你受到各种约束时它通常需要的决心和努力工作,来自不同部分的系统组成。建立有效的解决方案等。好的。奖励材料!PCRE递归模式!

既然我们提出了PHP,就需要说PCRE支持递归模式和子例程。因此,以下模式适用于preg_match(参见ideone.com):好的。

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