Як ми можемо зіставити ^ nb ^ n з виразкою Java?


99

Це друга частина серії навчальних регекс-статей. Він показує, як можна використовувати вказівники пошуку та вкладених посилань для узгодження нерегулярного мови a n b n . Вкладені посилання вперше вводяться в: Як цей регулярний вираз знаходить трикутні числа?

Однією з архетипних нерегулярних мов є:

L = { an bn: n > 0 }

Це мова всіх непустих рядків, що складаються з деякої кількості as, з якою дорівнює однакова кількість bs. Приклади рядків в цій мові є ab, aabb, aaabbb.

Ця мова може бути нерегулярною накачаною лемою . Насправді це архетипна без контекстна мова , яка може бути породжена без контекстною граматикою S → aSb | ab .

Тим не менш, сучасні реаггекс-реалізації явно розпізнають не просто звичайні мови. Тобто, вони не є "регулярними" за визначенням формальної теорії мови. PCRE і Perl підтримують рекурсивний регулярний вираз, а .NET підтримує визначення балансуючих груп. Ще менше "фантазійних" функцій, наприклад, відповідність зворотної референції, означає, що регулярний вираз не є регулярним.

Але наскільки настільки потужні ці "основні" риси? Чи можемо ми розпізнати L, наприклад, Java regex? Можемо чи ми , можливо , об'єднати lookarounds і вкладені посилання і мати малюнок , який працює з , наприклад , String.matchesщоб відповідати рядки , як ab, aabb, aaabbbі т.д.?

Список літератури

Пов'язані питання


4
Цю серію розпочали з дозволу деяких у спільноті ( meta.stackexchange.com/questions/62695/… ). Якщо прийом хороший, я планую продовжувати висвітлювати інші більш вдосконалені, а також більш основні риси регексу.
полігенмастильні матеріали


Нічого собі, я ніколи не знав, що регулярні вирази Java не обмежуватимуться регулярними виразами. Я думаю, це пояснює, чому я завжди думав, що вони не будуть повністю реалізовані. Я маю на увазі те, що в Java Regexs немає вбудованих доповнень, різниць або операторів продуктів, але це має сенс, оскільки вони не обмежені звичайними мовами.
Лан

Це запитання було додано до поширених запитань щодо регулярного вираження стека в розділі "Розширений Regex-Fu".
aliteralmind

Відповіді:


139

Відповідь, зайве говорити, ТАК! Ви можете безумовно , написати шаблон регулярного виразу Java відповідно з п б п . Він використовує позитивну підказку для твердження та одну вкладену посилання для "підрахунку".

Замість того, щоб одразу ж видавати шаблон, ця відповідь допоможе читачам провести процес його виведення. У міру того, як рішення будується повільно, даються різні підказки. У цьому аспекті, сподіваємось, ця відповідь буде містити набагато більше, ніж просто інший акуратний виразний шаблон. Сподіваємось, читачі також дізнаються, як «думати в регексе» та як гармонійно поєднувати різні конструкції, щоб вони могли отримати більше шаблонів самостійно в майбутньому.

Мова, що використовується для розробки рішення, буде PHP для його стисності. Заключний тест, коли шаблон буде доопрацьований, буде зроблений на Java.


Крок 1: Ознайомтесь із твердженням

Почнемо з більш простої проблеми: ми хочемо відповідати a+на початку рядка, але тільки в тому випадку, якщо за ним негайно слід b+. Ми можемо використовувати ^для прив’язки нашого матчу, і оскільки ми хочемо відповідати лише a+без b+, ми можемо використовувати твердження lookahead(?=…) .

Ось наша модель з простим тестовим джгутом:

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|bЄ результатом join-ing того, з чим потрапила кожна група '|'. У цьому випадку група 0 (тобто, з якою схемою відповідає) охоплювала aaa, а група 1 - захоплена b.

Урок : Ви можете зафіксувати всередині кругообігу. Ви можете використовувати вільний пробіл для підвищення читабельності.


Крок 3: Повторне відновлення пошуку в "цикл"

Перш ніж ми зможемо запровадити механізм підрахунку, нам потрібно зробити одну модифікацію нашої моделі. В даний час lookahead знаходиться поза +"петлі" повторення. Це добре зараз, тому що ми просто хотіли стверджувати, що є 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, то твердження просто провалюється

Тож групу 1, яка зараз є (b+), доведеться переписати на щось подібне (\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

А-ха! На нашому четвертому ітерації ми все ще могли відповідати \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повторюється повторно, і для кожного aзіставленого є відповідний bзахоплений у групі 1. +Закінчується, коли їх більше немає a, або якщо твердження не вдалося, тому що немає відповідного bдля ан a.

Щоб закінчити роботу, нам просто потрібно додати наш шаблон \1 $. Тепер це зворотне посилання на відповідність групі 1, а потім кінець рядка якоря. Якір гарантує, що bв рядку немає зайвих ; іншими словами, що насправді ми маємо в п б п .

Ось доопрацьований візерунок з додатковими тестовими кейсами, включаючи один, який має довжину 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 матчі: ab, aabb, aaabbb, і в 5000 б 5000 . На ideone.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+)(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 не підтримує рекурсивну схему.


Ще більше бонусного матеріалу! Узгодження a n b n c n !!

Отже , ми бачили , як відповідно з п б п , яка не є регулярним, але по- , як і раніше контекстно-вільної, але ми можемо також відповідати з п б п З п , яка не є навіть контекстно-вільною?

Відповідь, звичайно, ТАК! Читачам рекомендується намагатися вирішити це самостійно, але рішення пропонується нижче (з реалізацією в Java на ideone.com ).

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


Без сумніву, у цій довгій відповіді можуть бути помилки / помилки, тому, будь ласка, залиште відгуки як коментарі, щоб я міг їх переглянути самостійно.
полігенмастильні матеріали

Чудова робота. Щоб прочитати, мені знадобиться деякий час, але останній рядок в основному неможливо прочитати; це такий маленький шрифт. ------ Чекай. Це feature? .... Не впевнений, чи це гарна ідея. Я знаю, що є останнім символом, але його неможливо прочитати (окрім його копіювання).
Пітер Айтай

6
@Peter: виділіть невеликий текст і скопіюйте та вставте щось інше. Спеціально читати важко: це спойлер, рішення бонусної головоломки.
полігенмастильні матеріали

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.
полігенмастильні матеріали

Слід зазначити, що цей варіант простіший, але не такий хороший, як розміщена відповідь - рекурсія переповнюється на довгі рядки.
Кобі

@Kobi Це залежить від вашого визначення поняття "добре". Наприклад, рекурсивне рішення приблизно на порядок швидше, ніж інше ( codepad.viper-7.com/CWgy7c ). І це зрозуміти набагато простіше. Рекурсивне рішення - це майже пряма трансформація граматики в регулярний вираз (насправді ви могли просто написати це в граматизованому вигляді, це спрацювало б).
NikiC

1
@polygeniclubricants, ви можете зіставити цю схему з двома рекурсивними візерунками, однією, яка споживає as і bs без фіксації (і підтверджує, що є однакова кількість w / рекурсії), а потім слідує захоплюючий регулярний вираз, який жадібно споживає всі, а потім застосовує рекурсивний шаблон для споживання та перевірки наявності однакової кількості bs та cs. Регулярний вираз: /^(?=(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

ви можете це зробити з рекурсивним малюнком? Тому що якщо ви не можете, це цікавий поворот того, що балансуюча група може робити те, що рекурсивна схема не може. (І так, я дуже ціную доповнення).
полігенмастильні матеріали

До речі, причиною, через яку я пропустив рішення .NET, було те, що я маю плани на тему "Як ми можемо співставитись a^n b^nз .NET regex?" стаття в майбутньому, але ви більше ніж ласкаво просимо написати її, якщо хочете. Я не роблю цих статей лише для себе; Я хочу заохочувати інших робити це, а також мати гарний вміст на сайті.
полігенмастильні матеріали

Будь ласка, оновіть, якщо ви знайдете спосіб зробити це з рекурсивними візерунками. Я розігрувався з балансуючими групами, щоб зафіксувати слова, довжини яких складають ряд Фібоначчі, і я не міг змусити його працювати. Це може бути можливим за допомогою огляду навколо, подібного до того, що я робив.
Кобі

1
Я просто хотів би зазначити, що версія PCRE цього шаблону трохи хибна, оскільки вона відповідає, якщо наступний фрагмент символів довший за попередній. Дивіться тут: regex101.com/r/sdlRTm/1 Вам потрібно додати (?!b), (?!c)тощо, після таких груп захоплення: regex101.com/r/sdlRTm/2
jaytea
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.