Как это регулярное выражение Java обнаруживает палиндромы? - PullRequest
21 голосов
/ 08 сентября 2010

Это третья часть в серии статей об образовательных регулярных выражениях. Из этого следует Как это регулярное выражение находит треугольные числа? (где впервые введены вложенные ссылки) и Как сопоставить ^ n b ^ n с регулярным выражением Java? (где механизм «подсчета» прогнозируется более подробно). В этой части представлена ​​особая форма вложенного утверждения, которое в сочетании с вложенными ссылками позволяет регулярному выражению Java соответствовать тому, что большинство людей считает «невозможным»: palindromes !!

Язык палиндромов не является регулярным ; это на самом деле без контекста (для данного алфавита). Тем не менее, современная реализация регулярных выражений распознает не только обычные языки, а рекурсивные шаблоны Perl / PCRE и группы балансировки .NET могут легко распознавать палиндромы (см .: Вопросы, относящиеся * ).

Однако движок Java regex не поддерживает ни одну из этих «расширенных» функций. И все же «кто-то» (* wink *) сумел написать следующее регулярное выражение, которое, кажется, отлично справляется со своей задачей ( см. Также на ideone.com ) :

public class Palindrome {
    // asserts that the entirety of the string matches the given pattern
    static String assertEntirety(String pattern) {
        return "(?<=(?=^pattern$).*)".replace("pattern", pattern);
    }

    public static void main(String[] args) {
        final String PALINDROME =
            "(?x) | (?:(.) add)+ chk"
                .replace("add", assertEntirety(".*? (\\1 \\2?)"))
                .replace("chk", assertEntirety("\\2"));

        System.out.println(PALINDROME);
        // (?x) | (?:(.) (?<=(?=^.*? (\1 \2?)$).*))+ (?<=(?=^\2$).*)

        String[] tests = {
            "",     // true
            "x",    // true
            "xx",   // true
            "xy",   // false
            "xyx",  // true
            "xxx",  // true
            "xxyx", // false
            "racecar",                // true
            "step on no pets",        // true
            "aManaPlanaCanalPanaMa",  // true
            "this is impossible",     // FALSE!!!
        };
        for (String test : tests) {
            System.out.printf("[%s] %s%n", test, test.matches(PALINDROME));
        }
    }
}

Так что это похоже на работу, но как?

Ссылки


ОБЩИЙ СМЫСЛ ПРЕДУПРЕЖДЕНИЯ !!!

Это не лучший способ обнаружить палиндромы; это O(N^3) в лучшем случае. Выполнение этого обнаружения на языке программирования более общего назначения является одновременно более эффективным и более простым.

Вы бы не хотели использовать регулярное выражение для обнаружения палиндромов по тем же причинам, по которым вы не хотели бы использовать регулярное выражение для поиска простых чисел. Тем не менее, вы бы изучили , как нерекурсивное регулярное выражение небалансирующей группы может обнаруживать палиндромы по тем же причинам, что вы изучите , как можно использовать регулярное выражение для тестирования простоты: это весело , это сложно, это образование.

Похожие вопросы

1 Ответ

18 голосов
/ 08 сентября 2010

Общая картина

Сначала мы рассмотрим это регулярное выражение из общего алгоритма большой картинки, а затем более подробно рассмотрим конкретные детали реализации.Регулярное выражение - это почти прямой перевод следующего кода Java:

static boolean isPalindrome(String s) {
   if (s.isEmpty()) {
      return true;
   }
   String g2 = null;
   for (char ch : s.toCharArray()) {
      String g1 = String.valueOf(ch);
      // "add"
      if (g2 != null && s.endsWith(g1 + g2)) {
         g2 = g1 + g2;
      } else if (s.endsWith(g1)) {
         g2 = g1;
      } else {
         break;
      }
   }
   return s.equals(g2); // "chk"
}

Это, очевидно, не самый простой / эффективный код Java для проверки на палиндромы, но он работает, и, что самое интересное, он почти напрямую переводимрегулярное выражение с почти однозначным отображением.Вот снова регулярное выражение, воспроизведенное здесь для удобства, аннотированное, чтобы подчеркнуть поразительное сходство:

//  isEmpty  _for-loop_
//       ↓  /          \
    "(?x) | (?:(.) add)+ chk"
//             \_/  ↑
//             g1   loop body                   ___g2___
//                                             /        \
           .replace("add", assertEntirety(".*? (\\1 \\2?)"))
           .replace("chk", assertEntirety("\\2"));
                           // s.equals(g2)

Вложение : аннотированная и расширенная версия источникакод на ideone.com

(не стесняйтесь пока игнорировать детали assertEntirety: просто подумайте о нем как о механизме регулярных выражений черного ящика, который может сделать утверждение для всей строки независимо от того, где мы в данный момент находимся.)

Таким образом, основной алгоритм состоит в том, что мы пытаемся создать суффикс, подчиняющийся палиндромному ограничению, когда мы сканируем строку слева направо.Затем мы проверяем, можем ли мы построить полную строку таким способом.Если мы можем, то строка является палиндромом.Кроме того, в особом случае пустая строка является тривиальным палиндромом.

Как только алгоритм большой картинки понятен, мы можем проверить, как его реализует шаблон регулярного выражения.


Что свсе шаблоны регулярных выражений в Java String.replace?

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

Рассмотрим этот пример инициализации константы int (которая в конечном итоге содержит только число):

final int X = 604800;
final int Y = 60 * 60 * 24 * 7;
// now X == Y

Число, присвоенное X, является буквальным целым числом: мы можем ясно увидеть , что это за число.Это не относится к Y, который вместо этого использует выражение, и все же эта формула, кажется, передает идею о том, что представляет это число.Даже без правильного именования этих констант мы, тем не менее, понимаем, что Y, вероятно, представляет количество секунд в неделе, даже если мы не можем сразу узнать, что такое числовое значение.С другой стороны, с X мы точно знаем это число, но мы меньше понимаем, что оно представляет.

Использование строковых замен в фрагменте - аналогичная ситуация, но для строковых регулярных выражений,Вместо явного написания шаблона в виде одной буквенной строки, иногда систематическое и логическое вывод («формула») этого значения из более простых частей может быть гораздо более значимым.Это особенно верно для регулярных выражений, где часто важнее то, что мы понимаем, что делает шаблон, чем возможность видеть то, на что он похож как строковый литерал (который в любом случае не выглядит многообещающе, что со всеми дополнительными обратными слешами).

Для удобства здесь снова воспроизводится фрагмент фрагмента:

// the "formula"
     final String PALINDROME =
        "(?x) | (?:(.) add)+ chk"
           .replace("add", assertEntirety(".*? (\\1 \\2?)"))
           .replace("chk", assertEntirety("\\2"));

// the "value"
     System.out.println(PALINDROME);
     //                       ____add_____             chk_
     //               _______/            \____   _______/ \_____
     // (?x) | (?:(.) (?<=(?=^.*? (\1 \2?)$).*))+ (?<=(?=^\2$).*)
     //        |  \_/             \______/     |
     //        |   1                 2         |
     //        |_______________________________|

Вне всякого сомнения, «формула» в этом случае гораздо более читаема, чем конечная строка «значение»..

Конечно, есть гораздо более изощренные способы программно сгенерировать шаблон регулярных выражений, и, безусловно, можно писать таким образом, что вместо того, чтобы подчеркивать его значение, можно запутывать, но разумное использование даже простых замен строк может все же сделатьчудо (как показано в этом примере).

Урок : рассмотрим программную генерацию шаблонов регулярных выражений.


Как работает add работа?

Конструкция (?:(.) add)+, где add - это утверждение, которое выполняет своего рода "подсчет", имеетуже подробно обсуждался в предыдущих двух частях.Однако стоит отметить две особенности:

  • (.) захватывает группу 1, что позволяет использовать обратную ссылку позже
  • Утверждение assertEntirety вместо того, чтобы просто смотреть в будущее от нашего текущегопозиция
    • Мы обсудим это более подробно позже;просто представьте, что это способ утверждать всю строку

Шаблон, примененный к assertEntirety в add, выглядит следующим образом:

# prefix   _suffix_
#    ↓    /        \
    .*?   ( \1 \2? )
#         \________/   i.e. a reluctant "whatever" prefix (as short as possible)
#          group 2          followed by a suffix captured into group 2

Обратите внимание, что группа 2 ссылается на себя с необязательным спецификатором, методика, уже обсуждаемая во второй части серии.Само собой разумеется, что группа 2 является нашим «счетчиком» в этом шаблоне: это суффикс, который мы будем пытаться увеличивать влево на каждой итерации «цикла».Поскольку мы выполняем итерацию для каждого (.) слева направо, мы пытаемся добавить этот же символ (используя обратную ссылку к \1) к нашему суффиксу.

Еще раз напомним перевод Java-кода вышеупомянутого шаблона, воспроизведенного здесьдля удобства:

if (g2 != null && s.endsWith(g1 + g2)) {   // \2? is greedy, we try this first
   g2 = g1 + g2;
} else if (s.endsWith(g1)) {    // since \2? is optional, we may also try this
   g2 = g1;
} else {        // if there's no matching suffix, we "break" out of the "loop"
   break;
}

Тот факт, что \2? является необязательным, означает несколько вещей:

  • Это обеспечивает «базовый случай» для самоссылки (основная причинамы делаем это!)
  • Поскольку \2? является частью шаблона суффикса (и, следовательно, появляется позже в общем шаблоне), префиксная часть должна быть неохотной, следовательно, .*? вместо .*.Это позволяет \2? проявлять свою жадность.
  • «Счетчик» также может «сбрасываться» и давать «неправильный» результат
    • Во второй части мы показали, как может произойти возврат ?в том же виде проблемного сброса
      • Мы решили проблему с помощью притяжательного квантификатора ?+, но здесь это неприменимо

Третий пункт более подробно рассматривается в следующем разделе.

Урок : Тщательно проанализируйте взаимодействия между жадными / неохотными повторениями в частях шаблона.

Смежные вопросы


Зачем нам нужна chk фаза?

Как упоминалось в предыдущем разделе, необязательный и возвращаемый \2? означает, что наш суффикс может уменьшаться при некоторомобстоятельства.Мы рассмотрим такой сценарий шаг за шагом для этого ввода:

 x y x y z y x
↑
# Initial state, \2 is "uninitialized"
             _
(x)y x y z y x
  ↑
  # \1 captured x, \2 couldn't match \1\2 (since \2 is "uninitialized")
  #                but it could match \1 so it captured x
           ___
 x(y)x y z y x
    ↑
    # \1 captured y, \2 matched \1\2 and grew to capture yx
             _
 x y(x)y z y x
      ↑
      # \1 captured x, \2 couldn't match \1\2,
      #                but it could match \1 so it shrunk to capture x (!!!)
           ___
 x y x(y)z y x
        ↑
        # \1 captured y, \2 matched \1\2 and grew to capture yx
         _____
 x y x y(z)y x
          ↑
          # \1 captured z, \2 matched \1\2 and grew to capture zyx
       _______
 x y x y z(y)x
            ↑
            # \1 captured y, \2 matched \1\2 and grew to capture yzyx
     _________
 x y x y z y(x)
              ↑
              # \1 captured x, \2 matched \1\2 and grew to capture xyzyx

Мы можем изменить наш шаблон (и соответствующий код Java), чтобы пропустить фазу chk, и увидим, что это действительно то, что происходит:

    // modified pattern without a chk phase; yields false positives!
    final String PALINDROME_BROKEN =
        "(?x) | (?:(.) add)+"
            .replace("add", assertEntirety(".*? (\\1 \\2?)"));

    String s = "xyxyzyx"; // NOT a palindrome!!!

    Matcher m = Pattern.compile(PALINDROME_BROKEN).matcher(s);
    if (m.matches()) {
        System.out.println(m.group(2)); // prints "xyzyx"
    }

Как объяснено, "xyxyzyx", который является NOT палиндромом, ложно сообщается как единое целое, потому что мы не проверяли, стал ли растущий суффикс в конечном итоге полной строкой(чего он явно не сделал в этом случае).Поэтому фаза chk (которая является assertEntirety шаблона \2) является абсолютной необходимостью в нашей установке.Мы должны подтвердить, что нам действительно удалось нарастить суффикс.Если это так, то у нас есть палиндром.

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


Основной курс: assertEntirety

Хотя мы можем написать шаблон регулярных выражений Java для обнаружения палиндромов, все, кроме assertEntirety, уже было рассмотрено в предыдущих частяхсериал.Единственная новая вещь - это таинственный черный ящик, этот мощный механизм, который волшебным образом позволил нам сделать то, что иначе «невозможно».

Конструкция assertEntirety основана на следующем мета-шаблоне вложенных обходных путей:

(?<=(?=^pattern$).*)

" Я могу видеть место где-то позади меня, где я могу смотреть вперед и видеть ^pattern$"

Название «lookaround» подразумевает относительность к нашему текущему положению: мы смотрим вокруг нас, возможно, впереди или сзади, с того места, где мы стоим.Таким образом, вложив взгляд в взгляд назад, мы можем метафорически «взлететь в небо» и посмотреть на всю картину.

Абстрагирование этого мета-паттерна в assertEntirety немного похоже на запись макросов подстановки предварительной обработки.Наличие вложенных поисков везде, вероятно, ухудшает читабельность и удобство сопровождения, поэтому мы инкапсулируем его в assertEntirety, что не только скрывает сложность его внутренней работы, но и дополнительно подчеркивает его семантику, давая ему подходящее имя.

Урок : Рассмотрите возможность абстрагирования мета-паттернов, чтобы скрыть сложность и передать семантику.


Приложение: Об взгляде бесконечной длины в Java

Observantчитатели заметят, что assertEntirety содержит .* в виде сзади, что делает его теоретическую максимальную длину бесконечной.Нет, Java официально не поддерживает просмотр в бесконечную длину.Да, как это было адекватно продемонстрировано, все равно работает.Официально это классифицируется как «ошибка»;но «кто-то» (* wink *) также может считать это «скрытой функцией».

Вполне возможно, что эта «ошибка» будет «исправлена» в будущем.Удаление этой скрытой функции нарушит это конкретное решение проблемы палиндрома Java regex.

Смежные вопросы

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...