คำตอบคือไม่จำเป็นต้องพูดว่าใช่! แน่นอนที่สุดคุณสามารถเขียนรูปแบบ regex Java เพื่อให้ตรงกับnข n ใช้การมองเชิงบวกในการยืนยันและการอ้างอิงแบบซ้อนหนึ่งรายการสำหรับ "การนับ"
แทนที่จะให้รูปแบบทันทีคำตอบนี้จะแนะนำผู้อ่านตลอดขั้นตอนการได้มา คำแนะนำต่างๆจะได้รับเนื่องจากการแก้ปัญหาถูกสร้างขึ้นอย่างช้าๆ ในแง่นี้หวังว่าคำตอบนี้จะมีมากกว่ารูปแบบนิพจน์ทั่วไปอื่น ๆ หวังเป็นอย่างยิ่งว่าผู้อ่านจะได้เรียนรู้วิธี "คิดใน regex" และวิธีการรวมโครงสร้างต่างๆเข้าด้วยกันอย่างกลมกลืนเพื่อให้พวกเขาได้รับรูปแบบเพิ่มเติมด้วยตนเองในอนาคต
ภาษาที่ใช้ในการพัฒนาโซลูชันจะเป็นภาษา PHP เพื่อความกระชับ การทดสอบขั้นสุดท้ายเมื่อเสร็จสิ้นรูปแบบจะเสร็จสิ้นใน Java
ขั้นตอนที่ 1: Lookahead สำหรับการยืนยัน
เริ่มต้นให้กับปัญหาที่เรียบง่าย: เราต้องการเพื่อให้ตรงกับa+
ที่จุดเริ่มต้นของสตริง b+
แต่ถ้ามันทันทีตามด้วย เราสามารถใช้^
ในการยึดจับคู่ของเราและเนื่องจากเราเพียงต้องการให้ตรงกับa+
โดยไม่ต้องb+
เราสามารถใช้lookahead(?=…)
ยืนยัน
นี่คือรูปแบบของเราด้วยสายรัดทดสอบง่ายๆ:
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+
บทเรียน : คุณสามารถใช้รูปแบบในการมองหาเพื่อทำการยืนยัน
ขั้นตอนที่ 2: การถ่ายภาพในรูปลักษณ์ (และโหมดระยะห่างฟรี)
สมมติว่าแม้ว่าเราไม่ต้องการb+
ให้เป็นส่วนหนึ่งของการแข่งขัน แต่เราก็ต้องการที่จะจับมันเป็นกลุ่ม 1 ด้วยเช่นกันเนื่องจากเราคาดว่าจะมีรูปแบบที่ซับซ้อนมากขึ้นเรามาใช้x
ตัวปรับแต่งสำหรับการเว้นระยะห่างกัน สามารถทำให้ regex ของเราอ่านง่ายขึ้น
จากตัวอย่าง 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
โปรดทราบว่าเช่นaaa|b
เป็นผลมาจากไอเอ็นจีสิ่งที่แต่ละกลุ่มจับด้วยjoin
'|'
ในกรณีนี้กลุ่ม 0 (คือสิ่งที่รูปแบบการจับคู่) จับaaa
และกลุ่มที่ 1 b
จับ
บทเรียน : คุณสามารถจับภาพภายในการค้นหาได้ คุณสามารถใช้การเว้นระยะห่างเพื่อเพิ่มความสามารถในการอ่าน
ขั้นตอนที่ 3: ปรับโครงสร้าง Lookahead ให้เป็น "ลูป"
ก่อนที่เราจะแนะนำกลไกการนับของเราเราต้องทำการปรับเปลี่ยนรูปแบบของเราก่อน ปัจจุบัน Lookahead อยู่นอก+
"วนซ้ำ" นี้จะปรับเพื่อให้ห่างไกลเพราะเราแค่อยากจะยืนยันว่ามีb+
ของเราต่อไปa+
แต่สิ่งที่เราจริงๆต้องการที่จะทำที่สุดก็คือยืนยันว่าแต่ละa
ที่เราจะจับคู่ภายใน "ห่วง" มีความสอดคล้องb
ไปกับมัน
อย่ากังวลกับกลไกการนับในตอนนี้และทำการ refactoring ดังต่อไปนี้:
- ตัวอ้างอิงแรก
a+
ถึง(?: a )+
(โปรดทราบว่า(?:…)
เป็นกลุ่มที่ไม่จับภาพ)
- จากนั้นย้ายผู้มองเข้าไปในกลุ่มที่ไม่ได้จับภาพนี้
- โปรดทราบว่าตอนนี้เราต้อง "ข้าม"
a*
ก่อนจึงจะ "เห็น" ได้b+
ดังนั้นให้ปรับเปลี่ยนรูปแบบตามนั้น
ตอนนี้เรามีสิ่งต่อไปนี้:
$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
# │ │ └──┘ │ │
# │ │ 1 │ │
# │ └───────────┘ │
# │ lookahead │
# └───────────────────┘
# non-capturing group
ผลลัพธ์จะเหมือนเดิม ( ตามที่เห็นใน ideone.com ) ดังนั้นจึงไม่มีการเปลี่ยนแปลงในเรื่องนั้น สิ่งสำคัญคือตอนนี้เรากำลังยืนยันในทุก ๆ ครั้งของ+
"ลูป" ด้วยรูปแบบปัจจุบันของเราสิ่งนี้ไม่จำเป็น แต่ต่อไปเราจะทำให้กลุ่ม 1 "นับ" สำหรับเราโดยใช้การอ้างอิงตัวเอง
บทเรียน : คุณสามารถจับภาพภายในกลุ่มที่ไม่ได้จับภาพ Lookarounds สามารถทำซ้ำได้
ขั้นตอนที่ 4: นี่คือขั้นตอนที่เราเริ่มนับ
นี่คือสิ่งที่เราจะทำ: เราจะเขียนกลุ่ม 1 ใหม่ว่า:
- ในตอนท้ายของการวนซ้ำครั้งแรกของการจับคู่
+
ครั้งแรกa
ควรจับภาพb
- ในตอนท้ายของการทำซ้ำครั้งที่สองเมื่อ
a
มีการจับคู่อีกรายการก็ควรจับภาพbb
- ในตอนท้ายของการทำซ้ำครั้งที่สามควรจับภาพ
bbb
- ...
- ในตอนท้ายของการวนซ้ำที่nกลุ่ม 1 ควรจับภาพb n
- หากมีไม่เพียงพอที่
b
จะจับเข้ากลุ่ม 1 การยืนยันก็ล้มเหลว
ดังนั้นกลุ่มที่ 1 ซึ่งขณะนี้จะต้องถูกเขียนใหม่เพื่อสิ่งที่ต้องการ(b+)
(\1 b)
นั่นคือเราพยายาม "เพิ่ม" b
สิ่งที่กลุ่ม 1 จับได้ในการทำซ้ำก่อนหน้านี้
มีปัญหาเล็กน้อยที่รูปแบบนี้ไม่มี "ตัวพิมพ์ฐาน" นั่นคือกรณีที่สามารถจับคู่ได้โดยไม่ต้องอ้างอิงตัวเอง จำเป็นต้องมีกรณีพื้นฐานเนื่องจากกลุ่ม 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
และยังไงก็นับผิด! เราจะตรวจสอบว่าเหตุใดจึงเกิดขึ้นในขั้นตอนต่อไป
บทเรียน : วิธีหนึ่งในการ "เริ่มต้น" กลุ่มอ้างอิงตัวเองคือการทำให้การจับคู่อ้างอิงตัวเองเป็นทางเลือก
ขั้นตอนที่4½: ทำความเข้าใจกับสิ่งที่ผิดพลาด
ปัญหาคือว่าตั้งแต่ที่เราทำไม่จำเป็นที่ตรงกันอ้างอิงตนเองที่ "เคาน์เตอร์" สามารถ "ตั้งค่า" กลับไปที่ 0 เมื่อมีไม่เพียงพอb
's ลองตรวจสอบอย่างละเอียดว่าเกิดอะไรขึ้นในทุก ๆ การวนซ้ำของรูปแบบของเราด้วยการ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
เวลานี้)
บทเรียน : ระวังการย้อนรอย เอนจิน regex จะทำการย้อนรอยมากเท่าที่คุณอนุญาตจนกว่ารูปแบบที่กำหนดจะตรงกัน ซึ่งอาจส่งผลกระทบต่อประสิทธิภาพ (เช่นการย้อนรอยแบบหายนะ ) และ / หรือความถูกต้อง
ขั้นตอนที่ 5: ครอบครองตัวเองเพื่อช่วยเหลือ!
ตอนนี้ "แก้ไข" ควรชัดเจน: รวมการทำซ้ำที่เป็นทางเลือกกับตัวระบุปริมาณที่เป็นเจ้าของ นั่นคือแทนที่จะใช้เพียงแค่?
ใช้?+
แทน (โปรดจำไว้ว่าการทำซ้ำที่มีปริมาณเป็นความเป็นเจ้าของจะไม่ย้อนกลับแม้ว่า "ความร่วมมือ" ดังกล่าวอาจส่งผลให้รูปแบบโดยรวมตรงกันก็ตาม)
ในแง่ที่ไม่เป็นทางการมากนี้เป็นสิ่งที่?+
, ?
และ??
พูดว่า:
?+
- (ไม่บังคับ) "ไม่จำเป็นต้องมี"
- (เป็นเจ้าของ) "แต่ถ้ามีก็ต้องรับไว้ไม่ปล่อย!"
?
- (ไม่บังคับ) "ไม่จำเป็นต้องมี"
- (โลภ) "แต่ถ้าเป็นอย่างนั้นคุณสามารถรับได้ในตอนนี้"
- (backtracking) "แต่คุณอาจถูกขอให้ปล่อยในภายหลัง!"
??
- (ไม่บังคับ) "ไม่จำเป็นต้องมี"
- (อิดออด) "และแม้ว่าคุณจะยังไม่ต้องรับมันก็ตาม"
- (backtracking) "แต่คุณอาจถูกขอให้ดำเนินการในภายหลัง!"
ในการตั้งค่าของเรา\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!!!
โวลา !!! แก้ไขปัญหา!!! ตอนนี้เรากำลังนับอย่างถูกต้องตรงตามที่เราต้องการ!
บทเรียน : เรียนรู้ความแตกต่างระหว่างการทำซ้ำแบบโลภลังเลและเป็นเจ้าของ ตัวเลือกที่เป็นเจ้าของอาจเป็นการผสมผสานที่ทรงพลัง
ขั้นตอนที่ 6: เสร็จสิ้นการสัมผัส
ดังนั้นสิ่งที่เรามีในตอนนี้คือรูปแบบที่จับคู่a
ซ้ำ ๆ กันและสำหรับทุกรายการa
ที่ตรงกันจะมีการจับคู่ที่ตรงกันb
ในกลุ่ม 1 การ+
สิ้นสุดเมื่อไม่มีอีกต่อa
ไปหรือหากการยืนยันล้มเหลวเนื่องจากไม่มีข้อมูลที่ตรงกันb
สำหรับ กa
.
\1 $
เสร็จงานเราก็ต้องผนวกกับรูปแบบของเรา ตอนนี้เป็นการอ้างอิงย้อนกลับว่ากลุ่ม 1 จับคู่อะไรแล้วตามด้วยจุดสิ้นสุดของจุดยึดบรรทัด จุดยึดช่วยให้มั่นใจได้ว่าไม่มีอะไรพิเศษb
ในสตริง ในคำอื่น ๆ ที่ในความเป็นจริงเรามีnข 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ข 5000 มันใช้เวลาเพียง 0.06s เพื่อให้ทำงานบน ideone.com
ขั้นตอนที่ 7: การทดสอบ Java
ดังนั้นรูปแบบจึงทำงานใน PHP แต่เป้าหมายสูงสุดคือการเขียนรูปแบบที่ใช้งานได้ใน Java
public static void main(String[] args) {
String aNbN = "(?x) (?: a (?= a* (\\1?+ b)) )+ \\1";
String[] tests = {
"",
"ab",
"abb",
"aab",
"aabb",
"abab",
"abc",
repeat('a', 5000) + repeat('b', 4999),
repeat('a', 5000) + repeat('b', 5000),
repeat('a', 5000) + repeat('b', 5001),
};
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*
เห็นและแท้จริงคือ " +
ลูปหลัก" ทั้งสองอนุญาตให้มีการย้อนรอย ขอแนะนำให้ผู้อ่านยืนยันว่าเหตุใดจึงไม่เป็นปัญหาในแง่ของความถูกต้องและเหตุใดการทำให้ทั้งสองเป็นเจ้าของในเวลาเดียวกันก็ใช้ได้ผลเช่นกัน (แม้ว่าการผสมตัวบ่งชี้การครอบครองที่บังคับและไม่บังคับในรูปแบบเดียวกันอาจทำให้เกิดความเข้าใจผิดได้)
นอกจากนี้ยังควรจะกล่าวว่าในขณะที่มันเรียบร้อยว่ามีรูปแบบการ regex ที่จะตรงกับnขnนี้เป็นไปไม่ได้เสมอในการแก้ปัญหาที่ "ดีที่สุด" ในทางปฏิบัติ ทางออกที่ดีกว่ามากคือจับคู่จากนั้นเปรียบเทียบความยาวของสตริงที่จับโดยกลุ่ม 1 และ 2 ในภาษาโปรแกรมโฮสติ้ง^(a+)(b+)$
ใน PHP อาจมีลักษณะดังนี้ ( ดังที่เห็นใน ideone.com ):
function is_anbn($s) {
return (preg_match('/^(a+)(b+)$/', $s, $groups)) &&
(strlen($groups[1]) == strlen($groups[2]));
}
จุดประสงค์ของบทความนี้ไม่ใช่เพื่อโน้มน้าวผู้อ่านว่า regex สามารถทำเกือบทุกอย่าง เห็นได้ชัดว่าไม่สามารถทำได้และแม้กระทั่งสำหรับสิ่งที่ทำได้ควรพิจารณาการมอบหมายบางส่วนไปยังภาษาโฮสติ้งหากนำไปสู่โซลูชันที่ง่ายกว่า
ดังที่กล่าวไว้ด้านบนในขณะที่บทความนี้จำเป็นต้องติดแท็ก[regex]
สำหรับ stackoverflow แต่อาจจะมากกว่านั้น แม้ว่าจะมีคุณค่าในการเรียนรู้เกี่ยวกับการยืนยันการอ้างอิงที่ซ้อนกันตัวบ่งชี้ความเป็นเจ้าของ ฯลฯ บางทีบทเรียนที่ใหญ่กว่าในที่นี้คือกระบวนการสร้างสรรค์ที่เราสามารถพยายามแก้ปัญหาความมุ่งมั่นและการทำงานหนักที่มักต้องใช้เมื่อคุณต้องเผชิญ ข้อ จำกัด ต่างๆองค์ประกอบที่เป็นระบบจากส่วนต่างๆเพื่อสร้างโซลูชันการทำงาน ฯลฯ
วัสดุโบนัส! รูปแบบการเรียกซ้ำ PCRE!
เนื่องจากเรานำ PHP มาใช้จึงจำเป็นต้องบอกว่า PCRE รองรับรูปแบบการเรียกซ้ำและรูทีนย่อย ดังนั้นรูปแบบต่อไปนี้จึงใช้ได้กับpreg_match
( ตามที่เห็นใน ideone.com ):
$rRecursive = '/ ^ (a (?1)? b) $ /x';
ปัจจุบัน regex ของ Java ไม่รองรับรูปแบบการเรียกซ้ำ
วัสดุโบนัสมากยิ่งขึ้น! จับคู่nขnคn !!
ดังนั้นเราจึงได้เห็นวิธีการเพื่อให้ตรงกับnขnซึ่งเป็นที่ไม่ปกติ แต่ยังคงบริบทฟรี แต่สามารถเรายังตรงกับnขnคnซึ่งไม่ได้แม้บริบทฟรีหรือไม่
คำตอบคือแน่นอนใช่! ขอแนะนำให้ผู้อ่านลองแก้ปัญหานี้ด้วยตัวเอง แต่วิธีแก้ปัญหามีให้ด้านล่าง (พร้อมการใช้งานใน Java บน ideone.com )
^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $