负数的模正在融化我的大脑


188

我正在尝试修改一个整数以获得数组位置,以便它将循环。i % arrayLength对于正数,这样做很好,但对于负数,一切都会出错。

 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

所以我需要一个实现

int GetArrayIndex(int i, int arrayLength)

这样

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

我以前做过,但是由于某种原因,今天它融化了我的大脑:(


请参阅math.stackexchange.com/questions/519845/…上有关数学模数的讨论
PPC

Answers:


281

我总是使用自己的mod函数,定义为

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

当然,如果您不满意两次调用模数运算,可以将其写为

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

或其变体。

它起作用的原因是“ x%m”始终在[-m + 1,m-1]范围内。因此,如果它完全为负,则将其添加到m会将其置于正范围内,而不会更改其对m取模的值。


7
注意:为了获得完整的数字理论上的完整性,您可能需要在顶部添加一行“ if(m <0)m = -m;”。尽管在这种情况下,“ arrayLength”始终为正并不重要。
ShreevatsaR

4
如果要检查m的值,则还应排除零。
billpg

6
@RuudLenders:否。如果x = -5且m = 2,r = x%m则为-1,其后r+m1。不需要while循环。关键是(正如我在回答中所写)x%m始终严格大于-m,因此您m最多需要添加一次以使其为正。
ShreevatsaR 2013年

4
@dcastro:我想要-12 -10 MOD为8。这是在数学中最常见的惯例是,如果选择一个代表rab,那么它是这样的,0≤[R <| B |。
ShreevatsaR 2014年

8
+1。我不在乎任何单个语言对负模的作用-“最小非负残差”表现出数学规律性并消除了任何歧义。
Brett Hale 2014年

79

请注意,C#和C ++的%运算符实际上不是模数,而是余数。在您的情况下,所需的取模公式为:

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

您必须使用C#(或C ++)重新编码,但这是获得模数而不是余数的方式。


21
“请注意,C ++的%运算符实际上不是模数,而是余数。”谢谢,现在有意义了,总是想知道为什么它永远不能正确地用于负数。
leetNightshade

2
“请注意,C ++的%运算符实际上不是模,而是余数。”我不认为这是准确的,而且我也看不出为什么模与余数没有任何区别。这也是Modulo Operation Wikipedia页面上所说的。只是编程语言对负数的处理方式有所不同。C#中的模运算符显然会从“零”开始计数余数(-9%4 = -1,因为4 * -2是-8,相差-1),而另一个定义会将-9%4视为+3,因为-4 * 3为-12,其余为+3(例如在Google的搜索功能中,不确定那里的后端语言)。
泰勒斯

18
Tyress,模数和余数之间存在差异。例如: -21 mod 4 is 3 because -21 + 4 x 6 is 3. 但是-21 divided by 4 gives -5带有remainder of -1。对于正值,没有区别。因此,请告知自己有关这些差异的信息。不要相信维基百科所有的时间:)
ПетърПетров

2
为什么有人要使用余数函数而不是取模?他们为什么要%剩余?
亚伦·弗兰克

4
@AaronFranke-它是早期cpus的遗留物,具有除法硬件以快速产生商和余数-这就是该硬件确实带来了负红利。该语言简单地反映了硬件。大多数时候,程序员都在以积极的回报工作,而忽略了这个怪癖。速度至关重要。
制造商史蒂夫

15

单行实现%仅使用一次:

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

1
这样对吗?因为我不认为任何人都接受它,也没有对此发表任何评论。例如。mod(-10,6)将返回6。正确吗?它不应该返回4吗?
约翰·德米特里

3
@JohnDemetriou您的数字都错了:(A)它应该返回2,并且(B)它确实返回2;尝试运行代码。项目(A):要mod(-10, 6)手动查找,请重复添加或减去6,直到答案在范围内[0, 6)。该表示法的意思是“在左侧包含,在右侧包含”。在我们的例子中,我们将6加两次,得到2。代码很简单,很容易看出来是对的:首先,它等效n于上面的加/减,但是n如果从消极的一面。在这种情况下,我们将其修复。那里:评论:)
Evgeni Sergeev

1
顺便说一句,这就是为什么使用单个%可能是一个好主意的原因。请参阅编写更快的托管代码:知道什么东西成本中的表,查看托管代码中的东西成本。使用表中的%费用int div与表中列出的费用类似:大约是加法或减法的36倍,是乘法法的13倍。当然,除非这是代码的工作核心,否则没什么大不了的。
Evgeni Sergeev

2
但是,是否有一个%比测试和跳转更昂贵的工具,尤其是如果无法轻易预测的话?
Medinoc

6

即使您加上“ if(m <0)m = -m;”,即使您考虑了负红利/除数,ShreevatsaR的答案也并非在所有情况下都有效。

例如,-12 mod -10将为8,并且应为-2。

以下实现适用于正和负红利/除数,并符合其他实现(即Java,Python,Ruby,Scala,Scheme,Javascript和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;
    }
}

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

首先,mod通常以正模数调用函数(请注意,arrayLength在此处回答的原始问题中的变量,该变量可能永远不会为负),因此对于负模数,实际上不需要使该函数起作用。(这就是为什么我在对答案的评论中而不是在答案本身中提及负模的原因。)(续...)
ShreevatsaR 2014年

3
(...继续)其次,对于负模量应该做些惯例。参见例如维基百科。“通常,在数论中,总是选择正余数”,这也是我学到的方式(在伯顿基本数论中)。Knuth也以这种方式定义它(特别r = a - b floor(a/b)是始终为正)。即使在计算机系统中,例如Pascal和Maple,也将其定义为始终为正。
ShreevatsaR 2014年

@ShreevatsaR我知道欧几里得的定义指出结果总是正的-但是我给人的印象是,大多数现代mod实现都会为负除数“ n”返回[n + 1,0]范围内的值,这意味着-12 mod -10 = -2。Ive调查了Google CalculatorPythonRubyScala,他们都遵循了这一约定。
dcastro 2014年

另外,要添加到列表中:方案Javascript
dcastro 2014年

1
同样,仍然是一本好书。“总是肯定的”定义(我的答案)与ALGOL,Dart,Maple,Pascal,Z3等一致。“除数的符号”(此答案)与以下各项一致:APL,COBOL,J,Lua,Mathematica,MS Excel,Perl,Python,R,Ruby,Tcl等。两者均与“分红标志”不一致,例如:AWK,bash,bc,C99,C ++ 11,C#,D,Eiffel,Erlang,Go,Java ,OCaml,PHP,Rust,Scala,Swift,VB,x86程序集等。我真的看不到如何宣称一个约定是“正确的”而其他的则是“错误的”。
ShreevatsaR

6

增加一些理解。

根据欧几里得的定义,mod结果必须始终为正。

例如:

 int n = 5;
 int x = -3;

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

输出:

 -1

15
我很困惑...您说结果应该始终为正,但是将输出列为-1
杰夫·B

@JeffBridgman我已经基于欧几里得定义进行了说明。`对于余数,有两个可能的选择,一个为负,另一个为正,并且商也有两个可能的选择。通常,在数论中the positive remainder is always chosen,但是编程语言是根据语言和a和/或n的符号来选择的。[5] 标准Pascal和Algol68的给予了积极的余数(或0),甚至是负的除数,和一些编程语言,如C90,当离开它的实现无论是正还是为negative.`
阿宾

5

比较两个主要答案

(x%m + m)%m;

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

实际上,没有人提到第一个可能抛出OverflowException一阵子而第二个则不会抛出的事实。更糟糕的是,如果使用默认的未经检查的上下文,则第一个答案可能会返回错误的答案(mod(int.MaxValue - 1, int.MaxValue)例如,请参见)。因此,第二个答案不仅看起来更快,而且更正确。


4

只需将您的模数(arrayLength)加到%的负结果上,您就可以了。


4

对于更注重性能的开发人员

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

性能比较

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

至于转换为uint的性能成本,请看这里


3
方法-3 % 10应该为-3或7。由于需要非负结果,因此7是答案。您的实现返回3。您应该将两个参数都更改为uint并删除强制类型转换。
我喜欢旧的Stack Overflow

5
无符号算术仅当n为2的幂时才是等效的,在这种情况下(uint)k & (n - 1),如果编译器尚未为您做这件事,则您可以简单地使用逻辑和()(编译器通常足够聪明来解决这个问题)。
j_schultz

2

我喜欢Peter N Lewis在此线程上提出的技巧:“如果n的范围有限,则只需添加一个已知的[除数]的常数倍,即大于该整数的绝对值,就可以得到所需的结果。最低。”

所以如果我有一个以度为单位的值d,我想

d % 180f

并且我想避免如果d为负的问题,那么我只是这样做:

(d + 720f) % 180f

这假定尽管d可能为负,但已知它永远不会比-720更负。


2
-1:不够一般,(给出更一般的解决方案很容易)。
Evgeni Sergeev 2014年

4
这实际上非常有帮助。当您具有有意义的范围时,这可以简化计算。在我的情况math.stackexchange.com/questions/2279751/...
M.kazem Akhgary

确实,仅将其用于dayOfWeek计算(已知范围为-6至+6),并保存了两个%
NetMage

@EvgeniSergeev +0对我来说:不回答OP问题,但可以在更具体的情况下有所帮助(但仍在问题的范围内)
Erdal G.

1

您期望的行为与c#中记录的%运算符的行为相反-可能是因为您期望它以某种您更习惯的另一种语言工作的方式起作用。有关C#状态的文档(重点是我的):

对于整数类型的操作数,%b的结果是a-(a / b)* b产生的值。非零余数的符号与左操作数的符号相同

您可以通过一个额外的步骤来计算所需的值:

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

0

如果您的除数是肯定的,那么这里所有的答案都很好,但是还不够完整。这是我的实现,总是在以下范围内返回[0, b)使输出的符号与除数的符号相同,从而允许使用负除数作为输出范围的端点。

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

real_t可以是任何数字类型)


0

dcastro答案的单行实现(与其他语言最兼容):

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

如果您想继续使用%运算符(不能在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);
}

用例,都可以:

int r = (IntM)a % n;

// Or
int r = a % n(IntM);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.