Codingbat wordEnds с использованием regex

Я пытаюсь решить wordEnds from codingbat.com с помощью regex.

Учитывая строку и непустую строку слова, верните строку, состоящую из каждого char, непосредственно перед и сразу после каждого появления слова в строке. Игнорировать случаи, когда char нет или после слова, а char может быть дважды включен, если он находится между двумя словами.

wordEnds("abcXY123XYijk", "XY") → "c13i"
wordEnds("XY123XY", "XY") → "13"
wordEnds("XY1XY", "XY") → "11"
wordEnds("XYXY", "XY") → "XY"

Это самое простое, поскольку я могу сделать это с моим текущим знанием регулярного выражения:

public String wordEnds(String str, String word) {
  return str.replaceAll(
     ".*?(?=word)(?<=(.|^))word(?=(.|$))|.+"
       .replace("word", java.util.regex.Pattern.quote(word)),
     "$1$2"
  );
}

replace используется для размещения в фактической строке word в шаблоне для удобочитаемости. Pattern.quote не обязательно передавать свои тесты, но я думаю, что это необходимо для правильного решения на основе регулярных выражений.

Регулярное выражение имеет две основные части:

  • Если после сопоставления как можно большего количества символов ".*?", word все еще можно найти "(?=word)", затем найдите, чтобы захватить любой символ, предшествующий ему "(?<=(.|^))", совпадение "word", и посмотрите, чтобы захватить любой символ после него "(?=(.|$))".
    • Исходный тест "if" гарантирует, что атомный lookbehind захватывает, только если есть word
    • Использование lookahead для захвата следующего символа не потребляет его, поэтому его можно использовать как часть дальнейшего сопоставления
  • В противном случае сопоставьте то, что осталось "|.+"
    • Группы 1 и 2 будут записывать пустые строки

Я думаю, что это работает во всех случаях, но это, очевидно, довольно сложно. Мне просто интересно, могут ли другие предложить более простое регулярное выражение для этого.

Примечание. Я не ищу решение с помощью indexOf и цикла. Я хочу решение replaceAll на основе регулярного выражения. Я также нуждаюсь в рабочем регулярном выражении, которое передает все тесты кодирования.


Мне удалось уменьшить появление word внутри шаблона только до одного.

".+?(?<=(^|.)word)(?=(.?))|.+"

Я все еще смотрю, можно ли это упростить, но у меня также есть другой вопрос:

  • С помощью этого последнего шаблона я упростил .|$ только .?, но если я попытался упростить ^|. до .?, это не сработает. Почему это?

Ответ 1

На основе вашего решения мне удалось немного упростить код:

public String wordEnds(String str, String word) {
  return str.replaceAll(".*?(?="+word+")(?<=(.|^))"+word+"(?=(.|$))|.+","$1$2");
}

Другой способ записи:

public String wordEnds(String str, String word) {
  return str.replaceAll(
     String.format(".*?(?="+word+")(?<=(.|^))"+word+"(?=(.|$))|.+",word),
     "$1$2");
}

Ответ 2

С помощью этого последнего шаблона я упростил .|$ только .?, но если я попытался упростить ^|. до .?, это не сработает. Почему это?

В реализации Oracle поведение look-behind выглядит следующим образом:

  • "Изучая" регулярное выражение (с методом study() в каждом node), он знает максимальную длину и минимальную длину шаблона в группе look-behind. (Метод study() - это то, что допускает очевидную длину внешнего вида)
  • Он проверяет внешний вид , начиная совпадение в каждой позиции от индекса (current - min_length) до позиции (current - max_length) и выходит раньше, если условие выполнено.

Эффективно, он попытается сначала проверить внешний вид в кратчайшей строке.

Реализация умножает сложность соответствия на коэффициент O (k).

Это объясняет, почему смена ^|. на .? не работает: из-за стартовой позиции она эффективно проверяет word до .word. Здесь нет квантификатора, так как упорядочение задается диапазоном соответствия.

Вы можете проверить код метода match в Pattern.Behind и Pattern.NotBehind внутренних классах, чтобы проверить, что я сказал выше.


В отличие от .NET, look-behind, скорее всего, реализуется функцией обратного сопоставления, а это означает, что никакой сложности не возникает при сопоставлении сложности.

Мое подозрение связано с тем, что группа захвата в (?<=(a+))b соответствует всем a в aaaaaaaaaaaaaab. Показано, что квантификатор имеет свободное владение в группе поиска.

Я тестировал, что ^|. можно упростить до .? в .NET, и регулярное выражение работает правильно.

Ответ 3

Я работаю в .NET regex, но мне удалось изменить ваш шаблон на:

.+?(?<=(\w?)word)(?=(\w?))|.+

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

Он может ответить, почему вам не нужно указывать якоря ^ и $, для чего именно $ - это \r или \n или другое? (У .NET есть проблемы с $, и, возможно, вы не совсем захватываете Null из $, но нулевое значение \r или \n, которое позволило вам перейти на .? для $)