Mod de número negativo está derritiendo mi cerebro

189

Estoy tratando de modificar un número entero para obtener una posición de matriz para que se repita. Hacerlo i % arrayLengthfunciona bien para números positivos pero para números negativos todo sale 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

así que necesito una implementación de

int GetArrayIndex(int i, int arrayLength)

tal 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

He hecho esto antes, pero por alguna razón hoy me está derritiendo el cerebro :(

Coronel Panic
fuente
Vea la discusión sobre el módulo matemático en math.stackexchange.com/questions/519845/…
PPC

Respuestas:

281

Siempre uso mi propia modfunción, definida como

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

Por supuesto, si le preocupa tener dos llamadas a la operación de módulo, puede escribirlo como

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

o variantes de los mismos.

La razón por la que funciona es que "x% m" siempre está en el rango [-m + 1, m-1]. Por lo tanto, si es negativo, agregar m lo colocará en el rango positivo sin cambiar su valor módulo m.

ShreevatsaR
fuente
77
Nota: para completar la integridad teórica de los números, es posible que desee agregar una línea en la parte superior que diga "if (m <0) m = -m;" aunque en este caso no importa ya que "arrayLength" es presumiblemente siempre positivo.
ShreevatsaR
44
Si va a verificar el valor de m, también debe excluir cero.
billpg
66
@RuudLenders: No. Si x = -5 ym = 2, entonces r = x%mes -1, después de lo cual r+mes 1. El ciclo while no es necesario. El punto es que (como escribí en la respuesta), x%msiempre es estrictamente mayor que -m, por lo que debe agregar mcomo máximo una vez para que sea positivo.
ShreevatsaR
44
@dcastro: Quiero que -12 mod -10 sea 8. Esta es la convención más común en matemáticas, que si se elige un representante rpara amódulo b, entonces es tal que 0 ≤ r <| b |.
ShreevatsaR
8
+1. No me importa lo que haga un lenguaje individual para un módulo negativo: el 'residuo menos negativo' exhibe una regularidad matemática y elimina cualquier ambigüedad.
Brett Hale
80

Tenga en cuenta que el operador% de C # y C ++ en realidad NO es un módulo, es el resto. La fórmula para el módulo que desea, en su caso, es:

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

Debe recodificar esto en C # (o C ++), pero esta es la forma en que obtiene el módulo y no un resto.

Петър Петров
fuente
21
"Tenga en cuenta que el operador% C ++ en realidad NO es un módulo, es el resto". Gracias, tiene sentido ahora, siempre se preguntan por qué nunca funcionó correctamente con números negativos.
leetNightshade
2
"Tenga en cuenta que el operador% C ++ en realidad NO es un módulo, es el resto". No creo que esto sea exacto y no veo por qué un módulo es diferente del resto. Eso es lo que también dice en la página de Wikipedia de Modulo Operation. Es solo que los lenguajes de programación tratan los números negativos de manera diferente. El operador de módulo en C # obviamente cuenta los restos "desde" cero (-9% 4 = -1, porque 4 * -2 es -8 con una diferencia de -1), mientras que otra definición consideraría -9% 4 como +3, porque -4 * 3 es -12, resto +3 (como en la función de búsqueda de Google, no estoy seguro del idioma de fondo allí).
Tyress
18
Tyress, hay una diferencia entre módulo y resto. Por ejemplo: -21 mod 4 is 3 because -21 + 4 x 6 is 3. Pero -21 divided by 4 gives -5con a remainder of -1. Para valores positivos, no hay diferencia. Entonces, infórmese sobre estas diferencias. Y no confíes en Wikipedia todo el tiempo :)
Петър Петров
2
¿Por qué alguien querría usar la función restante en lugar de un módulo? ¿Por qué hicieron el %resto?
Aaron Franke
44
@AaronFranke, es un legado de cpus anteriores que tenían hardware de división para producir rápidamente un cociente y un resto, y esto es lo que hizo ese hardware con un dividendo negativo. El lenguaje simplemente reflejaba el hardware. La mayoría de las veces los programadores trabajaban con dividendos positivos e ignoraban este capricho. La velocidad era primordial.
ToolmakerSteve
16

Implementación de %una sola línea usando solo una vez:

int mod(int k, int n) {  return ((k %= n) < 0) ? k+n : k;  }
Evgeni Sergeev
fuente
1
¿es esto correcto? ya que no lo veo como aceptado por nadie, ni ningún comentario al respecto. Por ejemplo. mod (-10,6) devolverá 6. ¿Es eso correcto? ¿No debería devolver 4?
John Demetriou
3
@JohnDemetriou Sus números están equivocados: (A) debería devolver 2 y (B) devuelve 2; Intenta ejecutar el código. Ítem ​​(A): para encontrar mod(-10, 6)a mano, sumas o restas 6 repetidamente hasta que la respuesta esté en el rango [0, 6). Esta notación significa "inclusivo a la izquierda y exclusivo a la derecha". En nuestro caso, agregamos 6 dos veces, dando 2. El código es bastante simple, y es fácil ver que es correcto: primero, hace el equivalente de sumar / restar ncomo anteriormente, excepto que se detiene uno ncorto, si se aproxima desde El lado negativo. En ese caso lo arreglamos. Allí: comentarios :)
Evgeni Sergeev
1
Por cierto, aquí hay una razón por la cual usar una sola %podría ser una buena idea. Consulte la tabla Cuánto cuestan las cosas en el código administrado en el artículo Escribir código administrado más rápido: Sepa lo que cuestan las cosas . El uso %es similar al que int divfigura en la tabla: aproximadamente 36 veces más costoso que sumar o restar, y aproximadamente 13 veces más costoso que multiplicar. Por supuesto, no es gran cosa a menos que esto sea el núcleo de lo que está haciendo su código.
Evgeni Sergeev
2
Pero, ¿es %más costoso que una prueba y un salto, especialmente si no se puede predecir fácilmente?
Medinoc
6

La respuesta de ShreevatsaR no funcionará en todos los casos, incluso si agrega "if (m <0) m = -m;", si tiene en cuenta los dividendos / divisores negativos.

Por ejemplo, -12 mod -10 será 8, y debería ser -2.

La siguiente implementación funcionará para dividendos / divisores positivos y negativos y cumple con otras implementaciones (a saber, Java, Python, Ruby, Scala, Scheme, Javascript y la Calculadora de Google):

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;
    }
}

Conjunto de pruebas con 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 };
        }
    }
dcastro
fuente
En primer lugar, modgeneralmente se llama a una función con módulo positivo (tenga arrayLengthen cuenta la variable en la pregunta original que se está respondiendo aquí, que presumiblemente nunca es negativa), por lo que no es necesario hacer que la función funcione para un módulo negativo. (Es por eso que menciono el tratamiento del módulo negativo en un comentario sobre mi respuesta, no en la respuesta en sí.) (
Cont
3
(... cont.) En segundo lugar, qué hacer para un módulo negativo es una cuestión de convención. Ver, por ejemplo, Wikipedia . "Por lo general, en la teoría de números, el resto positivo siempre se elige", y así es como lo aprendí también (en la teoría de números de primaria de Burton ). Knuth también lo define de esa manera (específicamente, r = a - b floor(a/b)siempre es positivo). Incluso entre los sistemas informáticos, Pascal y Maple, por ejemplo, lo definen como siempre positivo.
ShreevatsaR
@ShreevatsaR Sé que la definición euclidiana establece que el resultado siempre será positivo, pero tengo la impresión de que la mayoría de las implementaciones de mods modernas devolverán un valor en el rango [n + 1, 0] para un divisor negativo "n", lo que significa que -12 mod -10 = -2. He buscado en Google Calculator , Python , Ruby y Scala , y todos siguen esta convención.
Dcastro
Además, para agregar a la lista: Esquema y Javascript
dcastro
1
Nuevamente, esta sigue siendo una buena lectura. La definición "siempre positiva" (mi respuesta) es consistente con ALGOL, Dart, Maple, Pascal, Z3, etc. El "signo del divisor" (esta respuesta) es consistente con: APL, COBOL, J, Lua, Mathematica, MS Excel, Perl, Python, R, Ruby, Tcl, etc. Ambos son inconsistentes con el "signo de dividendo" como en: AWK, bash, bc, C99, C ++ 11, C #, D, Eiffel, Erlang, Go, Java , OCaml, PHP, Rust, Scala, Swift, VB, x86, etc. Realmente no veo cómo puede afirmar que una convención es "correcta" y otras "incorrectas".
ShreevatsaR
6

Agregando algo de comprensión.

Por definición euclidiana, el resultado del mod debe ser siempre positivo.

Ex:

 int n = 5;
 int x = -3;

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

Salida:

 -1
Una papelera
fuente
15
Estoy confundido ... usted dice que el resultado siempre debe ser positivo, pero luego enumera la salida como -1?
Jeff B
@JeffBridgman He dicho que basado en la definición euclidiana. `hay dos opciones posibles para el resto, una negativa y otra positiva, y también hay dos opciones posibles para el cociente. Por lo general, en teoría de números the positive remainder is always chosen, pero los lenguajes de programación eligen dependiendo del lenguaje y los signos de a y / o n. [5] Pascal estándar y Algol68 dan un resto positivo (o 0) incluso para divisores negativos, y algunos lenguajes de programación, como C90, lo dejan a la implementación cuando n o a es negativo. '
Abin
5

Comparar dos respuestas predominantes

(x%m + m)%m;

y

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

En realidad, nadie mencionó el hecho de que el primero puede tirar un OverflowExceptionrato, mientras que el segundo no lo hará. Peor aún, con el contexto predeterminado sin marcar, la primera respuesta puede devolver la respuesta incorrecta (ver, mod(int.MaxValue - 1, int.MaxValue)por ejemplo). Entonces, la segunda respuesta no solo parece ser más rápida, sino también más correcta.

lilo0
fuente
4

Simplemente agregue su módulo (arrayLength) al resultado negativo de% y estará bien.

starblue
fuente
4

Para los desarrolladores más conscientes del rendimiento

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

Una pequeña comparación de rendimiento

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 cuanto al costo de rendimiento del elenco, mira aquí

Markus Cozowicz
fuente
3
Creo que -3 % 10debería ser -3 o 7. Como se desea un resultado no negativo, 7 sería la respuesta. Su implementación devuelve 3. Debe cambiar ambos parámetros uinty eliminar el reparto.
Me gustó el viejo Stack Overflow del
55
La aritmética sin signo solo es equivalente si nes una potencia de dos, en cuyo caso simplemente puede usar una lógica y ( (uint)k & (n - 1)) en su lugar, si el compilador ya no lo hace por usted (los compiladores suelen ser lo suficientemente inteligentes como para resolver esto).
j_schultz
2

Me gusta el truco presentado por Peter N Lewis en este hilo : "Si n tiene un rango limitado, entonces puede obtener el resultado que desea simplemente agregando un múltiplo constante conocido de [el divisor] que es mayor que el valor absoluto de la mínimo."

Entonces, si tengo un valor d que está en grados y quiero tomar

d % 180f

y quiero evitar los problemas si d es negativo, entonces solo hago esto:

(d + 720f) % 180f

Esto supone que aunque d puede ser negativo, se sabe que nunca será más negativo que -720.

RenniePet
fuente
2
-1: no es lo suficientemente general (y es muy fácil dar una solución más general).
Evgeni Sergeev
44
Esto es realmente muy útil. cuando tiene un rango significativo, esto puede simplificar el cálculo. en mi caso math.stackexchange.com/questions/2279751/…
M.kazem Akhgary
Exactamente, solo usé esto para el cálculo de dayOfWeek (rango conocido de -6 a +6) y ahorró tener dos %.
NetMage
@EvgeniSergeev +0 para mí: no responde la pregunta de OP pero puede ser útil en un contexto más específico (pero aún en el contexto de la pregunta)
Erdal G.
1

Espera un comportamiento que es contrario al comportamiento documentado del operador% en c #, posiblemente porque espera que funcione de una manera que funcione en otro idioma al que está más acostumbrado. La documentación sobre los estados de c # (énfasis mío):

Para los operandos de tipos enteros, el resultado de a% b es el valor producido por a - (a / b) * b. El signo del resto distinto de cero es el mismo que el del operando izquierdo

El valor que desea puede calcularse con un paso adicional:

int GetArrayIndex(int i, int arrayLength){
    int mod = i % arrayLength;
    return (mod>=0) : mod ? mod + arrayLength;
}
Andrés
fuente
1

Una implementación de una sola línea de la respuesta de dcastro (la más compatible con otros idiomas):

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

Si desea mantener el uso del %operador (no puede sobrecargar los operadores nativos 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);
}

Caso de uso, ambos trabajos:

int r = (IntM)a % n;

// Or
int r = a % n(IntM);
Erdal G.
fuente
0

Todas las respuestas aquí funcionan muy bien si su divisor es positivo, pero no está completo. Aquí está mi implementación que siempre regresa en un rango de [0, b), de modo que el signo de la salida es el mismo que el del divisor, permitiendo divisores negativos como punto final para el rango de salida.

PosMod(5, 3)devoluciones 2
PosMod(-5, 3)devoluciones 1
PosMod(5, -3)devoluciones -1
PosMod(-5, -3)devoluciones-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;
    }

(donde real_tpuede ser cualquier tipo de número)

Aaron Franke
fuente