Mod di numero negativo mi sta sciogliendo il cervello


189

Sto provando a modificare un numero intero per ottenere una posizione di array in modo che si arrotoli. Fare i % arrayLengthfunziona bene per i numeri positivi ma per i numeri negativi va tutto storto.

 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

quindi ho bisogno di un'implementazione di

int GetArrayIndex(int i, int arrayLength)

tale che

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

L'ho già fatto prima, ma per qualche ragione oggi mi sta sciogliendo il cervello :(


Vedi la discussione sul modulo matematico su math.stackexchange.com/questions/519845/…
PPC

Risposte:


281

Uso sempre la mia modfunzione, definita come

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

Naturalmente, se ti preoccupi di avere due chiamate all'operazione del modulo, potresti scriverlo come

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

o sue varianti.

Il motivo per cui funziona è che "x% m" è sempre nell'intervallo [-m + 1, m-1]. Quindi, se è del tutto negativo, l'aggiunta di m lo inserirà nell'intervallo positivo senza modificarne il valore modulo m.


7
Nota: per completezza teorica numerica completa, potresti voler aggiungere una riga in alto che dice "if (m <0) m = -m;" anche se in questo caso non importa come "arrayLength" sia presumibilmente sempre positivo.
ShreevatsaR,

4
Se stai per verificare il valore di m, dovresti anche escludere zero.
billpg,

6
@RuudLenders: No. Se x = -5 e m = 2, allora lo r = x%mè -1, dopodiché lo r+mè 1. Il ciclo while non è necessario. Il punto è che (come ho scritto nella risposta), x%mè sempre strettamente maggiore di -m, quindi è necessario aggiungere mal massimo una volta per renderlo positivo.
ShreevatsaR,

4
@dcastro: Io non voglio -12 -10 mod per essere 8. Questo è il più convenzione comune in matematica, che, se la scelta di un rappresentante rper il amodulo b, allora è tale che 0 ≤ r <| b |.
ShreevatsaR,

8
+1. Non mi interessa cosa faccia un singolo linguaggio per un modulo negativo - il "residuo meno non negativo" mostra una regolarità matematica e rimuove qualsiasi ambiguità.
Brett Hale,

80

Nota che l'operatore% C # e C ++ NON è in realtà un modulo, è il resto. La formula per modulo che vuoi, nel tuo caso, è:

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

Devi ricodificarlo in C # (o C ++) ma questo è il modo in cui ottieni modulo e non un resto.


21
"Nota che l 'operatore% di C ++ NON è in realtà un modulo, è il resto." Grazie, ora ha senso, mi chiedo sempre perché non abbia mai funzionato correttamente con numeri negativi.
leetNightshade

2
"Si noti che l'operatore% di C ++ NON è in realtà un modulo, è il resto." Non penso che questo sia accurato e non vedo perché un modulo sia diverso dal resto. Questo è ciò che dice anche sulla pagina Wikipedia di Modulo Operation. È solo che i linguaggi di programmazione trattano i numeri negativi in ​​modo diverso. L'operatore modulo in C # ovviamente conta i resti "da" zero (-9% 4 = -1, perché 4 * -2 è -8 con una differenza di -1) mentre un'altra definizione considererebbe -9% 4 come +3, perché -4 * 3 è -12, resto +3 (come nella funzione di ricerca di Google, non sicuro della lingua di back-end lì).
Tyress,

18
Tyress, c'è una differenza tra modulo e resto. Ad esempio: -21 mod 4 is 3 because -21 + 4 x 6 is 3. Ma -21 divided by 4 gives -5con a remainder of -1. Per valori positivi, non c'è differenza. Quindi, per favore informati su queste differenze. E non fidarti di Wikipedia tutto il tempo :)
Петър Петров

2
Perché qualcuno dovrebbe voler usare la funzione restante invece di un modulo? Perché hanno fatto il %resto?
Aaron Franke,

4
@AaronFranke - è un'eredità di cpus precedente che aveva hardware di divisione per produrre rapidamente un quoziente e un resto - e questo è ciò che quell'hardware ha dato un dividendo negativo. Il linguaggio rispecchiava semplicemente l'hardware. Il più delle volte i programmatori lavoravano con dividendi positivi e ignoravano questa stranezza. La velocità era fondamentale.
ToolmakerSteve

16

Implementazione su linea singola usando %una sola volta:

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

1
è corretto? poiché non lo vedo come accettato da nessuno, né alcun commento ad esso. Per esempio. mod (-10,6) restituirà 6. È corretto? non dovrebbe restituire 4?
John Demetriou,

3
@JohnDemetriou I tuoi numeri sono entrambi sbagliati: (A) dovrebbe restituire 2 e (B) restituire 2; prova a eseguire il codice. Articolo (A): per trovare mod(-10, 6)a mano, puoi aggiungere o sottrarre 6 ripetutamente fino a quando la risposta è nell'intervallo [0, 6). Questa notazione significa "inclusivo a sinistra ed esclusivo a destra". Nel nostro caso, aggiungiamo 6 due volte, dando 2. Il codice è abbastanza semplice, ed è facile vedere che è giusto: in primo luogo, fa l'equivalente di aggiungere / sottrarre ncome sopra, tranne per il fatto che si ferma un nbreve, se ci si avvicina da il lato negativo. In quel caso lo ripariamo. Ecco: commenti :)
Evgeni Sergeev

1
A proposito, ecco un motivo per cui usare un singolo %potrebbe essere una buona idea. Vedi la tabella Che cosa costa il codice gestito nell'articolo Scrivere un codice gestito più veloce: sapere cosa costa . L'uso %è simile a quello int divelencato nella tabella: circa 36 volte più costoso dell'aggiunta o sottrazione e circa 13 volte più costoso della moltiplicazione. Naturalmente, niente di grave a meno che questo non sia al centro di ciò che sta facendo il tuo codice.
Evgeni Sergeev

2
Ma è un singolo %più costoso di un test e saltare, soprattutto se non può essere facilmente previsto?
Medinoc,

6

La risposta di ShreevatsaR non funzionerà in tutti i casi, anche se si aggiunge "if (m <0) m = -m;", se si considerano dividendi / divisori negativi.

Ad esempio, -12 mod -10 sarà 8 e dovrebbe essere -2.

La seguente implementazione funzionerà sia per i dividendi / divisori positivi che negativi e è conforme ad altre implementazioni (vale a dire Java, Python, Ruby, Scala, Scheme, Javascript e 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 di test utilizzando 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 };
        }
    }

In primo luogo, una modfunzione viene generalmente chiamata con modulo positivo (notare la variabile arrayLengthnella domanda originale a cui si sta rispondendo qui, che presumibilmente non è mai negativa), quindi la funzione non ha davvero bisogno di essere fatta funzionare per modulo negativo. (Questo è il motivo per cui cito il trattamento del modulo negativo in un commento sulla mia risposta, non nella risposta stessa.) (
Continua

3
(... continua) In secondo luogo, cosa fare per un modulo negativo è una questione di convenzione. Vedi ad esempio Wikipedia . "Di solito, nella teoria dei numeri, viene sempre scelto il resto positivo", ed è così che l'ho imparato anch'io (nella teoria dei numeri elementari di Burton ). Knuth lo definisce anche in questo modo (in particolare, r = a - b floor(a/b)è sempre positivo). Anche tra i sistemi informatici, ad esempio Pascal e Maple, lo definiscono sempre positivo.
ShreevatsaR,

@ShreevatsaR So che la definizione euclidea afferma che il risultato sarà sempre positivo, ma ho l'impressione che la maggior parte delle implementazioni mod moderne restituirà un valore nell'intervallo [n + 1, 0] per un divisore negativo "n", il che significa che -12 mod -10 = -2. Ho esaminato Google Calculator , Python , Ruby e Scala , e tutti seguono questa convenzione.
dcastro,

Inoltre, per aggiungere all'elenco: Schema e Javascript
dcastro

1
Ancora una volta, questa è ancora una buona lettura. La definizione "sempre positiva" (la mia risposta) è coerente con ALGOL, Dart, Maple, Pascal, Z3, ecc. Il "segno di divisore" (questa risposta) è coerente con: APL, COBOL, J, Lua, Mathematica, MS Excel, Perl, Python, R, Ruby, Tcl, ecc. Entrambi sono incompatibili con il "segno di dividendo" come in: AWK, bash, bc, C99, C ++ 11, C #, D, Eiffel, Erlang, Go, Java , OCaml, PHP, Rust, Scala, Swift, VB, assembly x86, ecc. Non vedo davvero come si possa affermare che una convenzione è "corretta" e altre "sbagliate".
ShreevatsaR,

6

Aggiungendo un po 'di comprensione.

Per definizione euclidea il risultato mod deve essere sempre positivo.

Ex:

 int n = 5;
 int x = -3;

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

Produzione:

 -1

15
Sono confuso ... dici che il risultato dovrebbe essere sempre positivo, ma poi elencare l'output come -1?
Jeff B,

@JeffBridgman Ho affermato che sulla base della definizione euclidea. `ci sono due possibili scelte per il resto, una negativa e l'altra positiva, e ci sono anche due possibili scelte per il quoziente. Di solito, nella teoria dei numeri, the positive remainder is always chosenma i linguaggi di programmazione scelgono in base alla lingua e ai segni di a e / o n. [5] Lo standard Pascal e Algol68 forniscono un resto positivo (o 0) anche per i divisori negativi, e alcuni linguaggi di programmazione, come C90, lo lasciano all'implementazione quando uno di n o un è negativo.
Abin,

5

Confronto tra due risposte predominanti

(x%m + m)%m;

e

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

Nessuno in realtà ha menzionato il fatto che il primo potrebbe lanciare un OverflowExceptionpo 'di tempo, il secondo no. Ancora peggio, con il contesto predefinito non selezionato, la prima risposta potrebbe restituire la risposta errata (vedere mod(int.MaxValue - 1, int.MaxValue)ad esempio). Quindi la seconda risposta non solo sembra essere più veloce, ma anche più corretta.


4

Aggiungi il tuo modulo (arrayLength) al risultato negativo di% e andrà tutto bene.


4

Per gli sviluppatori più consapevoli delle prestazioni

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

Un piccolo confronto delle prestazioni

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

Per quanto riguarda il costo delle prestazioni del cast, dai un'occhiata qui


3
Pensa che -3 % 10dovrebbe essere -3 o 7. Dato che si desidera un risultato non negativo, 7 sarebbe la risposta. L'implementazione restituisce 3. È necessario modificare entrambi i parametri uinte rimuovere il cast.
Mi è piaciuto il vecchio Stack Overflow del

5
L'aritmetica senza segno è equivalente solo se nè una potenza di due, nel qual caso puoi semplicemente usare un logico e ( (uint)k & (n - 1)) invece, se il compilatore non lo fa già per te (i compilatori sono spesso abbastanza intelligenti da capirlo).
j_schultz,

2

Mi piace il trucco presentato da Peter N Lewis su questo thread : "Se n ha un intervallo limitato, puoi ottenere il risultato desiderato semplicemente aggiungendo un multiplo costante noto del [divisore] che è maggiore del valore assoluto del valore minimo."

Quindi, se ho un valore d che è in gradi e voglio prendere

d % 180f

e voglio evitare i problemi se d è negativo, quindi invece faccio solo questo:

(d + 720f) % 180f

Ciò presuppone che sebbene d possa essere negativo, è noto che non sarà mai più negativo di -720.


2
-1: non abbastanza generale, (ed è molto facile dare una soluzione più generale).
Evgeni Sergeev,

4
Questo in realtà è molto utile. quando hai un intervallo significativo, questo può semplificare il calcolo. nel mio caso math.stackexchange.com/questions/2279751/…
M.kazem Akhgary

Esattamente, l'ho usato solo per il calcolo di dayOfWeek (intervallo noto da -6 a +6) e ne ha salvato due %.
NetMage

@EvgeniSergeev +0 per me: non risponde alla domanda OP ma può essere utile in un contesto più specifico (ma ancora nel contesto della domanda)
Erdal G.

1

Ti aspetti un comportamento contrario al comportamento documentato dell'operatore% in c #, probabilmente perché ti aspetti che funzioni in un modo che funzioni in un'altra lingua a cui sei più abituato. La documentazione sugli stati c # (sottolineatura mia):

Per gli operandi di tipi interi, il risultato di a% b è il valore prodotto da a - (a / b) * b. Il segno del resto diverso da zero è uguale a quello dell'operando di sinistra

Il valore desiderato può essere calcolato con un ulteriore passaggio:

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

1

Un'implementazione a riga singola della risposta di dcastro (la più conforme ad altre lingue):

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

Se desideri mantenere l'uso %dell'operatore (non puoi sovraccaricare gli operatori nativi in ​​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 d'uso, entrambi funzionano:

int r = (IntM)a % n;

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

0

Tutte le risposte qui funzionano alla grande se il tuo divisore è positivo, ma non è del tutto completo. Ecco la mia implementazione che ritorna sempre su una gamma di[0, b) , in modo tale che il segno dell'output sia uguale al segno del divisore, consentendo divisori negativi come endpoint per l'intervallo di output.

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

(dove real_tpuò essere qualsiasi tipo di numero)

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.