Como podemos combinar um^n b^n com Java regex?

esta é a segunda parte de uma série de artigos da regex educacional. Mostra como os cabeçalhos e as referências aninhadas podem ser usados para corresponder ao languge não regular a N b N. As referências aninhadas são introduzidas pela primeira vez em: Como É Que este vértice encontra números triangulares?

uma das línguas Não-arquetípicas regulares é:

L = { anbn: n > 0 }

Esta é a linguagem de todas as cadeias de caracteres não-vazias que consistem em alguns números de a 's seguidos por um número igual de b' s. exemplos de cadeias de caracteres nesta língua são:ab, aabb, aaabbb.

esta linguagem pode ser mostrada como não regular pelo lema de bombeamento . É de fato uma linguagem arquetípica livre de contexto, que pode ser gerada pela gramática livre de contexto. S → aSb | ab.

No entanto, as actuais implementações regex reconhecer mais do que apenas línguas normais. Isto é, eles não são "regulares" pela definição formal da teoria da linguagem. O PCRE e o Perl suportam a definição de grupos de equilíbrio recursivo, e o.NET suporta a definição de grupos de equilíbrio. Ainda menos" fancy " recursos, por exemplo, backreference matching, significa que regex não é regular.

Mas quão poderosa é Esta característica "básica"? Podemos reconhecer L com a regex Java, por exemplo? Podemos talvez combinar lookarounds e referências aninhadas e ter um padrão que funciona com = = referências = = String.matches para corresponder textos como ab, aabb, aaabbb, etc?

referências

questões ligadas

Author: Community, 2010-09-05

3 answers

A resposta é, escusado será dizer, sim! você pode certamente escrever um padrão regex Java para corresponder a n B n. Ele usa um lookahead positivo para asserção, e uma referência aninhada para "contagem".

Em vez de dar imediatamente o padrão, esta resposta guiará os leitores através do processo de derivá-lo. Várias dicas são dadas como a solução é construída lentamente. Neste aspecto, espero que esta resposta conter muito mais do que apenas outro padrão regular puro. Esperemos que os leitores também aprendam a" pensar em regex", e como colocar várias construções harmoniosamente juntos, para que eles possam derivar mais padrões por conta própria no futuro.

A linguagem usada para desenvolver a solução será PHP para a sua concisão. O teste final uma vez que o padrão está finalizado será feito em Java.


Passo 1: procurar asserção Vamos começar com um problema mais simples: quero igualar a+ no início de uma string, mas só se for seguida imediatamente por b+. Podemos usar ^ a âncora a nossa combinação, e como só queremos igualar o a+ sem o b+, podemos usar a asserção(?=…). Aqui está o nosso padrão com um simples arnês de teste.
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 saída é (conforme visto em ideone.com):

aaa 0
aaab 1 aaa
aaaxb 0
xaaab 0
b 0
abbb 1 a
Isto é exactamente o resultado que queremos, só se for igual ao que queremos. início do texto, e só se for imediatamente seguido por b+.

lição: você pode usar padrões em lookarounds para fazer afirmações.


Passo 2: capturar num lookahead (E F R e E - S P A C i n g mode)

Agora vamos dizer que mesmo que não queiramos que o b+ faça parte do jogo, nós queremoscapturar de qualquer maneira no grupo 1. Além disso, como prevemos ter um padrão mais complicado, vamos usar x modificador para espaço livre para que possamos tornar a nossa regex mais legível.

Com base no nosso excerto anterior do PHP, temos agora o seguinte padrão:

$r2 = '/ ^ a+ (?= (b+) ) /x';
#             │   └──┘ │
#             │     1  │
#             └────────┘
#              lookahead

testAll($r2, $tests);

A saída é agora (como visto em ideone.com):

aaa 0
aaab 1 aaa|b
aaaxb 0
xaaab 0
b 0
abbb 1 a|bbb

Note que por exemplo aaa|b é o resultado de join-ing o que cada grupo capturou com '|'. Neste caso, o grupo 0 (ou seja, o que corresponde ao padrão) capturou aaa, e o grupo 1 capturou b.

Lição: Você pode capturar dentro de um lookaround. Você pode usar o espaço livre para melhorar a legibilidade.


Passo 3: Refactorar o lookahead no "loop"

Antes de introduzirmos o nosso mecanismo de contagem, temos de fazer uma modificação ao nosso padrão. Atualmente, o lookahead está fora da repetição "loop". Tudo bem até agora, porque só queríamos afirmar que há um b+ a seguir o nosso a+, mas o que nós realmente quer fazer, eventualmente, é afirmar que para cada a que correspondemos dentro do "loop", há um correspondente b para ir com ele. Não vamos preocupar-nos com o mecanismo de contagem por agora e fazer o Refactor do seguinte modo:
  • primeiro refactor a+ a (?: a )+ (note que (?:…) é um grupo que não captura)
  • depois mova a cabeça de leitura para dentro deste grupo de não-captura
      Note que agora temos de "saltar" antes de podermos "see" the b+, so modify the pattern accordingly
Então agora temos o seguinte:
$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
#          │     │      └──┘ │ │
#          │     │        1  │ │
#          │     └───────────┘ │
#          │       lookahead   │
#          └───────────────────┘
#           non-capturing group

A saída é a mesma que antes (vista em ideone.com então, não há mudança nesse aspecto. O importante é que agora estamos fazendo a afirmação em todas as iterações do "loop". Com o nosso padrão atual, isso não é necessário, mas em seguida vamos fazer o grupo 1 "contar" para nós usando auto-referência.

lição: você pode capturar dentro de um grupo de não-captura. Os olhares podem ser repetidos.


Passo 4: Este é o passo em que começamos a contar

Eis o que vamos fazer: reescrevemos o grupo 1 para que:
  • no final da primeira iteração do +, quando o primeiro a é combinado, ele deve capturar b
  • no final da segunda iteração, quando outro a é combinado, deve capturar bb
  • No final da terceira iteração, deve capturar bbb
  • ...
  • no final daN -é a iteração, o grupo 1 deve capturar B n
  • se não há o suficiente b para capturar no grupo 1 então a afirmação simplesmente falha
Então, o grupo 1, que é agora (b+), terá de ser reescrito para algo como (\1 b). Ou seja, tentamos "adicionar" a b a que grupo 1 capturado na iteração anterior.

Há um pequeno problema aqui em que este padrão está faltando o "caso base", ou seja, o caso onde ele pode corresponder sem a auto-referência. Um caso de base é necessário porque o grupo 1 começa "não-iniciado"; ele ainda não capturou nada (nem mesmo uma cadeia vazia), então uma tentativa de auto-referência sempre falhará.

Há muitas maneiras de contornar isto, mas por agora vamos apenas fazer a correspondência de auto-referência {[[216]}opcional , ou seja, \1?. Isto pode ou não funcionar perfeitamente, mas vamos ver o que isso faz, e se houver algum problema, então atravessaremos essa ponte quando chegarmos a ela. Além disso, vamos adicionar mais alguns casos de teste.

$tests = array(
  'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb'
);

$r4 = '/ ^ (?: a (?= a* (\1? b) ) )+ /x';
#          │     │      └─────┘ | │
#          │     │         1    | │
#          │     └──────────────┘ │
#          │         lookahead    │
#          └──────────────────────┘
#             non-capturing group

A saída é agora (como visto em 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....
A-ha! Parece que estamos muito perto da solução agora! Conseguimos que o grupo 1 "contasse" usando auto-referência! Mas espera... algo está errado com o segundo e o último teste caixas!! Não há suficientes, e de alguma forma contou mal! Vamos examinar porque isto aconteceu no próximo passo.

lição: uma maneira de" inicializar " um grupo de auto-referenciamento é fazer a correspondência de auto-referência opcional.


Passo 4½: compreender o que correu mal

O problema é que como fizemos a correspondência de auto-referência opcional, o "contador" pode "reiniciar" de volta a 0 quando não há suficientes b ' S. examine o que acontece em cada iteração do nosso padrão com aaaaabbb como entrada.

 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
A-ha! Na nossa 4ª iteração, ainda podemos igualar \1, mas não conseguimos igualar \1b! Uma vez que permitimos que a correspondência de auto-referência seja opcional com \1?, as backtracks do motor e tomou a opção "no thanks", que então nos permite igualar e capturar apenas b! Note, no entanto,que, exceto na primeira iteração, você pode sempre igualar apenas a auto-referência \1. Presente é óbvio, é claro, uma vez que é o que acabamos de capturar em nossa iteração anterior, e em nossa configuração podemos sempre igualá-lo novamente (por exemplo, se capturamos bbb da última vez, estamos garantidos que ainda haverá bbb, mas pode ou não haver bbbb desta vez).

lição: Cuidado com o retrocesso. O motor regex fará tanto retrocesso quanto você permitir até que o padrão indicado corresponda. Isto pode ter impacto no desempenho (isto é, [245]}catastrófico backtracking ) e / ou correcção.


Passo 5: auto-possessão para o resgate!

A" correcção " deve agora ser óbvia: combinar repetição opcional com possessivo quantificador. Isto é, em vez de simplesmente ?, use ?+ em vez disso (lembre-se que uma repetição que é quantificada como possessiva não recua, mesmo que tal "cooperação" possa resultar em uma correspondência do padrão geral).

Em termos muito informais, ?+, ? e Diz:

?+

  • (opcional) " não tem de estar lá,"
      (Possessivo) " mas se ele está lá, você deve pegá-lo e não deixá-lo ir!"

?

  • (opcional) " não tem de estar lá,"
      Mas se for você pode tomá-lo por agora,
      • "mas você pode ser pedido para deixá-lo ir mais tarde!"

??

  • (opcional) " não tem de estar lá,"
      "E mesmo que seja, ainda não tens de aceitar,"
      • (retroceder) "mas você pode ser convidado a tomá-lo mais tarde!"

Em nossa configuração, \1 não vai estar lá a primeira vez, mas vai sempre estar lá a qualquer momento depois disso, e nós sempre quero então bate. Assim, \1?+ realizaria exatamente o que queremos.

$r5 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ /x';
#          │     │      └──────┘ │ │
#          │     │          1    │ │
#          │     └───────────────┘ │
#          │         lookahead     │
#          └───────────────────────┘
#             non-capturing group

Agora a saída é (como visto em 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!!!
Voilà!!! Problema resolvido!!! Estamos agora a contar correctamente, exactamente como queremos!

lição: Aprenda a diferença entre a repetição gananciosa, relutante e possessiva. Opcional-possessivo pode ser uma combinação poderosa.


Passo 6: retoques finais E depois? temos agora um padrão que corresponde a a repetidamente, e para cada a que foi igualado, há um b correspondente capturado no grupo 1. O + termina quando não há mais a, ou se a afirmação falhou porque não há um b correspondente para um a.

Para terminar o trabalho, precisamos simplesmente de adicionar ao nosso padrão \1 $. Esta é agora uma referência traseira ao que o grupo 1 correspondeu, seguido pelo fim da âncora da linha. A âncora garante que ali não há nenhum extra b na corda; em outras palavras, que de fato temos a n B n.

Aqui está o padrão finalizado, com casos de teste adicionais, incluindo um com 10 mil caracteres de comprimento.
$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
Encontra 4 correspondências.: ab, aabb, aaabbb, e o 5000 b5000. É preciso apenas 0, 06 s para correr sobre ideone.com.

Passo 7: O teste Java

Então o padrão funciona em PHP, mas o objetivo final é escrever um padrão que funciona em 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);
}

O padrão funciona como esperado ( visto em ideone.com).


E agora chegamos à conclusão...

É preciso dizer que o a* no lookahead, e na verdade o "laço principal +", ambos permitem retroceder. Os leitores são encorajados a confirmar por que isso não é um problema em termos de correção, e por que, ao mesmo tempo, tornar ambos possessivos também funcionaria (embora a mistura de quantificadores possessivos obrigatórios e não obrigatórios no mesmo padrão possa levar a equívocos).

Também deve ser dito que, embora seja legal que haja um padrão regex que irá corresponder a n b n, esta nem sempre é a melhor solução na prática. Uma solução muito melhor é simplesmente combinar ^(a+)(b+)$, e então comparar o comprimento das cadeias capturadas pelos grupos 1 e 2 na linguagem de programação de hospedagem.

Em PHP, pode ser parecido com isto ( visto em ideone.com):

function is_anbn($s) {
   return (preg_match('/^(a+)(b+)$/', $s, $groups)) &&
      (strlen($groups[1]) == strlen($groups[2]));
}

O objectivo deste artigo é não convencer os leitores de que a regex pode fazer quase tudo; claramente não pode, e mesmo para as coisas que pode fazer, pelo menos uma delegação parcial na língua de acolhimento deve ser considerada se conduzir a uma solução mais simples.

Como mencionado no topo, embora este artigo seja necessariamente marcado [regex] para stackoverflow, é talvez sobre mais de que. Embora certamente haja valor em aprender sobre asserções, referências aninhadas, quantificador possessivo, etc, talvez a lição maior aqui é o processo criativo pelo qual se pode tentar resolver problemas, a determinação e o trabalho duro que muitas vezes requer quando você está sujeito a várias restrições, a composição sistemática de várias partes para construir uma solução de trabalho, etc.


Material de bónus! Padrão recursivo PCRE!

Uma vez que mencionámos o PHP, precisa ser dito que o PCRE suporta padrão recursivo e sub-rotinas. Assim, o seguinte padrão funciona para preg_match (como visto em ideone.com):
$rRecursive = '/ ^ (a (?1)? b) $ /x';

Actualmente a expressão regular do Java não suporta o padrão recursivo.


Ainda mais material de bónus! Correspondência a n B n C n !! Então nós vimos como igualar an B n o que não é regular, mas ainda livre de contexto, mas também podemos combinar anBnC n, que nem sequer é livre de contexto? A resposta é, claro, sim!Os leitores são encorajados a tentar resolver isto por conta própria, mas a solução é fornecida abaixo (com a implementação em Java on ideone.com).

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

 126
Author: polygenelubricants, 2013-03-30 15:00:11

Dado que não foi feita qualquer menção ao PCRE que suporta padrões recursivos, gostaria apenas de apontar o exemplo mais simples e eficiente do PCRE que descreve a linguagem em questão:

/^(a(?1)?b)$/
 20
Author: jaytea, 2011-09-25 10:37:04

Conforme mencionado na pergunta - com .LÍQUIDO de balanceamento de grupo, os padrões do tipo umnbncndn...zn pode ser combinado facilmente como

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

Por exemplo: http://www.ideone.com/usuOE


Editar:

Existe também um padrão PCRE para a linguagem generalizada com padrão recursivo, mas um lookahead é necessário. Não creio que isto seja uma tradução directa do acima.

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

Por exemplo: http://www.ideone.com/9gUwF

 10
Author: kennytm, 2010-09-06 20:01:05