จะตรวจสอบได้อย่างไรว่าตัวเลขเป็นจำนวนเฉพาะกับ regex หรือไม่?


128

ฉันพบตัวอย่างโค้ดต่อไปนี้สำหรับ Java บนRosettaCode :

public static boolean prime(int n) {
  return !new String(new char[n]).matches(".?|(..+?)\\1+");
}
  • ฉันไม่รู้จัก Java เป็นพิเศษ แต่เข้าใจทุกแง่มุมของข้อมูลโค้ดนี้ยกเว้น regex เอง
  • ฉันมีความรู้พื้นฐานถึงขั้นสูงเกี่ยวกับ Regex ตามที่คุณพบในฟังก์ชัน PHP ในตัว

วิธีการที่ไม่.?|(..+?)\\1+ตรงกับตัวเลขที่สำคัญ?


9
@Amir Rachum: !new String(new char[n]).matches(".?|(..+?)\\1+")เทียบเท่ากับ!((new String(new char[n])).matches(".?|(..+?)\\1+")).
Gumbo

14
สิ่งนี้ไม่เพียง แต่มีราคาแพงในการคำนวณเท่านั้น แต่ยังอาจทำให้หน่วยความจำมีราคาแพงอย่างร้ายแรง หากใครเลือกใช้แนวทางนี้ซึ่งฉันขอแนะนำเนื่องจากอัลกอริทึมในการค้นหาไพรม์นั้นง่ายมาก (ทำไมในโลกนี้ซับซ้อนและทำให้สิ้นเปลืองมาก) ควรทำการตรวจสอบก่อนที่จะมี "ถ่านใหม่ [n ] "เพื่อให้แน่ใจว่าต่ำกว่าเกณฑ์ที่เหมาะสม เช่นเรียก "prime (Integer.MAX_VALUE)" แล้วส่งข้อบกพร่องเมื่อมันพ่น OutOfMemoryError
nicerobot

28
@nicerobot: สว่างขึ้น?
แคม

6
@nicerobot: ที่จริงฉันเอาคืน เดิมทีฉันคิดว่าลักษณะทางวิชาการของคำถามนี้บอกเป็นนัยว่าใช้เพื่อจุดประสงค์ในการเรียนรู้เท่านั้นและคุณเป็นคนที่น่ารังเกียจ อย่างไรก็ตามในความคิดที่สองนั่นไม่ใช่กรณี ไม่เคยกล่าวถึงหรือบอกเป็นนัยในคำถามที่ว่า regex มีไว้เพื่อการเรียนรู้เท่านั้น อันที่จริงความประทับใจแรกของฉันคือมันดูเรียบง่ายมากพอ ๆ กับข้อมูลโค้ดดังนั้นผู้เริ่มต้นอาจคิดว่ามันสามารถใช้ในทางปฏิบัติได้ +1
แคม

7
@incrediman ไม่ต้องกังวล ฉันเห็นว่าคุณคิดอย่างนั้นได้อย่างไร เป็นเพียงความตั้งใจของฉันที่จะเตือนถึงผลที่ตามมาของการใช้สิ่งนี้ไม่ใช่เพื่อกีดกันการเรียนรู้วิธีการทำงาน ง่ายๆ "โปรดอย่าใช้สิ่งนี้" ก่อนความคิดเห็นที่เหลือของฉันอาจทำให้ความคิดเห็นน้อยลงจากมุมมองแรกของคุณ
nicerobot

คำตอบ:


120

คุณบอกว่าคุณเข้าใจส่วนนี้ แต่เพื่อเน้นว่า String ที่สร้างขึ้นมีความยาวเท่ากับจำนวนที่ให้มา ดังนั้นสตริงจึงมีสามอักขระถ้าและเฉพาะในกรณีn == 3นี้

.?

ส่วนแรกของ regex กล่าวว่า "อักขระใด ๆ ศูนย์หรือหนึ่งครั้ง" ดังนั้นโดยทั่วไปจะมีศูนย์หรือหนึ่ง character-- n == 0 || n == 1หรือต่อสิ่งที่ผมกล่าวถึงข้างต้น หากเรามีการจับคู่ให้ส่งคืนการปฏิเสธของสิ่งนั้น สิ่งนี้สอดคล้องกับความจริงที่ว่าศูนย์และหนึ่งไม่ใช่ไพรม์

(..+?)\\1+

ส่วนที่สองของ regex นั้นยุ่งยากกว่าเล็กน้อยโดยอาศัยกลุ่มและการอ้างอิงย้อนกลับ กลุ่มคืออะไรก็ได้ในวงเล็บซึ่งจะถูกจับและจัดเก็บโดยเอนจิน regex เพื่อใช้ในภายหลัง backreference คือกลุ่มที่ตรงกันซึ่งใช้ในภายหลังใน regex เดียวกัน

กลุ่มประกอบด้วยอักขระ 1 ตัวจากนั้น 1 ตัวขึ้นไปของอักขระใด ๆ (อักขระ + หมายถึงหนึ่งตัวขึ้นไป แต่เป็นอักขระหรือกลุ่มก่อนหน้าเท่านั้นดังนั้นนี่จึงไม่ใช่ "อักขระสองหรือสี่หรือหกตัวเป็นต้น" แต่เป็น "สองหรือสามเป็นต้น" เครื่องหมาย +? ก็เหมือนกับ + แต่ มันจะพยายามจับคู่อักขระให้น้อยที่สุด + โดยปกติจะพยายามฮุบสตริงทั้งหมดหากทำได้ซึ่งไม่ดีในกรณีนี้เนื่องจากป้องกันไม่ให้ส่วน backreference ทำงาน)

ส่วนถัดไปคือ backreference: อักขระชุดเดียวกันนั้น (สองตัวขึ้นไป) ปรากฏขึ้นอีกครั้ง กล่าวว่า backreference ปรากฏขึ้นอย่างน้อยหนึ่งครั้ง

ดังนั้น. กลุ่มที่จับได้จะสอดคล้องกับจำนวนอักขระตามธรรมชาติ (ตั้งแต่ 2 ตัวขึ้นไป) ที่จับได้ จากนั้นกลุ่มดังกล่าวจะปรากฏขึ้นตามจำนวนครั้งตามธรรมชาติ (ตั้งแต่ 2 เป็นต้นไป) หากมีการจับคู่หมายความว่าเป็นไปได้ที่จะหาผลคูณของตัวเลขสองจำนวนที่มากกว่าหรือเท่ากับ 2 ที่ตรงกับสตริงความยาว n ... หมายความว่าคุณมี n คอมโพสิต ดังนั้นอีกครั้งให้คืนค่าการปฏิเสธของการจับคู่ที่ประสบความสำเร็จ: n ไม่ใช่ไพรม์

หากไม่พบการจับคู่คุณจะไม่สามารถหาผลคูณของจำนวนธรรมชาติสองจำนวนที่มากกว่าหรือเท่ากับ 2 ได้ ... และคุณมีทั้งไม่ตรงกันและจำนวนเฉพาะดังนั้นการกลับมาของการปฏิเสธอีกครั้ง ของผลการแข่งขัน

คุณเห็นมันตอนนี้หรือไม่? มันเป็นเรื่องยุ่งยากอย่างไม่น่าเชื่อ (และมีราคาแพงในการคำนวณ!) แต่ก็เป็นเรื่องง่ายในเวลาเดียวกันเมื่อคุณได้รับมัน :-)

ฉันสามารถอธิบายได้อย่างละเอียดหากคุณมีคำถามเพิ่มเติมเช่นการแยกวิเคราะห์นิพจน์ทั่วไปทำงานอย่างไร แต่ตอนนี้ฉันพยายามทำให้คำตอบนี้เรียบง่าย (หรือง่ายที่สุดเท่าที่จะทำได้)


10
ฉันลองใช้ตรรกะนี้กับ JS ในคอนโซล Chrome dev บนหน้าเว็บ และเพิ่งผ่านไป 5 เพื่อตรวจสอบ เพจพัง!
Amogh Talpallikar

ความคิดเห็นด้านล่างให้คำอธิบายที่ดีกว่า โปรดอ่านก่อนดำเนินการต่อ!
Ivan Davidov

"ดีกว่า" เป็นเรื่องส่วนตัว - ฉันจะบอกว่ามันเข้าใกล้ปัญหาจากมุมที่แตกต่างกันและเป็นส่วนเติมเต็มที่ยอดเยี่ยมสำหรับคำตอบนี้ :-)
Platinum Azure

1
ฉันเขียนบล็อกโพสต์เพื่ออธิบายสิ่งนี้โดยมีรายละเอียดเพิ่มเติม: ทำให้เข้าใจผิดนิพจน์ทั่วไปที่ตรวจสอบว่าตัวเลขเป็นไพรม์หรือไม่
Illya Gerasymchuk

73

ผมจะอธิบายนอก regex ส่วนหนึ่งของการทดสอบ primality ที่: regex ต่อไปนี้ได้รับString sซึ่งประกอบด้วยการทำซ้ำString t, tพบ

    System.out.println(
        "MamamiaMamamiaMamamia".replaceAll("^(.*)\\1+$", "$1")
    ); // prints "Mamamia"

วิธีการทำงานคือ regex จับ(.*)เข้า\1และดูว่ามี\1+ตามมาหรือไม่ การใช้^และ$ทำให้แน่ใจว่าการจับคู่ต้องเป็นสตริงทั้งหมด

ดังนั้นในทางหนึ่งเราได้รับString sซึ่งเป็น "หลาย" ของString tและ regex จะพบสิ่งนั้นt(ยาวที่สุดเท่าที่จะเป็นไปได้เนื่องจาก\1เป็นความโลภ)

เมื่อคุณเข้าใจแล้วว่าเหตุใด regex จึงทำงานได้ (ละเว้นทางเลือกแรกใน regex ของ OP ในตอนนี้) การอธิบายวิธีใช้สำหรับการทดสอบเบื้องต้นนั้นง่ายมาก

  • ในการทดสอบความnเป็นอันดับแรกให้สร้างStringความยาวก่อนn(เติมด้วยค่าเดียวกันchar)
  • นิพจน์ทั่วไปรวบรวมStringความยาวบางส่วน (พูดk) เข้ามา\1และพยายามจับคู่\1+กับส่วนที่เหลือของString
    • หากมีการจับคู่แสดงว่าnเป็นผลคูณที่เหมาะสมkดังนั้นจึงnไม่เป็นค่าเฉพาะ
    • ถ้าไม่ตรงกันแสดงว่าไม่มีสิ่งนั้นkที่หารnด้วยnเหตุนี้จึงเป็นไพรม์

วิธีการที่ไม่.?|(..+?)\1+ตรงกับตัวเลขที่สำคัญ?

จริงๆแล้วมันไม่! มันตรงกับ Stringมีความยาวไม่สำคัญ!

  • .?: ส่วนแรกของการจับคู่แบบสลับStringของความยาว0หรือ1(ไม่ใช่เฉพาะตามคำจำกัดความ)
  • (..+?)\1+: ส่วนที่สองของการสลับซึ่งเป็นรูปแบบของนิพจน์ทั่วไปที่อธิบายไว้ข้างต้นการจับคู่Stringของความยาวnที่เป็น "ตัวคูณ" Stringของความยาวk >= 2(กล่าวnคือเป็นส่วนประกอบไม่ใช่ไพรม์)
    • โปรดทราบว่า?จริง ๆ แล้วตัวปรับแต่งแบบไม่เต็มใจไม่จำเป็นสำหรับความถูกต้อง แต่อาจช่วยเร่งกระบวนการโดยพยายามให้เล็กลงkก่อน

สังเกตตัว! booleanดำเนินการส่วนเสริมในreturnคำสั่ง: มันลบล้างmatches. เมื่อ regex ไม่ตรงกันnเป็นสิ่งสำคัญ! มันเป็นตรรกะเชิงลบสองเท่าจึงไม่น่าแปลกใจที่มันสับสน !!


การทำให้เข้าใจง่าย

นี่คือการเขียนโค้ดใหม่ง่ายๆเพื่อให้อ่านง่ายขึ้น:

public static boolean isPrime(int n) {
    String lengthN = new String(new char[n]);
    boolean isNotPrimeN = lengthN.matches(".?|(..+?)\\1+");
    return !isNotPrimeN;
}

ข้างต้นนั้นเหมือนกับโค้ด Java ดั้งเดิม แต่แยกออกเป็นหลายคำสั่งโดยมีการกำหนดตัวแปรโลคัลเพื่อให้เข้าใจตรรกะได้ง่ายขึ้น

นอกจากนี้เรายังสามารถทำให้ regex ง่ายขึ้นโดยใช้การทำซ้ำแบบ จำกัด ดังนี้:

boolean isNotPrimeN = lengthN.matches(".{0,1}|(.{2,})\\1+");

อีกครั้งที่ได้รับStringความยาวnเต็มไปด้วยเหมือนกันchar,

  • .{0,1}ตรวจสอบว่าn = 0,1ไม่ใช่เฉพาะ
  • (.{2,})\1+ตรวจสอบว่าnเป็นผลคูณที่เหมาะสมของk >= 2ไม่ใช่เฉพาะหรือไม่

ด้วยข้อยกเว้นของการปรับปรุงลังเล?บน\1(ละเว้นเพื่อความชัดเจน) ที่ regex ข้างต้นเป็นเหมือนเดิม


regex ที่สนุกยิ่งขึ้น

regex ต่อไปนี้ใช้เทคนิคที่คล้ายกัน ควรมีการศึกษา:

System.out.println(
    "OhMyGod=MyMyMyOhGodOhGodOhGod"
        .replaceAll("^(.+)(.+)(.+)=(\\1|\\2|\\3)+$", "$1! $2! $3!")
); // prints "Oh! My! God!"

ดูสิ่งนี้ด้วย


6
+1: ฉันคิดว่าแนวทางของคุณน่าจะดีกว่าของฉัน ไม่รู้ว่าทำไมฉันถึงได้รับคะแนนโหวตหรือเครื่องหมายถูกมากมาย ... คุณสมควรได้รับมากกว่านี้ฉันคิดว่า :-( ขออภัย
Platinum Azure

@ แพลตตินั่ม: ว้าวฉันไม่เคยคิดว่าคุณจะพูดแบบนั้นต่อสาธารณะ! ขอบคุณสำหรับการสนับสนุน. บางทีฉันอาจจะได้รับ[Populist]สักวันจากนี้
polygenelubricants

2
มันเป็นเพียงความจริง (อย่างที่ฉันรับรู้) ... ไม่ใช่เรื่องใหญ่จริงๆ ฉันไม่ได้มาที่นี่เพื่อรับตัวแทน (แม้ว่ามันจะเป็นโบนัสและเซอร์ไพรส์ที่น่ายินดีเสมอ) ... ฉันมาที่นี่เพื่อพยายามตอบคำถามเมื่อทำได้ ดังนั้นจึงไม่น่าแปลกใจเลยที่ฉันสามารถยอมรับได้เมื่อมีคนทำได้ดีกว่าที่ฉันมีในคำถามหนึ่ง ๆ
Platinum Azure

25

เคล็ดลับ regex ที่ดี (แม้ว่าจะไม่มีประสิทธิภาพมาก) ... :)

regex กำหนด non-primes ดังนี้:

N ไม่ใช่ไพรม์ถ้า N <= 1 OR N หารด้วย K> 1 บางตัวหารไม่ได้

แทนที่จะส่งการแทนค่าแบบดิจิทัลแบบธรรมดาของ N ไปยังเอนจิน regex มันจะถูกป้อนด้วยลำดับความยาว N ซึ่งประกอบด้วยอักขระที่ซ้ำกัน ส่วนแรกของการแยกจะตรวจสอบ N = 0 หรือ N = 1 และส่วนที่สองค้นหาตัวหาร K> 1 โดยใช้การอ้างอิงย้อนกลับ มันบังคับให้เอนจิน regex ค้นหาลำดับย่อยที่ไม่ว่างเปล่าซึ่งสามารถทำซ้ำได้อย่างน้อยสองครั้งเพื่อสร้างลำดับ หากมีลำดับต่อมาหมายความว่าความยาวของมันหาร N ดังนั้น N จึงไม่เป็นไพรม์


2
ผิดปกติพอสมควรแม้ว่าหลังจากอ่านคำอธิบายทางเทคนิคอื่น ๆ ที่ยาวและยาวกว่านี้ซ้ำแล้วซ้ำเล่าฉันก็พบว่าคำอธิบายนี้เป็นคำอธิบายที่ทำให้ 'คลิก' อยู่ในหัวของฉัน
Eight-Bit Guru

2
/^1?$|^(11+?)\1+$/

ใช้กับตัวเลขหลังการแปลงเป็นฐาน 1 (1 = 1, 2 = 11, 3 = 111, ... ) รอบนอกจะตรงกับสิ่งนี้ ถ้าไม่ตรงกันก็เป็นไพรม์

คำอธิบายที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.