Mod de nombre négatif fait fondre mon cerveau


189

J'essaie de modifier un entier pour obtenir une position de tableau afin qu'il boucle autour. Faire i % arrayLengthfonctionne bien pour les nombres positifs, mais pour les nombres négatifs, tout va mal.

 4 % 3 == 1
 3 % 3 == 0
 2 % 3 == 2
 1 % 3 == 1
 0 % 3 == 0
-1 % 3 == -1
-2 % 3 == -2
-3 % 3 == 0
-4 % 3 == -1

donc j'ai besoin d'une implémentation de

int GetArrayIndex(int i, int arrayLength)

tel que

GetArrayIndex( 4, 3) == 1
GetArrayIndex( 3, 3) == 0
GetArrayIndex( 2, 3) == 2
GetArrayIndex( 1, 3) == 1
GetArrayIndex( 0, 3) == 0
GetArrayIndex(-1, 3) == 2
GetArrayIndex(-2, 3) == 1
GetArrayIndex(-3, 3) == 0
GetArrayIndex(-4, 3) == 2

Je l'ai déjà fait mais pour une raison quelconque, cela fait fondre mon cerveau aujourd'hui :(


Voir la discussion sur le module mathématique sur math.stackexchange.com/questions/519845/…
PPC

Réponses:


281

J'utilise toujours ma propre modfonction, définie comme

int mod(int x, int m) {
    return (x%m + m)%m;
}

Bien sûr, si vous êtes dérangé d'avoir deux appels à l'opération de module, vous pouvez l'écrire comme

int mod(int x, int m) {
    int r = x%m;
    return r<0 ? r+m : r;
}

ou leurs variantes.

La raison pour laquelle cela fonctionne est que "x% m" est toujours dans la plage [-m + 1, m-1]. Donc, s'il est négatif, ajouter m le mettra dans la plage positive sans changer sa valeur modulo m.


7
Remarque: pour une complétude complète de la théorie des nombres, vous pouvez ajouter une ligne en haut indiquant "if (m <0) m = -m;" bien que dans ce cas cela n'a pas d'importance car "arrayLength" est vraisemblablement toujours positif.
ShreevatsaR

4
Si vous voulez vérifier la valeur de m, vous devez également exclure zéro.
billpg

6
@RuudLenders: Non. Si x = -5 et m = 2, alors r = x%mest -1, après quoi r+mest 1. La boucle while n'est pas nécessaire. Le fait est que (comme je l'ai écrit dans la réponse), x%mest toujours strictement supérieur à -m, vous devez donc ajouter mau plus une fois pour le rendre positif.
ShreevatsaR

4
@dcastro: Je veux que -12 mod -10 soit 8. C'est la convention la plus courante en mathématiques, que si vous choisissez un représentant rpour amodulo b, alors il est tel que 0 ≤ r <| b |.
ShreevatsaR

8
+1. Peu m'importe ce que fait un langage individuel pour un module négatif - le «moindre résidu non négatif» présente une régularité mathématique et supprime toute ambiguïté.
Brett Hale

80

Veuillez noter que l'opérateur% de C # et C ++ n'est en fait PAS un modulo, c'est le reste. La formule du modulo que vous souhaitez, dans votre cas, est:

float nfmod(float a,float b)
{
    return a - b * floor(a / b);
}

Vous devez recoder ceci en C # (ou C ++) mais c'est ainsi que vous obtenez modulo et non un reste.


21
"Veuillez noter que l'opérateur% de C ++ n'est en fait PAS un modulo, c'est du reste." Merci, c'est logique maintenant, demandez-vous toujours pourquoi il n'a jamais fonctionné correctement avec des nombres négatifs.
leetNightshade

2
"Veuillez noter que l'opérateur% de C ++ n'est en fait PAS un modulo, c'est un reste." Je ne pense pas que ce soit exact et je ne vois pas pourquoi un modulo est différent du reste. C'est ce qu'il dit également sur la page Wikipédia de Modulo Operation. C'est juste que les langages de programmation traitent les nombres négatifs différemment. L'opérateur modulo en C # compte évidemment les restes "à partir de" zéro (-9% 4 = -1, car 4 * -2 vaut -8 avec une différence de -1) alors qu'une autre définition considérerait -9% 4 comme +3, car -4 * 3 est -12, reste +3 (comme dans la fonction de recherche de Google, pas sûr de la langue du back-end).
Tyress

18
Tyress, il y a une différence entre le module et le reste. Par exemple: -21 mod 4 is 3 because -21 + 4 x 6 is 3. mais -21 divided by 4 gives -5avec un remainder of -1. Pour les valeurs positives, il n'y a pas de différence. Veuillez donc vous informer de ces différences. Et ne faites pas confiance à Wikipédia tout le temps :)
Петър Петров

2
Pourquoi quelqu'un voudrait-il utiliser la fonction reste au lieu d'un modulo? Pourquoi ont-ils fait du %reste?
Aaron Franke

4
@AaronFranke - c'est un héritage des processeurs précédents qui avaient du matériel de division pour produire rapidement un quotient et un reste - et c'est ce que ce matériel a fait avec un dividende négatif. Le langage reflétait simplement le matériel. La plupart du temps, les programmeurs travaillaient avec des dividendes positifs et ignoraient cette bizarrerie. La vitesse était primordiale.
ToolmakerSteve

16

Implémentation sur %une seule ligne en utilisant une seule fois:

int mod(int k, int n) {  return ((k %= n) < 0) ? k+n : k;  }

1
est-ce correct? car je ne le vois pas accepté par personne, ni aucun commentaire à ce sujet. Par exemple. mod (-10,6) retournera 6. Est-ce correct? ne devrait-il pas retourner 4?
John Demetriou

3
@JohnDemetriou Vos chiffres sont tous les deux faux: (A) il doit renvoyer 2 et (B) il renvoie 2; essayez d'exécuter le code. Item (A): pour trouver mod(-10, 6)à la main, vous ajoutez ou soustrayez 6 répétitivement jusqu'à ce que la réponse soit dans la fourchette [0, 6). Cette notation signifie «inclusif à gauche et exclusif à droite». Dans notre cas, nous ajoutons 6 deux fois, ce qui donne 2. Le code est assez simple, et il est facile de voir qu'il est juste: d'abord, il fait l'équivalent d'ajouter / soustraire ncomme ci-dessus, sauf qu'il arrête un ncourt, s'il s'approche de le côté négatif. Dans ce cas, nous le réparons. Là: commentaires :)
Evgeni Sergeev

1
Au fait, voici une raison pour laquelle utiliser un single %pourrait être une bonne idée. Consultez le tableau Combien coûtent les choses en code managé dans l'article Rédaction de code managé plus rapide: Know What Things Cost . L'utilisation %est tout aussi coûteuse que celle int divindiquée dans le tableau: environ 36 fois plus cher que d'ajouter ou de soustraire, et environ 13 fois plus cher que de multiplier. Bien sûr, pas de problème à moins que ce ne soit au cœur de ce que fait votre code.
Evgeni Sergeev

2
Mais un single est-il %plus cher qu'un test et un saut, surtout s'il ne peut pas être facilement prédit?
Medinoc

6

La réponse de ShreevatsaR ne fonctionnera pas dans tous les cas, même si vous ajoutez "si (m <0) m = -m;", si vous tenez compte des dividendes / diviseurs négatifs.

Par exemple, -12 mod -10 sera 8 et il devrait être -2.

L'implémentation suivante fonctionnera pour les dividendes / diviseurs positifs et négatifs et est conforme à d'autres implémentations (à savoir, Java, Python, Ruby, Scala, Scheme, Javascript et Google's Calculator):

internal static class IntExtensions
{
    internal static int Mod(this int a, int n)
    {
        if (n == 0)
            throw new ArgumentOutOfRangeException("n", "(a mod 0) is undefined.");

        //puts a in the [-n+1, n-1] range using the remainder operator
        int remainder = a%n;

        //if the remainder is less than zero, add n to put it in the [0, n-1] range if n is positive
        //if the remainder is greater than zero, add n to put it in the [n-1, 0] range if n is negative
        if ((n > 0 && remainder < 0) ||
            (n < 0 && remainder > 0))
            return remainder + n;
        return remainder;
    }
}

Suite de tests utilisant xUnit:

    [Theory]
    [PropertyData("GetTestData")]
    public void Mod_ReturnsCorrectModulo(int dividend, int divisor, int expectedMod)
    {
        Assert.Equal(expectedMod, dividend.Mod(divisor));
    }

    [Fact]
    public void Mod_ThrowsException_IfDivisorIsZero()
    {
        Assert.Throws<ArgumentOutOfRangeException>(() => 1.Mod(0));
    }

    public static IEnumerable<object[]> GetTestData
    {
        get
        {
            yield return new object[] {1, 1, 0};
            yield return new object[] {0, 1, 0};
            yield return new object[] {2, 10, 2};
            yield return new object[] {12, 10, 2};
            yield return new object[] {22, 10, 2};
            yield return new object[] {-2, 10, 8};
            yield return new object[] {-12, 10, 8};
            yield return new object[] {-22, 10, 8};
            yield return new object[] { 2, -10, -8 };
            yield return new object[] { 12, -10, -8 };
            yield return new object[] { 22, -10, -8 };
            yield return new object[] { -2, -10, -2 };
            yield return new object[] { -12, -10, -2 };
            yield return new object[] { -22, -10, -2 };
        }
    }

Premièrement, une modfonction est généralement appelée avec un module positif (notez la variable arrayLengthdans la question d'origine à laquelle on répond ici, qui n'est probablement jamais négative), donc la fonction n'a pas vraiment besoin d'être conçue pour fonctionner pour un module négatif. (C'est pourquoi je mentionne le traitement du module négatif dans un commentaire sur ma réponse, pas dans la réponse elle-même.) (Suite ...)
ShreevatsaR

3
(... suite) Deuxièmement, que faire pour un module négatif est une question de convention. Voir par exemple Wikipedia . «Habituellement, en théorie des nombres, le reste positif est toujours choisi», et c'est ainsi que je l'ai appris aussi (dans la théorie élémentaire des nombres de Burton ). Knuth le définit également de cette façon (en particulier, r = a - b floor(a/b)est toujours positif). Même parmi les systèmes informatiques, Pascal et Maple par exemple, le définissent comme toujours positif.
ShreevatsaR

@ShreevatsaR Je sais que la définition euclidienne déclare que le résultat sera toujours positif - mais j'ai l'impression que la plupart des implémentations de mod modernes renverront une valeur dans la plage [n + 1, 0] pour un diviseur négatif "n", ce qui signifie que -12 mod -10 = -2. J'ai examiné Google Calculator , Python , Ruby et Scala , et ils suivent tous cette convention.
dcastro

Aussi, à ajouter à la liste: Scheme and Javascript
dcastro

1
Encore une fois, c'est toujours une bonne lecture. La définition "toujours positive" (ma réponse) est cohérente avec ALGOL, Dart, Maple, Pascal, Z3, etc. Le "signe du diviseur" (cette réponse) est cohérent avec: APL, COBOL, J, Lua, Mathematica, MS Excel, Perl, Python, R, Ruby, Tcl, etc. Les deux sont incompatibles avec le «signe du dividende» comme dans: AWK, bash, bc, C99, C ++ 11, C #, D, Eiffel, Erlang, Go, Java , OCaml, PHP, Rust, Scala, Swift, VB, assemblage x86, etc. Je ne vois vraiment pas comment vous pouvez prétendre qu'une convention est "correcte" et d'autres "incorrecte".
ShreevatsaR

6

Ajout d'un peu de compréhension.

Par définition euclidienne, le résultat du mod doit être toujours positif.

Ex:

 int n = 5;
 int x = -3;

 int mod(int n, int x)
 {
     return ((n%x)+x)%x;
 }

Production:

 -1

15
Je suis confus ... vous dites que le résultat doit toujours être positif, mais ensuite listez la sortie comme -1?
Jeff B

@JeffBridgman J'ai déclaré cela basé sur la définition euclidienne. `il y a deux choix possibles pour le reste, l'un négatif et l'autre positif, et il y a aussi deux choix possibles pour le quotient. Habituellement, en théorie des nombres, the positive remainder is always chosenmais les langages de programmation choisissent en fonction de la langue et des signes de a et / ou n. [5] Le Pascal standard et Algol68 donnent un reste positif (ou 0) même pour les diviseurs négatifs, et certains langages de programmation, tels que C90, laissent le soin à l'implémentation lorsque n ou a est négatif. »
Abin

5

Comparaison de deux réponses prédominantes

(x%m + m)%m;

et

int r = x%m;
return r<0 ? r+m : r;

Personne n'a en fait mentionné le fait que le premier peut lancer un OverflowExceptioncertain temps tandis que le second ne le fera pas. Pire encore, avec un contexte non coché par défaut, la première réponse peut renvoyer la mauvaise réponse (voir mod(int.MaxValue - 1, int.MaxValue)par exemple). La deuxième réponse semble donc non seulement plus rapide, mais aussi plus correcte.


4

Ajoutez simplement votre module (arrayLength) au résultat négatif de% et tout ira bien.


4

Pour les développeurs plus sensibles aux performances

uint wrap(int k, int n) ((uint)k)%n

Une petite comparaison des performances

Modulo: 00:00:07.2661827 ((n%x)+x)%x)
Cast:   00:00:03.2202334 ((uint)k)%n
If:     00:00:13.5378989 ((k %= n) < 0) ? k+n : k

En ce qui concerne le coût des performances du casting, jetez un œil ici


3
Je pense que cela -3 % 10devrait être de -3 ou 7. Puisqu'un résultat non négatif est souhaité, 7 serait la réponse. Votre implémentation renvoie 3. Vous devez modifier les deux paramètres uintet supprimer la distribution.
J'ai aimé l'ancien Stack Overflow du

5
L'arithmétique non signée n'est équivalente que si nest une puissance de deux, auquel cas vous pouvez simplement utiliser un and ( (uint)k & (n - 1)) logique à la place, si le compilateur ne le fait pas déjà pour vous (les compilateurs sont souvent assez intelligents pour comprendre cela).
j_schultz

2

J'aime le truc présenté par Peter N Lewis sur ce fil : «Si n a une plage limitée, alors vous pouvez obtenir le résultat que vous voulez simplement en ajoutant un multiple constant connu de [le diviseur] qui est supérieur à la valeur absolue du le minimum."

Donc si j'ai une valeur d qui est en degrés et que je veux prendre

d % 180f

et je veux éviter les problèmes si d est négatif, alors je fais simplement ceci:

(d + 720f) % 180f

Cela suppose que bien que d puisse être négatif, on sait qu'il ne sera jamais plus négatif que -720.


2
-1: pas assez général, (et il est très facile de donner une solution plus générale).
Evgeni Sergeev

4
C'est en fait très utile. lorsque vous avez une plage significative, cela peut simplifier le calcul. dans mon cas math.stackexchange.com/questions/2279751/...
M.kazem Akhgary

Exactement, je viens de l'utiliser pour le calcul dayOfWeek (plage connue de -6 à +6) et cela a économisé en avoir deux %.
NetMage

@EvgeniSergeev +0 pour moi: ne répond pas à la question OP mais peut être utile dans un contexte plus spécifique (mais toujours dans le contexte de la question)
Erdal G.30

1

Vous vous attendez à un comportement contraire au comportement documenté de l'opérateur% dans c # - peut-être parce que vous vous attendez à ce qu'il fonctionne de manière à fonctionner dans un autre langage auquel vous êtes plus habitué. La documentation sur les états c # (c'est moi qui souligne):

Pour les opérandes de types entiers, le résultat de a% b est la valeur produite par a - (a / b) * b. Le signe du reste non nul est le même que celui de l'opérande de gauche

La valeur souhaitée peut être calculée avec une étape supplémentaire:

int GetArrayIndex(int i, int arrayLength){
    int mod = i % arrayLength;
    return (mod>=0) : mod ? mod + arrayLength;
}

1

Une implémentation en ligne unique de la réponse de dcastro (la plus compatible avec les autres langages):

int Mod(int a, int n)
{
    return (((a %= n) < 0) && n > 0) || (a > 0 && n < 0) ? a + n : a;
}

Si vous souhaitez conserver l'utilisation d' %opérateur (vous ne pouvez pas surcharger les opérateurs natifs en C #):

public class IntM
{
    private int _value;

    private IntM(int value)
    {
        _value = value;
    }

    private static int Mod(int a, int n)
    {
        return (((a %= n) < 0) && n > 0) || (a > 0 && n < 0) ? a + n : a;
    }

    public static implicit operator int(IntM i) => i._value;
    public static implicit operator IntM(int i) => new IntM(i);
    public static int operator %(IntM a, int n) => Mod(a, n);
    public static int operator %(int a, IntM n) => Mod(a, n);
}

Cas d'utilisation, les deux fonctionnent:

int r = (IntM)a % n;

// Or
int r = a % n(IntM);

0

Toutes les réponses ici fonctionnent très bien si votre diviseur est positif, mais ce n'est pas tout à fait complet. Voici mon implémentation qui retourne toujours sur une plage de [0, b), telle que le signe de la sortie est le même que le signe du diviseur, ce qui permet des diviseurs négatifs comme point final de la plage de sortie.

PosMod(5, 3)retourne 2
PosMod(-5, 3)retourne 1
PosMod(5, -3)retourne -1
PosMod(-5, -3)retourne-2

    /// <summary>
    /// Performs a canonical Modulus operation, where the output is on the range [0, b).
    /// </summary>
    public static real_t PosMod(real_t a, real_t b)
    {
        real_t c = a % b;
        if ((c < 0 && b > 0) || (c > 0 && b < 0)) 
        {
            c += b;
        }
        return c;
    }

(où real_tpeut être n'importe quel type de nombre)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.