どのようにしてa ^ nb ^ nをJava正規表現と一致させることができますか?


99

これは一連の教育的正規表現の記事の第2部です。これは、先読みとネストされた参照を使用して、非正規言語a n b nと一致させる方法を示しています。入れ子になった参照は最初に紹介されています:この正規表現はどのようにして三角形の数を見つけますか?

典型的な非正規言語の 1つは次のとおりです。

L = { an bn: n > 0 }

これは、いくつかaのの後に同じ数のが続く空でないすべての文字列の言語ですb。この言語の文字列の例はabaabbaaabbb

この言語は、ポンプの補題によって非規則的であることを示すことができます。これは実際には、文脈自由文法によって生成できる典型的な文脈自由言語です。 S → aSb | ab

それにもかかわらず、現代の正規表現の実装は、通常の言語だけではなく、それ以上のものを明確に認識します。つまり、それらは正式な言語理論の定義では「通常」ではありません。PCREとPerlは再帰正規表現をサポートし、.NETはバランスグループ定義をサポートします。「後方参照マッチング」などの「ファンシー」機能がさらに少ないということは、正規表現が規則的でないことを意味します。

しかし、この「基本」機能はどれほど強力なのでしょうか。Lたとえば、Java regexで認識できますか?我々は、おそらく前後参照とネストされた参照を組み合わせて、例えばで動作パターン持つことができるString.matchesような文字列と一致するようにabaabbaaabbbなど、?

参考文献

リンクされた質問


4
このシリーズは、コミュニティ(meta.stackexchange.com/questions/62695/…)の一部の許可を得て開始されました。レセプションが良ければ、regexのより高度で基本的な機能をカバーするつもりです。
polygenelubricants


うわー、私はJavaの正規表現が正規表現に制限されないことを知りませんでした。それが、私はいつもそれらが完全に実装されないと思っていた理由を説明していると思います。つまり、Java正規表現に組み込まれている補完演算子、差分演算子、製品演算子はありませんが、それらは通常の言語に限定されていないため、理にかなっています。
Lan

この質問は、「Advanced Regex-Fu」の下のStack Overflow Regular Expression FAQに追加されました。
aliteralmind 2014

回答:


139

答えは言うまでもなく、はい!a n b nに一致する Java正規表現パターンを作成できます。アサーションには正の先読みを使用し、「カウント」には1つのネストされた参照を使用します。

この回答は、すぐにパターンを示すのではなく、それを導き出すプロセスを読者に案内します。ソリューションがゆっくりと構築されるにつれて、さまざまなヒントが与えられます。この側面では、うまくいけば、この回答には単なる別の正規表現パターンよりもはるかに多くのものが含まれます。読者が「正規表現で考える」方法や、さまざまな構成要素を調和のとれた方法で組み合わせて、将来自分自身でより多くのパターンを導き出す方法についても学んでいただければ幸いです。

ソリューションの開発に使用される言語は、簡潔にするために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各グループがでキャプチャした-ing の結果であることに注意してください'|'。この場合、グループ0(つまり、パターンが一致したもの)がキャプチャーしaaa、グループ1がキャプチャーしましたb

レッスン:ルックアラウンド内でキャプチャできます。読みやすさを向上させるためにフリースペースを使用できます。


ステップ3:先読みを「ループ」にリファクタリングする

カウントメカニズムを紹介する前に、パターンに1つの変更を加える必要があります。現在、先読みは+繰り返し「ループ」の外にあります。これまでのところ、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
  • 2番目の反復の終わりに、別の反復aが一致すると、bb
  • 3回目の反復の終わりに、 bbb
  • ...
  • n番目の反復の終わりに、グループ1はb nをキャプチャする必要があります
  • bグループ1に取り込むのに十分でない場合、アサーションは単に失敗します

したがって、現在のグループ1は、の(b+)ように書き直す必要があります(\1 b)。つまりb、前の反復でキャプチャしたグループ1にaを「追加」しようとします。

このパターンには、「基本ケース」、つまり自己参照なしで一致する可能性があるケースがないため、わずかな問題があります。グループ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を「カウント」することができました。しかし、ちょっと待ってください... 2番目と最後のテストケースに問題があります!! 十分なbs がありません。どういうわけかそれは間違って数えられました!これが次のステップで起こった理由を調べます。

レッスン:自己参照グループを「初期化」する1つの方法は、自己参照マッチングをオプションにすることです。


ステップ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今回はないかもしれません)。

レッスン:バックトラッキングに注意してください。正規表現エンジンは、指定されたパターンが一致するまで、可能な限り多くのバックトラックを実行します。これは、パフォーマンス(つまり、致命的なバックトラック)や正確性に影響を与える可能性があります。


ステップ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-Posessiveは強力な組み合わせになります。


ステップ6:仕上げの仕上げ

私たちが今持っていることはパターンが一致したということですのでa、繰り返しは、すべてのためにa一致したこと、対応が存在しないbグループ1で撮影し+、それ以上が存在する場合には終了するa対応していないがあるので、またはアサーションが失敗した場合bのためにa

ジョブを完了するには、パターンに追加するだけです\1 $。これは、グループ1が一致したものへの後方参照になり、その後に行アンカーの終わりが続きます。アンカーはb文字列に余分ながないことを保証します。つまり、実際にはa 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 5000ideone.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*先読みと実際の「メイン+ループ」の両方でバックトラックが可能であると言う必要があります。読者は、これが正確さの点で問題ではない理由、および同時に両方の所有格にすることも機能する理由を確認することをお勧めします(おそらく、同じパターンで必須と非必須の所有格指定子を混在させると誤解が生じる可能性があります)。

また、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]スタックオーバーフローのタグが付けられていますが、おそらくそれ以上のものです。アサーション、ネストされた参照、所有格指定子などについて学ぶことには確かに価値がありますが、おそらくここでのより大きな教訓は、問題を解決しようとすることができる創造的なプロセス、決断、そしてあなたがさまざまな制約、実用的なソリューションを構築するためのさまざまなパーツからの体系的な構成など。


おまけ素材!PCRE再帰パターン!

PHPを起動したので、PCREは再帰的なパターンとサブルーチンをサポートしていると言う必要があります。したがって、次のパターンはpreg_matchideone.comで見られるように)機能します。

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

現在、Javaの正規表現は再帰的なパターンをサポートしていません。


さらにおまけ素材!マッチングa n b n c n !!

私たちが見てきたので、どのように一致させるために、n個の Bのnは非正規であるが、それでも文脈自由、私たちも一致させることができ、n個の B N C のnでも文脈自由ではありませんか、?

もちろん、答えはYESです。読者は自分でこれを解決することを試みることをお勧めしますが、解決策を以下に示します(ideone.comのJavaでの実装を使用)。

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


この長い回答には間違いや誤植があるのは間違いないので、フィードバックをコメントとして残して、自分で修正できるようにしてください。
polygenelubricants

よくやった。読み直すには少し時間がかかりますが、最後の行は基本的に読むことができません。それはそのような小さなフォントです。 - - - あ、ちょっと待って。それはfeature...?...それが良いアイデアかどうかわかりません。私は最後のシンボルが何であるかを知っていますが、それを読み取ることができません(コピーして貼り付けることを除いて)。
Peter Ajtai 2010

6
@ピーター:小さなテキストをハイライトし、コピーして他の何かに貼り付けます。意図的に読むのが難しくなっています。それはネタバレであり、ボーナスパズルの解決策です。
polygenelubricants

8
+1:素晴らしい説明、これらの「高度な記事」は素晴らしいアイデアです。
Callum Rogers

1
@LarsH PHP preg_match()PCREの例です。Java正規表現は、古いバージョンのPerl正規表現に基づいているようです。つまり、PHP正規表現はJavaのバージョンよりも強力です。2013-02-21現在、pcre.txtには、Perl 5.12にほぼ対応していると記載されています。Perlは現在5.16ですが、5.18は数か月先です。(実際のところ、正規表現にはあまり追加されていません)
ブラッドギルバート

20

再帰パターンをサポートするPCREについて言及されていないことを考えると、問題の言語を記述するPCREの最も単純で最も効率的な例を指摘したいと思います。

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

+1すごい、PCREが再帰パターンをサポートすることを知りませんでした(まだ学習中です!この情報に対応するために記事を改訂しました。a^n b^n c^nただし、再帰的なパターンが一致することはないと思います。
polygenelubricants

このオプションは単純ですが、投稿された回答ほど良くはありません-再帰は長い文字列でオーバーフローします。
Kobi

@Kobiこれは「良い」というあなたの定義に依存します。たとえば、再帰的なソリューションは、他のソリューションよりも1桁程度高速です(codepad.viper-7.com/CWgy7c)。そして、それは理解するのがはるかに簡単です。再帰的な解決策は、ほとんどの場合、文法を正規表現に直接変換したものです(実際には、文法化された形式で記述するだけで機能します)。
NikiC、2011

1
@polygeniclubricants、そのパターンを2つの再帰パターンと一致させることができます。1つはキャプチャせずにasとbs を消費する(そして、再帰で同じ量があることを確認する)後に、すべてのaを貪欲に消費するキャプチャ正規表現を続け、再帰を適用します。を消費して同じ数bのとがあることを確認するパターンc。正規表現は次のとおり/^(?=(a(?-1)?b)c)a+(b(?-1)?c)$/xです。クレジット:nikic.github.io/2012/06/15/…–
Josh Reback

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

再帰的なパターンでこれを行うことができますか?できない場合は、バランスグループが再帰的なパターンではできないことを実行できるという興味深いひねりです。(そしてはい、私は非常にサプリメントを高く評価しています)。
polygenelubricants

ちなみに、.NETソリューションを省略したのは、「a^n b^n。NET正規表現とどのように一致させることができるのか」という計画があるためです。将来の記事ですが、必要に応じて書いても大歓迎です。私はこれらの記事を自分のためだけにやっているのではありません。私は他の人にもサイトで良いコンテンツを提供するように勧めたいと思います。
polygenelubricants

再帰的なパターンでそれを行う方法を見つけたら、更新してください。フィボナッチシリーズを構成する長さの単語をキャプチャするためにバランスグループをいじってみましたが、うまく機能しませんでした。私が行ったのと同様に、見回すを使用することで可能になる場合があります。
Kobi

1
このパターンのPCREバージョンは、次の文字のチャンクが前のチャンクよりも長い場合に一致するため、少し欠陥があることを指摘しておきます。ここを参照してください:regex101.com/r/sdlRTm/1次のようにキャプチャグループの後(?!b)(?!c)、などを追加する必要があります:regex101.com/r/sdlRTm/2
jaytea
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.