Câu trả lời là, không cần phải nói, CÓ! Chắc chắn bạn có thể viết một mẫu regex Java để khớp với một n b n . Nó sử dụng một cái nhìn tích cực để khẳng định và một tham chiếu lồng nhau để "đếm".
Thay vì đưa ra mô hình ngay lập tức, câu trả lời này sẽ hướng dẫn người đọc trong quá trình tìm ra nó. Nhiều gợi ý khác nhau được đưa ra khi giải pháp được xây dựng chậm. Ở khía cạnh này, hy vọng câu trả lời này sẽ chứa đựng nhiều thứ hơn là chỉ một mẫu regex gọn gàng khác. Hy vọng rằng độc giả cũng sẽ học được cách "suy nghĩ trong regex", và cách kết hợp các cấu trúc khác nhau một cách hài hòa với nhau, để họ có thể tự tìm ra nhiều mẫu hơn trong tương lai.
Ngôn ngữ được sử dụng để phát triển giải pháp sẽ là PHP vì tính ngắn gọn của nó. Kiểm tra cuối cùng sau khi hoàn tất mẫu sẽ được thực hiện trong Java.
Bước 1: Nhìn trước để khẳng định
Hãy bắt đầu với một vấn đề đơn giản hơn: chúng ta muốn so khớp a+
ở đầu một chuỗi, nhưng chỉ khi nó được theo sau ngay lập tức b+
. Chúng ta có thể sử dụng ^
để neo trận đấu của chúng tôi, và vì chúng tôi chỉ muốn để phù hợp với a+
mà không có b+
, chúng ta có thể sử dụng lookahead khẳng định (?=…)
.
Đây là mẫu của chúng tôi với một dây nịt thử nghiệm đơn giản:
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);
Đầu ra là ( như đã thấy trên ideone.com ):
aaa 0
aaab 1 aaa
aaaxb 0
xaaab 0
b 0
abbb 1 a
Đây chính xác là kết quả chúng tôi muốn: chúng tôi khớp a+
, chỉ khi nó ở đầu chuỗi và chỉ khi nó ngay sau đó b+
.
Bài học : Bạn có thể sử dụng các mẫu trong cách xem xét để đưa ra khẳng định.
Bước 2: Chụp ở chế độ nhìn trước (và chế độ giãn cách tự do)
Bây giờ chúng ta hãy nói rằng mặc dù chúng tôi không muốn b+
trở thành một phần của trận đấu, chúng tôi muốn nắm bắt nó anyway vào nhóm 1. Ngoài ra, như chúng tôi dự kiến có một mô hình phức tạp hơn, chúng ta hãy sử dụng của x
modifier cho tự do khoảng cách vì vậy chúng tôi có thể làm cho regex của chúng tôi dễ đọc hơn.
Dựa trên đoạn mã PHP trước đây của chúng tôi, bây giờ chúng tôi có mẫu sau:
$r2 = '/ ^ a+ (?= (b+) ) /x';
# │ └──┘ │
# │ 1 │
# └────────┘
# lookahead
testAll($r2, $tests);
Kết quả bây giờ là ( như đã thấy trên ideone.com ):
aaa 0
aaab 1 aaa|b
aaaxb 0
xaaab 0
b 0
abbb 1 a|bbb
Lưu ý rằng ví dụ: aaa|b
là kết quả của join
-ing những gì mỗi nhóm được chụp '|'
. Trong trường hợp này, nhóm 0 (tức là mẫu phù hợp) được chụp aaa
và nhóm 1 được chụp b
.
Bài học : Bạn có thể nắm bắt bên trong một cái nhìn bao quát. Bạn có thể sử dụng khoảng cách trống để nâng cao khả năng đọc.
Bước 3: Cấu trúc lại lookahead vào "vòng lặp"
Trước khi có thể giới thiệu cơ chế đếm của mình, chúng ta cần thực hiện một sửa đổi đối với mẫu của mình. Hiện tại, lookahead nằm ngoài +
"vòng lặp" lặp lại. Điều này là tốt cho đến nay bởi vì chúng tôi chỉ muốn khẳng định rằng có một số b+
theo dõi của chúng tôi a+
, nhưng những gì chúng tôi thực sự muốn làm cuối cùng là khẳng định rằng đối với mỗi a
cái mà chúng tôi khớp bên trong "vòng lặp", sẽ có một phần tương ứng b
đi kèm với nó.
Bây giờ đừng lo lắng về cơ chế đếm và chỉ cần thực hiện cấu trúc lại như sau:
- Cơ cấu lại đầu tiên
a+
cho (?: a )+
(lưu ý rằng đó (?:…)
là một nhóm không nắm bắt)
- Sau đó di chuyển hướng nhìn vào bên trong nhóm không chụp này
- Lưu ý rằng bây giờ chúng ta phải "bỏ qua"
a*
trước khi có thể "nhìn thấy" b+
, vì vậy hãy sửa đổi mẫu cho phù hợp
Vì vậy, bây giờ chúng ta có những thứ sau:
$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
# │ │ └──┘ │ │
# │ │ 1 │ │
# │ └───────────┘ │
# │ lookahead │
# └───────────────────┘
# non-capturing group
Đầu ra vẫn giống như trước đây ( như đã thấy trên ideone.com ), vì vậy không có thay đổi về vấn đề đó. Điều quan trọng là bây giờ chúng ta đang thực hiện khẳng định tại mỗi lần lặp của +
"vòng lặp". Với mẫu hiện tại của chúng tôi, điều này là không cần thiết, nhưng tiếp theo chúng tôi sẽ đặt "tính" nhóm 1 cho chúng tôi bằng cách sử dụng tự tham chiếu.
Bài học : Bạn có thể chụp bên trong một nhóm không chụp. Các cách nhìn có thể được lặp lại.
Bước 4: Đây là bước mà chúng ta bắt đầu đếm
Đây là những gì chúng tôi sẽ làm: chúng tôi sẽ viết lại nhóm 1 sao cho:
- Vào cuối lần lặp đầu tiên của
+
, khi lần đầu tiên a
được so khớp, nó sẽ nắm bắtb
- Vào cuối lần lặp thứ hai, khi một lần lặp khác
a
được so khớp, nó sẽ nắm bắtbb
- Vào cuối lần lặp thứ ba, nó sẽ nắm bắt
bbb
- ...
- Vào cuối lần lặp thứ n , nhóm 1 sẽ nắm bắt được b n
- Nếu không có đủ
b
để nắm bắt vào nhóm 1 thì xác nhận đơn giản là không thành công
Vì vậy, nhóm 1, bây giờ (b+)
, sẽ phải được viết lại thành một cái gì đó giống như (\1 b)
. Đó là, chúng tôi cố gắng "thêm" a b
vào nhóm 1 đã được chụp trong lần lặp trước.
Có một vấn đề nhỏ ở đây là mẫu này thiếu "trường hợp cơ sở", tức là trường hợp mà nó có thể khớp mà không cần tự tham chiếu. Một trường hợp cơ sở là bắt buộc vì nhóm 1 bắt đầu "chưa được khởi tạo"; nó vẫn chưa nắm bắt bất kỳ thứ gì (thậm chí không phải là một chuỗi trống), vì vậy nỗ lực tự tham chiếu sẽ luôn thất bại.
Có nhiều cách giải quyết vấn đề này, nhưng bây giờ chúng ta hãy chỉ làm cho đối sánh tự tham chiếu là tùy chọn , tức là \1?
. Điều này có thể hoạt động hoàn hảo hoặc có thể không hoàn hảo, nhưng chúng ta hãy xem điều đó có tác dụng gì, và nếu có bất kỳ vấn đề gì thì chúng ta sẽ vượt qua cây cầu đó khi chúng ta đến với nó. Ngoài ra, chúng tôi sẽ thêm một số trường hợp thử nghiệm khác trong khi chúng tôi đang ở đó.
$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb'
);
$r4 = '/ ^ (?: a (?= a* (\1? b) ) )+ /x';
# │ │ └─────┘ | │
# │ │ 1 | │
# │ └──────────────┘ │
# │ lookahead │
# └──────────────────────┘
# non-capturing group
Kết quả bây giờ là ( như đã thấy trên 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....
A-ha! Có vẻ như bây giờ chúng ta đã thực sự gần đến giải pháp! Chúng tôi đã quản lý để đưa nhóm 1 "đếm" bằng cách sử dụng tự tham chiếu! Nhưng khoan đã ... có gì đó không ổn với trường hợp thử nghiệm thứ hai và cuối cùng !! Không có đủ b
s, và bằng cách nào đó nó được tính sai! Chúng tôi sẽ xem xét lý do tại sao điều này xảy ra trong bước tiếp theo.
Bài học : Một cách để "khởi tạo" nhóm tự tham chiếu là làm cho đối sánh tự tham chiếu là tùy chọn.
Bước 4½: Tìm hiểu vấn đề đã xảy ra
Vấn đề là vì chúng tôi đã thực hiện tùy chọn đối sánh tự tham chiếu, nên "bộ đếm" có thể "đặt lại" về 0 khi không có đủ b
. Hãy kiểm tra chặt chẽ những gì xảy ra ở mỗi lần lặp lại mẫu của chúng ta với aaaaabbb
đầu vào.
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
A-ha! Ở lần lặp thứ 4, chúng tôi vẫn có thể khớp \1
, nhưng chúng tôi không thể khớp \1b
! Vì chúng tôi cho phép đối sánh tự tham chiếu là tùy chọn \1?
, động cơ sẽ lùi lại và sử dụng tùy chọn "không, cảm ơn", sau đó cho phép chúng tôi đối sánh và nắm bắt b
!
Tuy nhiên, hãy lưu ý rằng ngoại trừ lần lặp đầu tiên, bạn luôn có thể khớp chỉ với tự tham chiếu \1
. Tất nhiên, điều này là hiển nhiên, vì đó là những gì chúng tôi vừa chụp được trong lần lặp trước đó và trong thiết lập của mình, chúng tôi luôn có thể khớp lại nó (ví dụ: nếu chúng tôi đã chụp bbb
lần trước, chúng tôi đảm bảo rằng sẽ vẫn có bbb
, nhưng có thể hoặc có thể không phải là bbbb
lúc này).
Bài học : Cẩn thận với việc bẻ khóa. Công cụ regex sẽ thực hiện việc bẻ khóa ngược nhiều như bạn cho phép cho đến khi mẫu nhất định khớp. Điều này có thể ảnh hưởng đến hiệu suất (tức là nứt ngược thảm khốc ) và / hoặc tính đúng đắn.
Bước 5: Tự sở hữu để giải cứu!
"Sửa chữa" bây giờ nên rõ ràng: kết hợp lặp lại tùy chọn với định lượng sở hữu . Đó là, thay vì chỉ đơn giản ?
, hãy sử dụng ?+
thay thế (hãy nhớ rằng sự lặp lại được định lượng là sở hữu không có tác dụng ngược, ngay cả khi sự "hợp tác" như vậy có thể dẫn đến sự trùng khớp của mẫu tổng thể).
Theo các thuật ngữ rất thân mật, đây là những gì ?+
, ?
và ??
nói:
?+
- (tùy chọn) "Nó không cần phải ở đó,"
- (sở hữu) "nhưng nếu nó ở đó, bạn phải nắm lấy nó và không được buông ra!"
?
- (tùy chọn) "Nó không cần phải ở đó,"
- (tham lam) "nhưng nếu có, bạn có thể lấy nó ngay bây giờ,"
- (backtracking) "nhưng bạn có thể được yêu cầu bỏ qua sau!"
??
- (tùy chọn) "Nó không cần phải ở đó,"
- (miễn cưỡng) "và ngay cả khi đó là bạn vẫn chưa cần phải lấy nó"
- (backtracking) "nhưng bạn có thể được yêu cầu lấy nó sau!"
Trong thiết lập của chúng tôi, \1
sẽ không có ngay lần đầu tiên, nhưng nó sẽ luôn ở đó bất cứ lúc nào sau đó và chúng tôi luôn muốn khớp với nó sau đó. Do đó, \1?+
sẽ đạt được chính xác những gì chúng ta muốn.
$r5 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
Bây giờ đầu ra là ( như đã thấy trên 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à !!! Vấn đề đã được giải quyết !!! Bây giờ chúng tôi đang đếm đúng, chính xác theo cách chúng tôi muốn!
Bài học : Tìm hiểu sự khác biệt giữa sự lặp lại tham lam, miễn cưỡng và chiếm hữu. Sở hữu tùy chọn có thể là một sự kết hợp mạnh mẽ.
Bước 6: Hoàn thiện các bước chạm
Vì vậy, những gì chúng ta có ngay bây giờ là một mẫu khớp a
lặp đi lặp lại và đối với mỗi mẫu a
đã khớp, sẽ có một mẫu tương ứng b
được bắt trong nhóm 1. Mẫu +
chấm dứt khi không còn mẫu nào nữa a
hoặc nếu xác nhận không thành công vì không có mẫu tương ứng b
cho một a
.
Để hoàn thành công việc, chúng ta chỉ cần thêm vào mẫu của chúng ta \1 $
. Đây bây giờ là tham chiếu ngược về nhóm 1 đã khớp, tiếp theo là ký tự neo cuối dòng. Mỏ neo đảm bảo rằng không có bất kỳ phần thừa nào b
trong chuỗi; nói cách khác, trong thực tế, chúng ta có một n b n .
Đây là mẫu đã hoàn thiện, với các trường hợp thử nghiệm bổ sung, bao gồm cả một trường hợp dài 10.000 ký tự:
$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
Nó tìm thấy 4 trận đấu: ab
, aabb
, aaabbb
, và một 5000 b 5000 . Chỉ mất 0,06 giây để chạy trên ideone.com .
Bước 7: Kiểm tra Java
Vì vậy, mẫu hoạt động trong PHP, nhưng mục đích cuối cùng là viết một mẫu hoạt động trong 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);
}
Mô hình hoạt động như mong đợi ( như đã thấy trên ideone.com ).
Và bây giờ chúng ta đi đến kết luận ...
Cần phải nói rằng a*
trong cái nhìn và thực sự là " +
vòng lặp chính ", cả hai đều cho phép quay lui. Người đọc được khuyến khích xác nhận lý do tại sao đây không phải là một vấn đề về tính đúng đắn, và tại sao đồng thời khiến cho cả hai đều có hiệu quả (mặc dù có lẽ việc trộn định lượng sở hữu bắt buộc và không bắt buộc trong cùng một mẫu có thể dẫn đến nhận thức sai).
Cũng cần phải nói rằng mặc dù có một mẫu regex phù hợp với a n b n , nhưng đây không phải lúc nào cũng là giải pháp "tốt nhất" trong thực tế. Một giải pháp tốt hơn nhiều là chỉ cần đối sánh ^(a+)(b+)$
và sau đó so sánh độ dài của các chuỗi được nhóm 1 và 2 nắm bắt trong ngôn ngữ lập trình lưu trữ.
Trong PHP, nó có thể trông giống như thế này ( như được thấy trong ideone.com ):
function is_anbn($s) {
return (preg_match('/^(a+)(b+)$/', $s, $groups)) &&
(strlen($groups[1]) == strlen($groups[2]));
}
Mục đích của bài viết này KHÔNG phải để thuyết phục người đọc rằng regex có thể làm được hầu hết mọi thứ; nó rõ ràng là không thể, và ngay cả đối với những thứ nó có thể làm, ít nhất ủy quyền một phần cho ngôn ngữ lưu trữ nên được xem xét nếu nó dẫn đến một giải pháp đơn giản hơn.
Như đã đề cập ở trên, mặc dù bài viết này nhất thiết phải được gắn thẻ [regex]
cho stackoverflow, nhưng có lẽ nó còn nhiều hơn thế nữa. Mặc dù chắc chắn có giá trị trong việc tìm hiểu về các khẳng định, tham chiếu lồng nhau, định lượng sở hữu, v.v., nhưng có lẽ bài học lớn hơn ở đây là quá trình sáng tạo mà qua đó người ta có thể cố gắng giải quyết vấn đề, sự quyết tâm và chăm chỉ mà nó thường đòi hỏi khi bạn phải các ràng buộc khác nhau, thành phần có hệ thống từ các bộ phận khác nhau để xây dựng một giải pháp làm việc, v.v.
Tài liệu thưởng! Mẫu đệ quy PCRE!
Vì chúng tôi đã giới thiệu PHP, nên cần phải nói rằng PCRE hỗ trợ các chương trình con và mẫu đệ quy. Do đó, mẫu sau hoạt động cho preg_match
( như đã thấy trên ideone.com ):
$rRecursive = '/ ^ (a (?1)? b) $ /x';
Hiện tại regex của Java không hỗ trợ mẫu đệ quy.
Tài liệu thưởng nhiều hơn nữa! Phù hợp với a n b n c n !!
Vì vậy, chúng ta đã thấy làm thế nào để so khớp a n b n không chính quy, nhưng vẫn không có ngữ cảnh, nhưng chúng ta cũng có thể so khớp a n b n c n , thậm chí không có ngữ cảnh không?
Tất nhiên câu trả lời là CÓ! Người đọc được khuyến khích cố gắng tự giải quyết vấn đề này, nhưng giải pháp được cung cấp bên dưới (với việc triển khai bằng Java trên ideone.com ).
^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $