我们如何将a ^ nb ^ n与Java正则表达式匹配?


99

这是一系列教育正则表达式文章的第二部分。它显示了如何使用前瞻和嵌套引用来匹配非常规语言a n b n。嵌套引用首先在以下地方引入:此正则表达式如何查找三角数?

原型非常规语言之一是:

L = { añ bñ: n > 0 }

这是所有非空字符串的语言,其中包括一定数量的,a后跟相等数量b的。在这个语言字符串的例子有abaabbaaabbb

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

尽管如此,现代正则表达式实现显然不仅仅可以识别常规语言。也就是说,根据形式语言理论的定义,它们不是“正常的”。PCRE和Perl支持递归正则表达式,.NET支持平衡组定义。更少的“花哨”功能(例如反向引用匹配)意味着正则表达式不是常规的。

但是,这种“基本”功能到底有多强大?L例如,我们可以用Java正则表达式识别吗?我们也许可以结合lookarounds和嵌套引用,并有一个模式,与如作品String.matches来匹配字符串一样abaabbaaabbb,等?

参考文献

关联问题


4
本系列文章是在社区中一些人的允许下启动的(meta.stackexchange.com/questions/62695/…)。如果接待良好,我计划继续涵盖正则表达式的其他更高级以及更基本的功能。
polygenelubricants 2010年


哇,我从不知道Java的正则表达式不会局限于正则表达式。我想这解释了为什么我一直认为它们不会完全实施。我的意思是Java Regexs中没有内置的补码,差异或产品运算符,但这是有道理的,因为它们不限于常规语言。
2013年

该问题已添加到“高级Regex-Fu”下的“ 堆栈溢出正则表达式常见问题解答 ”中。
aliteralmind 2014年

Answers:


139

答案是不用说,是的!你可以肯定写一个Java正则表达式匹配一个ñ b ñ。它使用肯定的前瞻性进行断言,并使用一个嵌套的引用进行“计数”。

该答案不是立即给出模式,而是将引导读者完成推导过程。随着解决方案的逐步构建,给出了各种提示。在这方面,希望这个答案将不仅仅包含另一个整洁的正则表达式模式。希望读者还将学习如何“思考正则表达式”,以及如何将各种结构和谐地组合在一起,以便他们将来可以自己衍生出更多模式。

为简洁起见,用于开发解决方案的语言将是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

请注意,例如aaa|bjoin每个组使用捕获的结果'|'。在这种情况下,捕获组0(即模式匹配的对象)aaa,并捕获组1 b

课程:您可以捕获一个环顾四周。您可以使用自由间距来增强可读性。


步骤3:将前瞻重构为“循环”

在介绍计数机制之前,我们需要对我们的模式进行一次修改。当前,前瞻位于+重复“循环”之外。到目前为止,这还不错,因为我们只是想断言b+我们后面有一个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)。也就是说,我们尝试将“添加” 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组“计数”!但是等等...第二个和最后一个测试用例出了点问题!bs 不够,以某种方式计入了错误!我们将在下一步研究为什么会发生这种情况。

课程:一种“初始化”自引用组的方法是使自引用匹配为可选。


步骤4½:了解问题所在

问题在于,由于我们将自引用匹配设置为可选,因此当计数器不足时,“计数器”可以“重置”为0 b。让我们仔细检查一下模式的每次迭代(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

请注意,但是,除了第一次迭代外,您始终可以仅匹配自引用\1。当然,这很明显,因为这是我们在上一次迭代中捕获的,并且在我们的设置中,我们始终可以再次对其进行匹配(例如,如果bbb上次捕获,则可以保证仍然存在bbb,但可能还是可能不是bbbb这次)。

课程:当心回溯。regex引擎将尽您所能进行尽可能多的回溯,直到给定的模式匹配为止。这可能会影响性能(即灾难性的回溯)和/或正确性。


第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!!!

哎呀!问题解决了!!!现在,我们正按照我们想要的方式正确计数!

课程:了解贪婪,勉强和所有格重复的区别。可选拥有可能是强大的组合。


步骤6:画龙点睛

因此,我们现在拥有的是一个a重复匹配的模式,对于每个匹配的模式,在组1中a都有一个对应的b捕获。+当不再存在时a,或者如果断言失败,因为没有对应的b,则终止。一个a

要完成工作,我们只需要追加到我们的pattern即可\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场比赛:abaabbaaabbb,和一个5000 b 5000。在ideone.com上运行仅需0.06s


步骤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*是,前瞻和“主+循环”都允许回溯。鼓励读者确认为什么这不是正确性方面的问题,以及为什么同时拥有所有格也起作用(尽管也许以相同的方式混合强制性和非强制性所有格量词可能会导致误解)。

还应该说,虽然有一个匹配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]为stackoverflow,但它可能不止于此。虽然肯定有学习断言,嵌套引用,所有格限定符等的价值,但是也许更大的一课是创造性的过程,通过该过程人们可以尝试解决问题,遇到困难时通常需要的决心和努力工作各种约束,从各个部分的系统组成,以建立有效的解决方案,等等。


奖金材料!PCRE递归模式!

由于我们确实提出了PHP,因此需要说PCRE支持递归模式和子例程。因此,以下模式适用于preg_match如在ideone.com上所示):

$rRecursive = '/ ^ (a (?1)? b) $ /x';

当前,Java的正则表达式不支持递归模式。


更多的奖励材料!匹配一个n b n c n

因此,我们已经看到了如何匹配非常规n b n,但是仍然与上下文无关,但是我们还可以匹配甚至不是上下文无关n b n c n吗?

答案是肯定的!鼓励读者尝试自己解决这个问题,但是下面提供了解决方案(ideone.com上的Java实现)。

^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $


毫无疑问,这个冗长的答案中可能存在错误/错别字,因此请留下反馈作为评论,以便我自己进行修改。
polygenelubricants 2010年

很好。我要花点时间才能读完,但是最后一行基本上是不可能读的;它是这么小的字体。 - - - 等一下。那是feature?....不确定这是个好主意。我知道最后一个符号是什么,但无法读取(除了复制粘贴外)。
彼得·阿杰泰

6
@Peter:突出显示小文本,然后复制并粘贴到其他内容中。故意阅读很难:它是破坏者,是奖金难题的解决方案。
polygenelubricants 2010年

8
+1:奇妙的解释,这些“高级文章”是很棒的主意。
卡勒姆·罗杰斯

1
@LarsH PHP preg_match()PCRE的示例。Java regexes 似乎基于Perl regexps的旧版本。这意味着PHP regexes比Java版本更强大。截至2013年2月21日pcre.txt说,它用Perl 5.12大约对应。虽然Perl目前为5.​​16,但还有5.18个月的折扣。(在那时,正则表达式实际上没有增加太多)
Brad Gilbert

20

鉴于没有提及支持递归模式的PCRE,我只想指出描述问题语言的PCRE最简单,最有效的示例:

/^(a(?1)?b)$/

+1哇,我不知道PCRE是否支持递归模式(我还在学习!每天!)。我已经对文章进行了修改以适应这些信息。我不认为递归模式可以匹配a^n b^n c^n
polygenelubricants 2010年

应该注意的是,此选项比较简单,但不如发布的答案好-递归在长字符串上溢出。
科比

@Kobi这取决于您对“好”的定义。例如,递归解决方案比另一个解决方案快一个数量级(codepad.viper-7.com/CWgy7c)。而且它更容易理解。递归解决方案几乎是将语法直接转换为正则表达式(实际上,您可以将其以语法形式编写出来,就可以了)。
NikiC 2011年

1
@polygeniclubricants,您可以将该模式与两个递归模式进行匹配,一个模式不消耗a而消耗s和bs(并验证递归的量是否相同),然后是一个贪婪地消耗所有a的捕获正则表达式,然后应用递归要使用的模式,并验证bs和cs的数目相同。正则表达式为:/^(?=(a(?-1)?b)c)a+(b(?-1)?c)$/x。感谢:nikic.github.io/2012/06/15/...
乔什-瑞贝克

11

正如问题中提到的那样-使用.NET平衡组,可以很容易地匹配a n b n c n d n …z n类型的模式

^
  (?<A>a)+
  (?<B-A>b)+  (?(A)(?!))
  (?<C-B>c)+  (?(B)(?!))
  ...
  (?<Z-Y>z)+  (?(Y)(?!))
$

例如:http : //www.ideone.com/usuOE


编辑:

对于具有递归模式的通用语言,也有一个PCRE模式,但是需要提前。我不认为这是上述内容的直接翻译。

^
  (?=(a(?-1)?b))  a+
  (?=(b(?-1)?c))  b+
  ...
  (?=(x(?-1)?y))  x+
     (y(?-1)?z)
$

例如:http : //www.ideone.com/9gUwF


1
@poly:谢谢:)。实际上我并不熟悉.NET模式,但是对于这种模式,事实证明,使用平衡组非常容易,因此我补充了这个答案。
kennytm 2010年

您可以使用递归模式吗?因为如果不能,那是一个有趣的转折,那就是平衡组可以执行递归模式无法执行的操作。(是的,我非常感谢补编)。
polygenelubricants 2010年

顺便说一句,我省略.NET解决方案的原因是因为我确实有“如何a^n b^n与.NET正则表达式匹配”的计划。将来的文章,但是如果您愿意的话,欢迎您继续写。我不是为了我自己而写这些文章。我确实希望鼓励其他人也这样做,以便在网站上拥有良好的内容。
多基因润滑剂

如果您找到使用递归模式的方法,请进行更新。我与平衡小组一起玩耍,以捕捉其长度构成斐波那契数列的单词,但无法使其正常工作。使用环顾四周,与我所做的类似,这是可能的。
科比

1
我只想指出,此模式的PCRE版本存在一些缺陷,因为如果下一个字符块比上一个字符长,则它可以匹配。请参阅此处:regex101.com/r/sdlRTm/1您需要在捕获组之后添加(?!b)(?!c)等等,如下所示:regex101.com/r/sdlRTm/2
jaytea
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.