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

175 c# math modulo

我试图修改一个整数来获得一个数组位置,以便它循环.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
Run Code Online (Sandbox Code Playgroud)

所以我需要一个实现

int GetArrayIndex(int i, int arrayLength)
Run Code Online (Sandbox Code Playgroud)

这样的

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
Run Code Online (Sandbox Code Playgroud)

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

Shr*_*saR 265

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

int mod(int x, int m) {
    return (x%m + m)%m;
}
Run Code Online (Sandbox Code Playgroud)

当然,如果你对两次调用模数运算感到困扰,你可以把它写成

int mod(int x, int m) {
    int r = x%m;
    return r<0 ? r+m : r;
}
Run Code Online (Sandbox Code Playgroud)

或其变体.

它起作用的原因是"x%m"总是在[-m + 1,m-1]的范围内.因此,如果它是负数,则向其添加m将使其处于正范围而不更改其模数m的值.

  • 注意:为了完整的数论完整性,您可能希望在顶部添加一行"if(m <0)m = -m;" 虽然在这种情况下并不重要,因为"arrayLength"可能总是正面的. (7认同)
  • +1.我不关心任何单个语言对负模数的作用 - "最小非负残差"表现出数学规律性并消除任何歧义. (7认同)
  • @RuudLenders:No.如果x = -5且m = 2,那么`r = x%m`是`-1`,之后`r + m`是'1`.不需要while循环.关键是(正如我在答案中所写),`x%m`总是严格大于`-m`,所以你需要最多添加一次`m`以使其为正. (5认同)
  • 如果要检查m的值,则还应排除零. (4认同)
  • @billpg:mod没有为m = 0定义,所以实际上没有任何函数可以用于该情况.恕我直言,调用者有责任检查.(没有人应该*想要*mod 0.)OTOH,mod*是*为负m定义的,所以我建议在代码中修复该bug,如果函数可以用负m调用.无论如何,应该进行错误检查/处理是一个长期存在的问题:p (3认同)
  • @dcastro:我*希望* -12 mod -10为8。这是数学上最常见的惯例,如果为a取模b代表代表性的r,则0≤ r &lt;| b |。 (3认同)
  • 对于它的价值,对这个答案的首部列出的两个答案进行了快速的超级迭代,高分辨率计时器测试,并且"返回(x%m + m)%m;`之间具有轻微,一致的性能优势这两个,在我的系统上使用C#,.NET CLR 4.0. (3认同)
  • 顺便提一下,世界上基本上没有任何情况下一次调用 mod 和一个分支会比两次调用 mod 快。 (2认同)
  • @BrettHale 同意。它对于循环缓冲区也很有用。即`index =index_of(value,values); value =values[(index - 1) %values.length];` 假设您不能对数组进行负索引,如果模运算符可以返回负数,则会崩溃。 (2认同)
  • @KeithIrwin:我对预先生成的随机数数组进行了重新测试,结果相同,只是差异稍小一些。 (2认同)

Пет*_*ров 74

请注意,C#和C++的%运算符实际上不是模数,它是余数.在您的情况下,您想要的模数公式为:

float nfmod(float a,float b)
{
    return a - b * floor(a / b);
}
Run Code Online (Sandbox Code Playgroud)

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

  • "请注意,C++的%运算符实际上不是模数,它是余数."谢谢,它现在有意义,总是想知道为什么它从来没有与负数一起正常工作. (17认同)
  • Tyress,模数和余数之间存在差异.例如:`-21 mod 4是3,因为-21 + 4 x 6是3.`但是`-21除以4得到-5`,其余为-1.对于正值,没有区别.所以请告知自己这些差异.并且不要一直信任维基百科:) (14认同)
  • "请注意,C++的%运算符实际上不是模数,它是余数."我不认为这是准确的,我不明白为什么模数与余数有任何不同.这就是它在Modulo Operation Wikipedia页面上所说的内容.只是编程语言对负数的处理方式不同.C#中的模运算符显然将余数从"零"(-9%4 = -1,因为4*-2是-8,差值为-1),而另一个定义将-9%4视为+3,因为-4*3是-12,余数为+3(例如在谷歌的搜索功能中,不确定那里的后端语言). (2认同)
  • 为什么有人要使用余数函数而不是取模?他们为什么要使%剩余? (2认同)
  • @AaronFranke-它是早期cpus的遗留物,具有除法硬件,可以快速产生商和余数-这就是该硬件确实带来了负红利。该语言简单地反映了硬件。大多数时候,程序员都在以正的红利工作,而忽略了这个怪癖。速度至关重要。 (2认同)

Evg*_*eev 13

%仅使用一次的单行实现:

int mod(int k, int n) {  return ((k %= n) < 0) ? k+n : k;  }
Run Code Online (Sandbox Code Playgroud)

  • @JohnDemetriou你的数字都是错的:(A)它应该返回2而(B)它返回2; 尝试运行代码.项目(A):手动找到`mod(-10,6)`,你要么重复加6,要么直到答案在[[0,6]`范围内.这种表示法的意思是"包含在左边,在右边独占".在我们的例子中,我们添加了两次,得到2.代码很简单,并且很容易看出它是正确的:首先,它相当于上面添加/减去`n`,除了它停止一个`n "简短,如果从消极方面接近.在那种情况下我们修复它.有:评论:) (3认同)
  • 但是,单个“%”是否比测试和跳转更昂贵,尤其是在无法轻易预测的情况下? (2认同)

lil*_*lo0 9

比较两个主要答案

(x%m + m)%m;
Run Code Online (Sandbox Code Playgroud)

int r = x%m;
return r<0 ? r+m : r;
Run Code Online (Sandbox Code Playgroud)

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


Abi*_*hew 6

增加一些了解.

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

例如:

 int n = 5;
 int x = -3;

 int mod(int n, int x)
 {
     return ((n%x)+x)%x;
 }
Run Code Online (Sandbox Code Playgroud)

输出:

 -1
Run Code Online (Sandbox Code Playgroud)

  • 我很困惑...你说结果应该总是正面的,但是然后将输出列为"-1"? (15认同)

Ren*_*Pet 5

我喜欢 Peter N Lewis 在这个线程中提出的技巧:“如果 n 的范围有限,那么您只需添加一个已知的[除数]常量倍数即可得到您想要的结果,该倍数大于除数的绝对值最低限度。”

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

d % 180f
Run Code Online (Sandbox Code Playgroud)

如果d为负数,我想避免出现问题,那么我只需这样做:

(d + 720f) % 180f
Run Code Online (Sandbox Code Playgroud)

这假设虽然d可能为负数,但已知它永远不会比 -720 更负。

  • 这实际上非常有帮助。当你有有意义的范围时,这可以简化计算。就我而言https://math.stackexchange.com/questions/2279751/how-to-simplify-2-modular-operators (8认同)
  • -1:不够通用(并且很容易给出更通用的解决方案)。 (2认同)
  • @EvgeniSergeev +0 对我来说:不回答OP问题,但在更具体的上下文中可能会有所帮助(但仍在问题的上下文中) (2认同)

dca*_*tro 5

ShreevatsaR的答案不适用于所有情况,即使您添加"if(m <0)m = -m;",如果您考虑负红利/除数.

例如,-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;
    }
}
Run Code Online (Sandbox Code Playgroud)

使用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 };
        }
    }
Run Code Online (Sandbox Code Playgroud)

  • (... contd)其次,如何处理负模数是一个常规问题.参见[例如维基百科](https://en.wikipedia.org/w/index.php?title=Modulo_operation&oldid=588101238#Remainder_calculation_for_the_modulo_operation)."通常,在数论中,总是选择正余数",这也是我学习它的方法(在Burton的*初等数论*中).Knuth也以这种方式定义它(具体地说,`r = a - b floor(a/b)`总是正的).即使在计算机系统中,例如Pascal和Maple,也要将其定义为始终为正. (3认同)
  • 首先,“mod”函数通常以正模数调用(请注意此处回答的原始问题中的变量“arrayLength”,它可能永远不会为负),因此该函数实际上并不需要工作为负模量。(这就是为什么我在对我的答案的评论中提到负模量的处理,而不是在答案本身中。)(续...) (2认同)
  • 同样,[this](https://en.wikipedia.org/w/index.php?title=Modulo_operation&amp;oldid=764138354) 仍然是一本好书。“始终为正”的定义(我的答案)与 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 程序集等。我真的不明白你怎么能声称一个约定是“正确的”而其他约定是“错误的”。 (2认同)

sta*_*lue 4

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