Come determinare se un numero è un numero primo con regex?


128

Ho trovato il seguente esempio di codice per Java su RosettaCode :

public static boolean prime(int n) {
  return !new String(new char[n]).matches(".?|(..+?)\\1+");
}
  • Non conosco Java in particolare, ma comprendo tutti gli aspetti di questo frammento tranne la regex stessa
  • Ho una conoscenza di base di base-avanzata di Regex come la trovi nelle funzioni PHP integrate

In che modo .?|(..+?)\\1+corrispondono i numeri primi?


9
@Amir Rachum: !new String(new char[n]).matches(".?|(..+?)\\1+")è equivalente a !((new String(new char[n])).matches(".?|(..+?)\\1+")).
Gumbo,

14
Questo non è solo costoso dal punto di vista computazionale, ma è anche potenzialmente devastante in termini di memoria. Se qualcuno scegliesse di utilizzare questo approccio, che sconsigliai poiché l'algoritmo per trovare i numeri primi è così semplice (perché nel mondo lo complica e lo rende così dispendioso), un controllo dovrebbe essere condotto prima del "nuovo carattere [n ] "per assicurarsi che sia al di sotto di una soglia ragionevole. Ad esempio, chiamare "prime (Integer.MAX_VALUE)" e quindi presentare un bug quando genera OutOfMemoryError.
Nicerobot,

28
@nicerobot: alleggerire?
Cam

6
@nicerobot: in realtà, lo riprendo. Inizialmente pensavo che la natura accademica di questa domanda implicava il suo uso solo a fini di apprendimento e che tu fossi una seccona odiosa. Comunque a pensarci bene non è così; non è mai stato menzionato o implicito nella domanda che la regex è solo a scopo di apprendimento. In effetti la mia prima impressione è che abbia un aspetto molto semplice per quanto riguarda gli snippet di codice, quindi un principiante potrebbe davvero supporre che possa essere utilizzato in pratica. +1.
Cam

7
@incrediman non preoccuparti. Vedo come potresti pensarlo. Era solo mia intenzione avvertire delle conseguenze dell'uso di questo, non scoraggiare l'apprendimento di come funziona. Un semplice "Per favore non distribuire questo." prima del resto del mio commento avrebbe potuto rendere il suono meno condiscendente dalla tua prospettiva iniziale.
Nicerobot,

Risposte:


120

Hai detto di comprendere questa parte, ma solo per enfatizzare, la stringa generata ha una lunghezza pari al numero fornito. Quindi la stringa ha tre caratteri se e solo se n == 3.

.?

La prima parte del regex dice "qualsiasi personaggio, zero o uno volte". Quindi, in pratica, c'è zero o uno character-- o, per quello che ho citato sopra, n == 0 || n == 1. Se abbiamo la corrispondenza, restituiamo la negazione di quella. Ciò corrisponde al fatto che zero e uno NON sono primi.

(..+?)\\1+

La seconda parte della regex è un po 'più complicata, basandosi su gruppi e riferimenti secondari. Un gruppo è qualsiasi cosa tra parentesi, che verrà quindi catturato e memorizzato dal motore regex per un uso successivo. Un backreference è un gruppo corrispondente che verrà utilizzato successivamente nella stessa regex.

Il gruppo cattura 1 carattere, quindi 1 o più di qualsiasi personaggio. (Il carattere + significa uno o più, ma SOLO del carattere o gruppo precedente. Quindi questo non è "due o quattro o sei caratteri ecc.", Ma piuttosto "due o tre ecc." Il +? È come +, ma cerca di far corrispondere il minor numero possibile di caratteri. + normalmente cerca di inghiottire l'intera stringa se possibile, il che è male in questo caso perché impedisce alla parte di backreference di funzionare.)

La parte successiva è il backreference: quello stesso set di caratteri (due o più), che appare di nuovo. Detto backreference appare una o più volte.

Così. Il gruppo catturato corrisponde a un numero naturale di caratteri (da 2 in poi) catturati. Detto gruppo appare quindi un numero naturale di volte (anche da 2 in poi). Se esiste una corrispondenza, ciò implica che è possibile trovare un prodotto con due numeri maggiori o uguali a 2 che corrispondono alla stringa di lunghezza n ... il che significa che hai un composto n. Quindi, restituisci la negazione della partita corretta: n NON è primo.

Se non è possibile trovare una corrispondenza, non è possibile trovare un prodotto con due numeri naturali maggiori o uguali a 2 ... e si ha sia una non corrispondenza che un numero primo, quindi di nuovo il ritorno della negazione del risultato della partita.

lo vedi adesso? È incredibilmente complicato (e computazionalmente costoso!) Ma è anche un po 'semplice allo stesso tempo, una volta ottenuto. :-)

Posso chiarire se hai ulteriori domande, ad esempio su come funziona effettivamente l'analisi regex. Ma sto cercando di mantenere questa risposta semplice per ora (o più semplice che possa mai essere).


10
Ho provato questa logica con JS nella console di sviluppo di Chrome. sulla pagina web. e appena passato 5 per controllare. La pagina si è bloccata!
Amogh Talpallikar,

Il commento qui sotto fornisce una spiegazione migliore. Si prega di leggere prima di andare avanti!
Ivan Davidov,

"Meglio" è soggettivo, direi che affronta il problema da una prospettiva diversa ed è un complemento meraviglioso a questa risposta. :-)
Platinum Azure,

1
In realtà ho scritto un post sul blog spiegando questo con maggiori dettagli: Demistificare l'espressione regolare che controlla se un numero è primo .
Illya Gerasymchuk,

73

Spiegherò la parte regex al di fuori del test di primalità: la seguente regex, data una String sche consiste nel ripetere String t, trova t.

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

Il modo in cui funziona è che la cattura regex (.*)in \1, e poi vede se c'è \1+lo seguono. Utilizzando il ^e$ garantisce che una corrispondenza debba essere dell'intera stringa.

Quindi, in un certo senso, ci viene dato String s, che è un "multiplo" di String t, e il regex lo troverà t(il più lungo possibile, poiché \1è avido).

Una volta compreso il motivo per cui questo regex funziona, allora (ignorare il primo alternato nel regex di OP per ora) spiegare come viene utilizzato per il test di primalità è semplice.

  • Per testare la primalità di n, prima genera una Stringlunghezza n(riempita con la stessachar )
  • Il regex cattura un Stringpo 'di lunghezza (diciamo k) in \1, e cerca di abbinarsi \1+al resto delString
    • Se c'è una corrispondenza, allora nè un multiplo corretto di k, e quindi nnon è primo.
    • Se non c'è corrispondenza, allora non kesiste tale che divide n, ed nè quindi un numero primo

In che modo .?|(..+?)\1+corrispondono i numeri primi?

In realtà no! Si abbina String la cui lunghezza non è primo!

  • .?: La prima parte delle alternanze corrisponde Stringdi lunghezza 0o 1(NON primo per definizione)
  • (..+?)\1+: La seconda parte dell'alternanza, una variazione della regex spiegata sopra, corrispondenze Stringdi lunghezza nche è "un multiplo" di una Stringlunghezza k >= 2(cioèn è un composto, NON un numero primo).
    • Si noti che il modificatore riluttante ?non è in realtà necessario per la correttezza, ma può aiutare ad accelerare il processo provando kprima a ridurre le dimensioni

Nota l' ! booleanoperatore complemento returnnell'istruzione: annulla il matches. È quando il regex NON corrisponde, nè primo! È una logica a doppio negativo, quindi non c'è da meravigliarsi che sia un po 'confuso !!


Semplificazione

Ecco una semplice riscrittura del codice per renderlo più leggibile:

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

Quanto sopra è essenzialmente uguale al codice Java originale, ma suddiviso in più istruzioni con assegnazioni a variabili locali per facilitare la comprensione della logica.

Possiamo anche semplificare la regex, usando la ripetizione finita, come segue:

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

Ancora una volta, data una Stringlunghezza n, riempito con lo stesso char,

  • .{0,1}controlla se n = 0,1, NON primo
  • (.{2,})\1+controlla se nè un multiplo corretto di k >= 2, NON primo

Con l'eccezione del modificatore riluttante ?su \1(omesso per chiarezza), la regex sopra è identica all'originale.


Più regex divertente

Il regex seguente usa una tecnica simile; dovrebbe essere educativo:

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

Guarda anche


6
+1: Penso che il tuo approccio sia probabilmente migliore del mio. Non ho idea del perché ho ricevuto così tanti voti o il segno di spunta ... te lo meriti di più, credo. :-( Siamo spiacenti
Platinum Azure

@ Platinum: Wow, non avrei mai pensato che avresti fatto un giro dicendo questo pubblicamente! Grazie per il supporto. Forse avrò un [Populist]giorno da questo.
poligenelubrificanti

2
Bene, è solo la verità (come la percepisco io) ... non è un grosso problema davvero. Non sono qui per un rappresentante (anche se è sempre un bonus e una piacevole sorpresa) ... Sono qui per cercare di rispondere alle domande quando posso. Quindi non dovrebbe sorprendere il fatto che io possa ammettere quando qualcuno ha fatto meglio di quanto non abbia fatto in una domanda particolare.
Platinum Azure

25

Bel trucco regex (anche se molto inefficiente) ... :)

Il regex definisce i non primi come segue:

N non è primo se e solo se N <= 1 OR N è divisibile per alcuni K> 1.

Invece di passare la semplice rappresentazione digitale di N al motore regex, viene alimentato con una sequenza di lunghezza N, composta da un carattere ripetuto. La prima parte della disgiunzione controlla N = 0 o N = 1 e la seconda cerca un divisore K> 1, usando i riferimenti indietro. Obbliga il motore regex a trovare alcune sotto-sequenze non vuote che possono essere ripetute almeno due volte per formare la sequenza. Se esiste una tale sottosequenza, significa che la sua lunghezza divide N, quindi N non è primo.


2
Stranamente, anche dopo aver letto ripetutamente le altre spiegazioni più lunghe e più tecniche, ho trovato questa spiegazione come quella che mi ha fatto "fare clic" nella mia testa.
Guru a

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

Applica ai numeri dopo la conversione in base 1 (1 = 1, 2 = 11, 3 = 111, ...). I non primi corrisponderanno a questo. Se non corrisponde, è primo.

Spiegazione qui .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.