Как это регулярное выражение находит треугольные числа?

Часть серии учебных статей регулярных выражений, это нежное введение в концепцию вложенных ссылок.

Первые несколько треугольных номеров:

 1 = 1
 3 = 1 + 2
 6 = 1 + 2 + 3
10 = 1 + 2 + 3 + 4
15 = 1 + 2 + 3 + 4 + 5

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

  • Учитывая n, сначала создаем строку длины n, заполненную тем же символом
  • Затем мы сопоставляем эту строку с шаблоном ^(\1.|^.)+$
    • n является треугольным, если и только если этот шаблон соответствует строке

Вот некоторые фрагменты, показывающие, что это работает на нескольких языках:

PHP (на ideone.com)

$r = '/^(\1.|^.)+$/';

foreach (range(0,50) as $n) {
  if (preg_match($r, str_repeat('o', $n))) {
     print("$n ");
  }
}

Java (на ideone.com)

for (int n = 0; n <= 50; n++) {
    String s = new String(new char[n]);
    if (s.matches("(\\1.|^.)+")) {
        System.out.print(n + " ");
    }
}

С# (на ideone.com)

Regex r = new Regex(@"^(\1.|^.)+$");

for (int n = 0; n <= 50; n++) {
    if (r.IsMatch("".PadLeft(n))) {
       Console.Write("{0} ", n);
    }
}

Итак, это регулярное выражение, похоже, работает, но может ли кто-нибудь объяснить, как?

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

Ответ 1

Объяснение

Здесь схематическое разбиение рисунка:

from beginning…
|         …to end
|         |
^(\1.|^.)+$
 \______/|___match
  group 1    one-or-more times

(…) brackets определить группу захвата 1, а эта группа соответствует несколько раз с помощью +. Этот подшаблон привязан с помощью ^ и $, чтобы увидеть, может ли он соответствовать всей строке.

Группа 1 пытается сопоставить this|that alternates:

  • \1., то есть, какая группа 1 соответствует (self reference!), плюс один из "any" character,
  • или ^., то есть просто "любой" один символ в начале

Обратите внимание, что в группе 1 мы имеем ссылку на то, что соответствует группе 1! Это вложенная/самостоятельная ссылка, и это основная идея, представленная в этом примере. Имейте в виду, что, когда группа захвата повторяется, обычно она сохраняет только последний захват, поэтому самооценка в этом случае по существу говорит:

"Постарайтесь сопоставить то, что я сопоставлял в прошлый раз, плюс еще один. Это то, что я буду на этот раз".

Подобно рекурсии, должен быть "базовый случай" с саморекламами. На первой итерации + группа 1 еще ничего не зафиксировала (это НЕ то же самое, что сказать, что она начинается с пустой строки). Следовательно, вводится второе чередование, как способ "инициализировать" группу 1, которая позволяет захватить один символ, когда он находится в начале строки.

Итак, поскольку он повторяется с +, группа 1 сначала пытается сопоставить 1 символ, затем 2, затем 3, затем 4 и т.д. Сумма этих чисел является треугольным числом.


Дальнейшие исследования

Обратите внимание, что для упрощения мы использовали строки, которые состоят из того же повторяющегося символа, что и наш ввод. Теперь, когда мы знаем, как работает этот шаблон, мы можем видеть, что этот шаблон также может соответствовать строкам типа "1121231234", "aababc" и т.д.

Заметим также, что если мы обнаружим, что n является треугольным числом, т.е. n = 1 + 2 +... + k, длина строки, захваченной группой 1 в конце, будет k.

Обе эти точки показаны в следующем фрагменте С# (также см. на ideone.com):

Regex r = new Regex(@"^(\1.|^.)+$");

Console.WriteLine(r.IsMatch("aababc"));     // True
Console.WriteLine(r.IsMatch("1121231234")); // True
Console.WriteLine(r.IsMatch("iLoveRegEx")); // False

for (int n = 0; n <= 50; n++) {
    Match m = r.Match("".PadLeft(n));
    if (m.Success) {
       Console.WriteLine("{0} = sum(1..{1})", n, m.Groups[1].Length);
    }
}
// 1 = sum(1..1)
// 3 = sum(1..2)
// 6 = sum(1..3)
// 10 = sum(1..4)
// 15 = sum(1..5)
// 21 = sum(1..6)
// 28 = sum(1..7)
// 36 = sum(1..8)
// 45 = sum(1..9)

Вкусные примечания

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

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

Java немного отличается тем, что String.matches, Pattern.matches и Matcher.matches попытаться сопоставить шаблон со всей входной строкой. Поэтому анкеры могут быть опущены в приведенном выше фрагменте.

Обратите внимание, что в других контекстах вам может понадобиться вместо \A и \Z привязки. Например, в многострочный режим, ^ и $ соответствуют началу и концу каждой строки на входе.

Последнее, что в .NET regex, вы действительно можете получить все промежуточные захваты, сделанные группой повторного захвата. В большинстве случаев вы не можете: все промежуточные захваты теряются, и вы можете сохранить последнее.

Связанные вопросы


Бонусный материал: использование регулярных выражений для поиска мощности двойки!!!

С очень незначительной модификацией вы можете использовать те же методы, которые представлены здесь, чтобы найти мощность двух.

Здесь основное математическое свойство, которое вы хотите использовать:

  • 1 = 1
  • 2 = (1) + 1
  • 4 = (1 + 2) + 1
  • 8 = (1 + 2 + 4) + 1
  • 16 = (1 + 2 + 4 + 8) + 1
  • 32 = (1 + 2 + 4 + 8 + 16) + 1

Решение приведено ниже (но сначала попробуйте решить его!!!!)

(см. на ideone.com в PHP, Java и С#):

^(\1\1|^.)*.$