Comment déterminer si un nombre est un nombre premier avec regex?

128

J'ai trouvé l'exemple de code suivant pour Java sur RosettaCode :

public static boolean prime(int n) {
  return !new String(new char[n]).matches(".?|(..+?)\\1+");
}
  • Je ne connais pas Java en particulier mais je comprends tous les aspects de cet extrait de code à l'exception de l'expression régulière elle-même
  • J'ai des connaissances de base à avancées de Regex telles que vous les trouvez dans les fonctions PHP intégrées

Comment faire .?|(..+?)\\1+correspondre les nombres premiers?

kitlite
la source
9
@Amir Rachum: !new String(new char[n]).matches(".?|(..+?)\\1+")équivaut à !((new String(new char[n])).matches(".?|(..+?)\\1+")).
Gumbo
14
Ce n'est pas seulement coûteux en calcul, mais c'est aussi potentiellement très coûteux en mémoire. Si quelqu'un choisit d'utiliser cette approche, ce que je déconseillerais puisque l'algorithme de recherche de nombres premiers est si simple (pourquoi dans le monde le complique-t-il et le rend-il si gaspilleur), une vérification doit être effectuée avant le "new char [n ] "pour s'assurer qu'il est inférieur à un seuil raisonnable. Par exemple, appelez "prime (Integer.MAX_VALUE)", puis déposez un bogue lorsqu'il lance OutOfMemoryError.
nicerobot
28
@nicerobot: Allégez-vous?
Cam du
6
@nicerobot: en fait, je reprends ça. Je pensais à l'origine que la nature académique de cette question impliquait son utilisation uniquement à des fins d'apprentissage, et que vous étiez une chatte désagréable. Cependant, après réflexion, ce n'est pas le cas; il n'est jamais mentionné ni même implicite dans la question que l'expression régulière est uniquement à des fins d'apprentissage. En fait, ma première impression est que c'est très simple en ce qui concerne les extraits de code, donc un débutant pourrait en effet supposer qu'il pourrait être utilisé dans la pratique. +1.
Cam du
7
@incrediman pas de soucis. Je peux voir comment vous pourriez penser cela. C'était seulement mon intention de mettre en garde contre les conséquences de son utilisation, et non de décourager d'apprendre comment cela fonctionne. Un simple "Veuillez ne pas déployer ceci." avant le reste de mon commentaire, cela aurait pu le rendre moins condescendant de votre point de vue initial.
nicerobot

Réponses:

120

Vous avez dit que vous comprenez cette partie, mais juste pour souligner, la chaîne générée a une longueur égale au nombre fourni. La chaîne comporte donc trois caractères si et seulement si n == 3.

.?

La première partie de l'expression régulière dit "n'importe quel caractère, zéro ou une fois". Donc , fondamentalement, est là zéro ou un character-- ou, par ce que je l' ai mentionné ci - dessus, n == 0 || n == 1. Si nous avons la correspondance, retournez la négation de cela. Cela correspond au fait que zéro et un ne sont PAS premiers.

(..+?)\\1+

La deuxième partie de l'expression régulière est un peu plus délicate, s'appuyant sur des groupes et des références arrière. Un groupe est tout ce qui est entre parenthèses, qui sera ensuite capturé et stocké par le moteur regex pour une utilisation ultérieure. Une référence arrière est un groupe apparié qui est utilisé plus tard dans la même expression régulière.

Le groupe capture 1 personnage, puis 1 ou plus de n'importe quel personnage. (Le caractère + signifie un ou plusieurs, mais UNIQUEMENT du caractère ou du groupe précédent. Donc ce n'est pas "deux ou quatre ou six caractères etc.", mais plutôt "deux ou trois etc." Le +? Est comme +, mais il essaie de faire correspondre le moins de caractères possible. + essaie normalement d'avaler toute la chaîne s'il le peut, ce qui est mauvais dans ce cas car cela empêche la partie de référence de fonctionner.)

La partie suivante est la référence arrière: ce même jeu de caractères (deux ou plus), qui apparaît à nouveau. Ladite référence arrière apparaît une ou plusieurs fois.

Alors. Le groupe capturé correspond à un nombre naturel de caractères (à partir de 2) capturés. Ledit groupe apparaît alors un certain nombre naturel de fois (également à partir de 2). S'il y a une correspondance, cela implique qu'il est possible de trouver un produit de deux nombres supérieurs ou égaux à 2 qui correspondent à la chaîne de longueur n ... ce qui signifie que vous avez un n composé. Encore une fois, retournez la négation de la correspondance réussie: n n'est PAS premier.

Si aucune correspondance ne peut être trouvée, alors vous ne pouvez pas trouver votre produit de deux nombres naturels supérieurs ou égaux à 2 ... et vous avez à la fois une non-correspondance et un nombre premier, d'où à nouveau le retour de la négation du résultat du match.

Le voyez-vous maintenant? C'est incroyablement délicat (et coûteux en calcul!) Mais c'est aussi assez simple en même temps, une fois que vous l'avez compris. :-)

Je peux élaborer si vous avez d'autres questions, comme sur le fonctionnement réel de l'analyse des regex. Mais j'essaie de garder cette réponse simple pour le moment (ou aussi simple que possible).

Platine Azure
la source
10
J'ai essayé cette logique avec JS dans la console de développement Chrome. sur la page Web. et vient de passer 5 pour vérifier. La page s'est plantée!
Amogh Talpallikar
Le commentaire ci-dessous donne une meilleure explication. Veuillez le lire avant de continuer!
Ivan Davidov du
«Mieux» est subjectif - je dirais qu'il aborde le problème sous un angle différent et est un merveilleux complément à cette réponse. :-)
Platinum Azure du
1
En fait, j'ai écrit un article de blog expliquant cela avec plus de détails: Démystifier l'expression régulière qui vérifie si un nombre est premier .
Illya Gerasymchuk
73

J'expliquerai la partie regex en dehors des tests de primalité: la regex suivante, étant donné une String squi consiste à répéter String t, trouve t.

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

La façon dont cela fonctionne est que l'expression régulière capture (.*)dans \1, puis voit s'il y a une \1+suite. L'utilisation de ^et $garantit qu'une correspondance doit concerner la chaîne entière.

Donc, d'une certaine manière, on nous donne String s, qui est un "multiple" de String t, et l'expression rationnelle le trouvera t(le plus long possible, car il \1est gourmand).

Une fois que vous avez compris pourquoi cette expression régulière fonctionne, alors (en ignorant la première alternative dans l'expression régulière d'OP pour le moment) expliquer comment elle est utilisée pour les tests de primalité est simple.

  • Pour tester la primalité de n, commencez par générer un Stringde longueur n(rempli avec le même char)
  • Le regex capture un Stringd'une longueur (disons k) dans \1, et essaie de correspondre \1+au resteString
    • S'il y a correspondance, alors nest un multiple propre de k, et nn'est donc pas premier.
    • S'il n'y a pas de correspondance, alors il n'y en a pas kqui divise n, et nest donc un

Comment faire .?|(..+?)\1+correspondre les nombres premiers?

En fait, ce n'est pas le cas! Il correspond String dont la longueur n'est PAS prime!

  • .?: La première partie de l'alternance correspond à la Stringlongueur 0ou 1(PAS prime par définition)
  • (..+?)\1+: La deuxième partie de l'alternance, une variation de l'expression rationnelle expliquée ci-dessus, correspond à une Stringlongueur nqui est "un multiple" de a Stringde longueur k >= 2(c'est n-à- dire est un composite, PAS un premier).
    • Notez que le modificateur réticent ?n'est en fait pas nécessaire pour l'exactitude, mais il peut aider à accélérer le processus en essayant d' kabord plus petit

Notez l' ! booleanopérateur complément dans l' returninstruction: il annule le matches. C'est quand le regex NE correspond PAS , nc'est primordial! C'est une logique double négative, donc pas étonnant que ce soit un peu déroutant !!


Simplification

Voici une simple réécriture du code pour le rendre plus lisible:

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

Ce qui précède est essentiellement le même que le code Java d'origine, mais divisé en plusieurs instructions avec des affectations à des variables locales pour rendre la logique plus facile à comprendre.

Nous pouvons également simplifier l'expression rationnelle, en utilisant la répétition finie, comme suit:

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

Encore une fois, étant donné une Stringlongueurn , remplie de la même chose char,

  • .{0,1}vérifie si n = 0,1, PAS amorcer
  • (.{2,})\1+vérifie si nest un multiple correct dek >= 2 , PAS prime

À l'exception du modificateur réticent ?sur\1 (omis pour plus de clarté), l'expression rationnelle ci - dessus est identique à l'original.


Plus de regex amusant

L'expression régulière suivante utilise une technique similaire; il doit être éducatif:

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

Voir également

lubrifiants polygènes
la source
6
+1: Je pense que votre approche est probablement meilleure que la mienne. Je ne sais pas pourquoi j'ai eu autant de votes positifs ou la coche ... vous le méritez plus, je pense. :-( Désolé
Platinum Azure
@Platinum: Wow, je n'aurais jamais pensé que vous alliez dire ça publiquement! Merci pour le soutien. Peut-être que j'en tirerai un [Populist]peu de temps.
polygenelubricants
2
Eh bien, c'est juste la vérité (telle que je la perçois) ... pas vraiment énorme. Je ne suis pas là pour les représentants (même si c'est toujours un bonus et une agréable surprise) ... je suis là pour essayer de répondre aux questions quand je le peux. Il n'est donc pas surprenant que je puisse admettre que quelqu'un l'a fait mieux que moi dans une question particulière.
Platinum Azure
25

Belle astuce regex (bien que très inefficace) ... :)

L'expression régulière définit les non-nombres premiers comme suit:

N n'est pas premier si et seulement si N <= 1 OU N est divisible par un K> 1.

Au lieu de passer la simple représentation numérique de N au moteur d'expression régulière, il est alimenté par une séquence de longueur N, composée d'un caractère répétitif. La première partie de la disjonction vérifie N = 0 ou N = 1, et la seconde recherche un diviseur K> 1, en utilisant des références arrière. Cela force le moteur de regex à trouver une sous-séquence non vide qui peut être répétée au moins deux fois afin de former la séquence. Si une telle sous-séquence existe, cela signifie que sa longueur divise N, donc N n'est pas premier.

Eyal Schneider
la source
2
Curieusement, même après avoir lu à plusieurs reprises les autres explications plus longues et plus techniques, j'ai trouvé que cette explication était celle qui a fait «cliquer» dans ma tête.
Eight-Bit Guru
2
/^1?$|^(11+?)\1+$/

Appliquer aux nombres après conversion en base 1 (1 = 1, 2 = 11, 3 = 111, ...). Les non-nombres premiers correspondront à cela. Si cela ne correspond pas, c'est le premier.

Explication ici .

Dinah
la source