대답은 말할 필요도없이 YES입니다! n b n 과 일치 하는 Java 정규식 패턴을 가장 확실하게 작성할 수 있습니다 . 어설 션에는 긍정적 인 예견을 사용하고 "카운팅"에는 하나의 중첩 참조를 사용합니다.
이 답변은 즉시 패턴을 제공하는 대신 독자에게 패턴 을 유도 하는 과정 을 안내 합니다. 솔루션이 천천히 구성됨에 따라 다양한 힌트가 제공됩니다. 이 측면에서 바라건대이 답변은 다른 깔끔한 정규식 패턴 이상을 포함 할 것입니다. 독자들이 "정규식으로 생각"하는 방법과 다양한 구성을 조화롭게 조합하는 방법을 배우면 앞으로 스스로 더 많은 패턴을 도출 할 수 있기를 바랍니다.
솔루션 개발에 사용되는 언어는 간결함을 위해 PHP입니다. 패턴이 완성되면 최종 테스트는 Java로 수행됩니다.
1 단계 : 어설 션에 대한 예측
더 간단한 문제로 시작 해보자 : 우리 a+
는 문자열의 시작 부분에서 일치 를 원 하지만 바로 뒤에 b+
. 우리가 사용 ^
하는 앵커 우리의 일치, 우리는 단지를 일치시킬 이후 a+
를 빼고 b+
, 우리가 사용할 수 있습니다 내다 주장을 (?=…)
.
다음은 간단한 테스트 도구를 사용한 패턴입니다.
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+
.
Lesson : 어설 션을 만들기 위해 lookaround에서 패턴을 사용할 수 있습니다.
2 단계 : 미리보기 (및 자유 간격 모드)에서 캡처
이제 b+
경기의 일부가되는 것을 원하지 않더라도 어쨌든 그룹 1로 캡처 하고 싶다고 가정 해 봅시다. 또한 더 복잡한 패턴이있을 것으로 예상 하므로 자유 간격에x
modifier를 사용 하겠습니다. 정규식을 더 읽기 쉽게 만들 수 있습니다.
이전 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
예를 들어 각 그룹이으로 캡처 한 -ing aaa|b
결과입니다 . 이 경우 그룹 0 (즉 , 일치하는 패턴)이 캡처 되고 그룹 1이 캡처 됩니다.join
'|'
aaa
b
Lesson : 둘러보기 내부를 캡처 할 수 있습니다. 빈 공간을 사용하여 가독성을 높일 수 있습니다.
3 단계 : 미리보기를 "루프"로 리팩토링
카운팅 메커니즘을 도입하기 전에 패턴을 한 가지 수정해야합니다. 현재, 미리보기는 +
반복 "루프" 밖에 있습니다. 이것은 우리가 우리의 b+
뒤에 오는 것이 있다고 단언하고 싶었 기 때문에 지금까지는 괜찮습니다 a+
. 그러나 우리가 정말로 하고 싶은 a
것은 우리가 "루프"내부에서 일치 하는 각각에 대해 b
그것에 상응하는 것이 있다고 주장하는 것입니다.
지금은 계산 메커니즘에 대해 걱정하지 말고 다음과 같이 리팩토링을 수행하십시오.
- 우선 팩터
a+
에 (?: a )+
(주는 (?:…)
비 촬상 기)
- 그런 다음이 비 캡처 그룹 내에서 예견을 이동합니다.
- 이제를 "보기"
a*
전에 "건너 뛰기" 해야 b+
하므로 그에 따라 패턴을 수정해야합니다.
이제 우리는 다음을 가지고 있습니다.
$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
# │ │ └──┘ │ │
# │ │ 1 │ │
# │ └───────────┘ │
# │ lookahead │
# └───────────────────┘
# non-capturing group
출력은 이전과 동일하므로 ( ideone.com에서 볼 수 있음 ) 이와 관련하여 변경 사항이 없습니다. 중요한 것은 이제 우리는 "루프" 의 모든 반복 에서 주장을하고 있다는 것입니다 +
. 현재 패턴에서는 이것이 필요하지 않지만 다음으로 자기 참조를 사용하여 그룹 1을 "수"로 만들 것입니다.
Lesson : 비 캡처 그룹 내에서 캡쳐 할 수 있습니다. 둘러보기를 반복 할 수 있습니다.
4 단계 : 계산을 시작하는 단계입니다.
다음과 같이 할 것입니다. 그룹 1을 다음과 같이 다시 작성합니다.
- 의 첫 번째 반복이 끝날
+
때 첫 번째 항목 a
이 일치하면 캡처해야합니다.b
- 두 번째 반복이 끝날 때 다른 반복
a
이 일치하면bb
- 세 번째 반복이 끝나면 다음을 캡처해야합니다.
bbb
- ...
- n 번째 반복 이 끝나면 그룹 1은 b n을 캡처해야합니다.
b
그룹 1로 캡처 하기에 충분하지 않으면 어설 션이 실패합니다.
지금은 그룹 1, 그래서 (b+)
, 같은를 다시 작성해야합니다 (\1 b)
. 즉 b
, 이전 반복에서 캡처 한 그룹 1에 a를 "추가"하려고합니다 .
여기에는이 패턴에 "기본 케이스"가 누락되어 있다는 점에서 약간의 문제가 있습니다. 즉, 자체 참조없이 일치 할 수있는 경우입니다. 그룹 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을 "계수"하도록 관리했습니다! 하지만 잠깐 ... 두 번째 및 마지막 테스트 케이스에 문제가 있습니다 !! b
s 가 충분하지 않으며 어떻게 든 잘못 계산했습니다! 다음 단계에서 왜 이런 일이 발생했는지 살펴 보겠습니다.
Lesson : 자체 참조 그룹을 "초기화"하는 한 가지 방법은 자체 참조 일치를 선택 사항으로 만드는 것입니다.
4½ 단계 : 무엇이 잘못되었는지 이해
문제는 우리가 자기 참조 매칭을 선택적으로 만들었 기 때문에 "카운터"가 충분하지 않을 때 "재설정"할 수 있다는 것 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
아하! 4 번째 반복에서는 여전히 일치 할 수 있지만 일치 \1
할 수 없습니다 \1b
! 자체 참조 일치를에서 선택 사항으로 허용하기 때문에 \1?
엔진은 역 추적하고 "아니요"옵션을 선택하여 일치하고 캡처 할 수 있습니다 b
.
그러나 첫 번째 반복을 제외하고는 항상 자체 참조 만 일치시킬 수 \1
있습니다. 우리가 우리의 이전 반복에서 캡처 한 것, 그리고 우리의 설정에서 우리는 항상 우리가 캡처 된 경우 (예 : 다시 일치 할 수 있기 때문에 이것은 물론, 분명 bbb
지난 시간, 우리는 여전히있을 것입니다 보장하고 bbb
있지만,이 수도 있고 bbbb
이번이 아닐 수도 있습니다 ).
교훈 : 역 추적을 조심하세요. 정규식 엔진은 주어진 패턴이 일치 할 때까지 허용하는만큼 역 추적을 수행합니다. 이는 성능 (예 : 치명적인 역 추적 ) 및 / 또는 정확성에영향을 미칠 수 있습니다.
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!!!
Voilà !!! 문제 해결됨!!! 우리는 이제 정확히 우리가 원하는 방식으로 정확하게 계산하고 있습니다!
Lesson : 욕심, 주저, 소유 반복의 차이를 배웁니다. 선택적 소유는 강력한 조합이 될 수 있습니다.
6 단계 : 터치 마무리
그래서 지금 우리가 가지고있는 것은 a
반복적으로 일치하는 패턴입니다. 일치하는 모든 패턴에 대해 그룹 1에서 캡처 된 a
해당 패턴 이 있습니다. 더 이상없는 경우 또는 해당 대상 이 없어서 주장이 실패하면 종료됩니다. .b
+
a
b
a
작업을 완료하려면 패턴에 추가하기 만하면 \1 $
됩니다. 이것은 이제 어떤 그룹 1이 일치하는지에 대한 역 참조이며 그 뒤에 라인 앵커의 끝이 이어집니다. 앵커는 b
문자열에 여분 의 가 없도록 합니다. 즉, 사실 우리는 a n b n을 가지고 있습니다.
다음은 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 B 5000 . 그것은 걸립니다 만 ideone.com에서 실행 0.06s .
7 단계 : 자바 테스트
따라서 패턴은 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*
룩어 헤드와 실제로 "주 +
루프"는 둘 다 역 추적을 허용 한다고 말할 필요가 있습니다 . 독자들은 이것이 정확성 측면에서 왜 문제가되지 않는지, 왜 동시에 소유욕으로 만드는 것이 효과가 있는지 확인하도록 권장됩니다 (동일한 패턴에서 필수 소유물과 비 필수 소유물 수량자를 혼합하면 오해로 이어질 수 있음).
또한 n b n 과 일치 하는 정규식 패턴이 있다는 것이 깔끔하지만 실제로 이것이 항상 "최상의"솔루션은 아닙니다. 훨씬 더 나은 솔루션은 단순히을 일치 ^(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 !!
그래서 우리 는 비정규 적이지만 여전히 컨텍스트가없는 n b n 을 일치 시키는 방법을 보았습니다 .하지만 컨텍스트가없는 것도 아닌 n b n c n을 일치시킬 수도 있습니까?
물론 대답은 예입니다! 독자는이 문제를 스스로 해결하도록 권장하지만 솔루션은 아래에 제공됩니다 ( ideone.com의 Java 구현 포함 ).
^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $