เราจะจับคู่ ^ nb ^ n กับ Java regex ได้อย่างไร


99

นี่เป็นส่วนที่สองของชุดบทความเกี่ยวกับ regex เพื่อการศึกษา มันแสดงให้เห็นว่า lookaheads และการอ้างอิงที่ซ้อนกันสามารถนำมาใช้เพื่อให้ตรงกับ languge ไม่ใช่ปกติnขn การอ้างอิงแบบซ้อนเป็นครั้งแรกใน: regex นี้ค้นหาตัวเลขสามเหลี่ยมได้อย่างไร

หนึ่งในภาษาที่ไม่ใช่ภาษาทั่วไปตามแบบฉบับคือ:

L = { an bn: n > 0 }

นี่คือภาษาของทุกสายไม่ว่างเปล่าซึ่งประกอบด้วยจำนวนของบางa's ตามด้วยจำนวนที่เท่ากันb' s ตัวอย่างของสตริงในภาษานี้มีab, ,aabbaaabbb

ภาษานี้สามารถแสดงให้เป็นที่ไม่ปกติโดยแทรกสูบน้ำ มันมีอยู่ในความเป็นจริงตามแบบฉบับภาษาบริบทฟรีซึ่งสามารถสร้างขึ้นโดยไวยากรณ์บริบทฟรี S → aSb | ab

อย่างไรก็ตามการติดตั้ง regex ในยุคปัจจุบันสามารถจดจำได้มากกว่าภาษาทั่วไปอย่างชัดเจน นั่นคือพวกเขาไม่ "ปกติ" ตามนิยามทฤษฎีภาษาที่เป็นทางการ PCRE และ Perl รองรับ regex แบบเรียกซ้ำและ. NET รองรับการกำหนดกลุ่มที่สมดุล คุณลักษณะที่ "แฟนซี" น้อยกว่าเช่นการจับคู่การอ้างอิงกลับหมายความว่านิพจน์ทั่วไปไม่ปกติ

แต่คุณลักษณะ "พื้นฐาน" นี้มีประสิทธิภาพเพียงใด? เราสามารถรับรู้Lด้วย Java regex ได้หรือไม่? เราอาจจะสามารถรวม lookarounds และการอ้างอิงที่ซ้อนกันและมีรูปแบบที่ทำงานร่วมกับเช่นString.matchesเพื่อให้ตรงกับสายเช่นab, aabb, aaabbbetc?

อ้างอิง

คำถามที่เชื่อมโยง


4
ซีรีส์นี้เริ่มต้นโดยได้รับอนุญาตจากบางคนในชุมชน ( meta.stackexchange.com/questions/62695/… ) หากการรับสัญญาณดีฉันวางแผนที่จะครอบคลุมคุณสมบัติขั้นสูงอื่น ๆ รวมถึงคุณสมบัติพื้นฐานอื่น ๆ ของ regex
polygenelubricants


ว้าวฉันไม่เคยรู้เลยว่า regexs ของ Java จะไม่ จำกัด เฉพาะนิพจน์ทั่วไป ฉันเดาว่านั่นอธิบายได้ว่าทำไมฉันถึงคิดมาตลอดว่าพวกเขาจะไม่นำไปใช้อย่างสมบูรณ์ สิ่งที่ฉันหมายถึงคือไม่มีส่วนเสริมความแตกต่างหรือตัวดำเนินการผลิตภัณฑ์ที่สร้างไว้ใน Java Regexs แต่ก็สมเหตุสมผลเนื่องจากไม่ได้ จำกัด เฉพาะภาษาปกติ
ลาน

คำถามนี้ถูกเพิ่มในคำถามที่พบบ่อยเกี่ยวกับนิพจน์ทั่วไปของStack Overflowภายใต้ "Advanced Regex-Fu"
aliteralmind

คำตอบ:


141

คำตอบคือไม่จำเป็นต้องพูดว่าใช่! แน่นอนที่สุดคุณสามารถเขียนรูปแบบ 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 = {
                "",      // 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*เห็นและแท้จริงคือ " +ลูปหลัก" ทั้งสองอนุญาตให้มีการย้อนรอย ขอแนะนำให้ผู้อ่านยืนยันว่าเหตุใดจึงไม่เป็นปัญหาในแง่ของความถูกต้องและเหตุใดการทำให้ทั้งสองเป็นเจ้าของในเวลาเดียวกันก็ใช้ได้ผลเช่นกัน (แม้ว่าการผสมตัวบ่งชี้การครอบครองที่บังคับและไม่บังคับในรูปแบบเดียวกันอาจทำให้เกิดความเข้าใจผิดได้)

นอกจากนี้ยังควรจะกล่าวว่าในขณะที่มันเรียบร้อยว่ามีรูปแบบการ regex ที่จะตรงกับnnนี้เป็นไปไม่ได้เสมอในการแก้ปัญหาที่ "ดีที่สุด" ในทางปฏิบัติ ทางออกที่ดีกว่ามากคือจับคู่จากนั้นเปรียบเทียบความยาวของสตริงที่จับโดยกลุ่ม 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 ไม่รองรับรูปแบบการเรียกซ้ำ


วัสดุโบนัสมากยิ่งขึ้น! จับคู่nnn !!

ดังนั้นเราจึงได้เห็นวิธีการเพื่อให้ตรงกับnnซึ่งเป็นที่ไม่ปกติ แต่ยังคงบริบทฟรี แต่สามารถเรายังตรงกับnnnซึ่งไม่ได้แม้บริบทฟรีหรือไม่

คำตอบคือแน่นอนใช่! ขอแนะนำให้ผู้อ่านลองแก้ปัญหานี้ด้วยตัวเอง แต่วิธีแก้ปัญหามีให้ด้านล่าง (พร้อมการใช้งานใน Java บน ideone.com )

^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $


ไม่ต้องสงสัยเลยว่าคำตอบยาว ๆ นี้อาจมีข้อผิดพลาด / พิมพ์ผิดดังนั้นโปรดแสดงความคิดเห็นเป็นความคิดเห็นเพื่อให้ฉันแก้ไขได้ด้วยตัวเอง
polygenelubricants

เยี่ยมมาก ฉันต้องใช้เวลาสักพักในการอ่าน แต่บรรทัดสุดท้ายนั้นไม่สามารถอ่านได้โดยทั่วไป มันเป็นแบบอักษรขนาดเล็ก ------ เดี๋ยวก่อน นั่นคือfeature? .... ไม่แน่ใจว่ามันเป็นความคิดที่ดี ฉันรู้ว่าสัญลักษณ์สุดท้ายคืออะไร แต่ไม่สามารถอ่านได้ (นอกเหนือจากการคัดลอกการวาง)
Peter Ajtai

6
@ ปีเตอร์: เน้นข้อความขนาดเล็กแล้วคัดลอกและวางลงในอย่างอื่น มันยากที่จะอ่านโดยตั้งใจ: มันเป็นสปอยเลอร์วิธีแก้ปริศนาโบนัส
polygenelubricants

8
+1: คำอธิบายที่ยอดเยี่ยม "บทความขั้นสูง" เหล่านี้เป็นแนวคิดที่ยอดเยี่ยม
Callum Rogers

1
@LarsH ของ PHP preg_match()เป็นตัวอย่างของPCRE regexes Java ดูเหมือนจะขึ้นอยู่กับรุ่นเก่าของ regexps ซึ่งหมายความว่า PHP regexes มีประสิทธิภาพมากกว่าเวอร์ชันใน Java ในฐานะของ2013/02/21 , pcre.txtกล่าวว่ามันสอดคล้องกับประมาณ 5.12 ขณะที่ Perl อยู่ที่ 5.16 โดยมี 5.18 ไม่กี่เดือน (จริงๆแล้วยังไม่มีการเพิ่ม regexes มากนักในเวลานั้น)
Brad Gilbert

20

เนื่องจากไม่มีการกล่าวถึง PCRE ที่รองรับรูปแบบการเรียกซ้ำฉันแค่ต้องการชี้ให้เห็นตัวอย่างที่ง่ายและมีประสิทธิภาพที่สุดของ PCRE ที่อธิบายภาษาที่เป็นปัญหา:

/^(a(?1)?b)$/

+1 ว้าวฉันไม่รู้ว่า PCRE รองรับรูปแบบการเรียกซ้ำ (ฉันยังเรียนอยู่! ทุกวัน!) ฉันได้แก้ไขบทความเพื่อรองรับข้อมูลนี้แล้ว ฉันไม่คิดว่ารูปแบบการเรียกซ้ำสามารถจับคู่a^n b^n c^nได้
polygenelubricants

ควรสังเกตว่าตัวเลือกนี้ง่ายกว่า แต่ไม่ดีเท่าคำตอบที่โพสต์ - การเรียกซ้ำจะมากเกินไปในสตริงที่ยาว
Kobi

@ โกบีอันนี้ขึ้นอยู่กับคำจำกัดความของคำว่า "ดี" ของคุณ ตัวอย่างเช่นโซลูชันแบบวนซ้ำจะเร็วกว่าคำสั่งอื่น ๆ ประมาณหนึ่ง ( codepad.viper-7.com/CWgy7c ) และเข้าใจง่ายกว่ามาก วิธีแก้ปัญหาแบบวนซ้ำเป็นการแปลงไวยากรณ์โดยตรงให้เป็น regex (จริงๆแล้วคุณสามารถเขียนในรูปแบบไวยากรณ์ได้ก็จะได้ผล)
NikiC

1
@polygeniclubricants คุณสามารถจับคู่รูปแบบนั้นกับรูปแบบการเรียกซ้ำสองรูปแบบหนึ่งจะใช้as และbs โดยไม่ต้องจับภาพ (และตรวจสอบว่ามีจำนวนเท่ากันโดยมีการเรียกซ้ำ) ตามด้วย regex ที่จับซึ่งกิน a ทั้งหมดอย่างละโมบจากนั้นใช้การเรียกซ้ำ รูปแบบที่จะใช้และตรวจสอบว่ามีจำนวนbs และcs เท่ากัน regex คือ: /^(?=(a(?-1)?b)c)a+(b(?-1)?c)$/x. เครดิตถึง: nikic.github.io/2012/06/15/…
Josh

11

ดังที่ได้กล่าวไว้ในคำถาม - ด้วยกลุ่มการปรับสมดุล. NET รูปแบบของประเภทa n b n c n d n … z nสามารถจับคู่ได้อย่างง่ายดายเช่น

^
  (?<A>a)+
  (?<B-A>b)+  (?(A)(?!))
  (?<C-B>c)+  (?(B)(?!))
  ...
  (?<Z-Y>z)+  (?(Y)(?!))
$

ตัวอย่างเช่นhttp://www.ideone.com/usuOE


แก้ไข:

นอกจากนี้ยังมีรูปแบบ PCRE สำหรับภาษาทั่วไปที่มีรูปแบบการเรียกซ้ำ แต่จำเป็นต้องมีผู้ค้นหา ฉันไม่คิดว่านี่เป็นการแปลโดยตรงจากข้างต้น

^
  (?=(a(?-1)?b))  a+
  (?=(b(?-1)?c))  b+
  ...
  (?=(x(?-1)?y))  x+
     (y(?-1)?z)
$

ตัวอย่างเช่นhttp://www.ideone.com/9gUwF


1
@poly: ขอบคุณ :) อันที่จริงฉันไม่คุ้นเคยกับรูปแบบ. NET แต่สำหรับรูปแบบประเภทนี้มันกลายเป็นเรื่องง่ายมากกับการจัดกลุ่มดังนั้นฉันจึงเสริมคำตอบนี้
kennytm

คุณสามารถใช้รูปแบบการเรียกซ้ำได้หรือไม่? เพราะถ้าคุณทำไม่ได้นั่นเป็นสิ่งที่น่าสนใจที่กลุ่มการปรับสมดุลสามารถทำสิ่งที่รูปแบบซ้ำ ๆ ไม่สามารถทำได้ (และใช่ฉันชอบอาหารเสริมมาก)
polygenelubricants

อย่างไรก็ตามสาเหตุที่ฉันไม่ใส่โซลูชัน. NET เป็นเพราะฉันมีแผนสำหรับ "เราจะจับคู่a^n b^nกับ. NET regex ได้อย่างไร" บทความในอนาคต แต่คุณยินดีที่จะเขียนมันหากคุณต้องการ ฉันไม่ได้ทำบทความเหล่านี้เพื่อตัวเองเท่านั้น ฉันต้องการสนับสนุนให้ผู้อื่นทำเช่นกันเพื่อให้มีเนื้อหาที่ดีบนไซต์
polygenelubricants

โปรดอัปเดตหากคุณทราบวิธีดำเนินการด้วยรูปแบบการเรียกซ้ำ ฉันเล่นกับกลุ่มสมดุลเพื่อจับคำที่มีความยาวทำให้เป็นอนุกรมฟีโบนักชี แต่ไม่สามารถใช้งานได้ อาจเป็นไปได้โดยใช้การมองไปรอบ ๆ คล้ายกับสิ่งที่ฉันทำ
Kobi

1
ฉันแค่อยากจะชี้ให้เห็นว่าเวอร์ชัน PCRE ของรูปแบบนี้มีข้อบกพร่องเล็กน้อยเนื่องจากตรงกับว่าอักขระตัวถัดไปยาวกว่าก่อนหน้านี้หรือไม่ ดูที่นี่: regex101.com/r/sdlRTm/1คุณจำเป็นต้องเพิ่ม(?!b), (?!c)ฯลฯ หลังจากที่กลุ่มจับชอบโดย: regex101.com/r/sdlRTm/2
jaytea
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.