负数的模运算让我感到困扰

257

我试图对一个整数取模,以便得到一个数组位置,使其能够循环。对于正数来说,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)
< p >这样的

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

我以前做过这个,但出于某种原因,今天它让我感到很棘手:(


请参阅有关http://math.stackexchange.com/questions/519845/modulo-of-a-negative-number的**数学**模数讨论。 - PPC
1
https://blogs.msdn.microsoft.com/ericlippert/2011/12/05/whats-the-difference-remainder-vs-modulus/ 是一篇很棒的文章。 - Samra
15个回答

1

有许多实现mod函数的方法,我认为值得列出所有的方法——至少根据维基百科,我相信还有更多。

// Important to be able to use `MathF`.
using System;

public static class MathFUtils {
    public static class Mod {
        public static float Trunc(float a, float b) =>
            a - b * ((int)(a / b));

        public static float Round(float a, float b) =>
            a - b * MathF.Round(a / b);

        public static float Floor(float a, float b) =>
            a - b * MathF.Floor(a / b);

        public static float Ceil(float a, float b) =>
            a - b * MathF.Ceiling(a / b);

        public static float Euclidean(float a, float b) =>
            a - MathF.Abs(b) * MathF.Floor(a / MathF.Abs(b));
    }
}

根据维基百科(以及我的经验),坚持使用欧几里得。 从数学和概率属性的角度来看,它是最有用的。 如果您需要截断,那么我相信%就可以胜任。

此外,对于那些可能对它们各自的作用及其如何运作感到困惑的人,我强烈建议阅读维基百科文章(即使很难)并查看每个表示形式的图像。

当然,这些不一定是最有效率的,但它们确实可以发挥作用。 如果您担心性能问题,我建议找到本地C#专家,或者当他们穿过我们的世界时询问他们。


1
所有这里的答案都适用于除数为正数的情况,但它并不完整。这是我的实现,总是在范围[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可以是任何数值类型)

这与dcastro的答案有何不同? - ToolmakerSteve
@ToolmakerSteve 看起来是相同的行为,但实现方式不同。 - Aaron Franke
1
好的。就编程而言,或许可以删除或修改第一句话“所有这里的答案都很好,如果你的除数是正数的话,但它并不完整。”,因为它并不准确。在这篇文章被写出来的时候,dCastro的回答已经存在了。也许那时它被埋得相当深... - ToolmakerSteve

0

ShreevatsaR的第二个答案:

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

可以在较新版本的C#中使用var模式和switch表达式以一行代码的方式编写:

int mod(int x, int m) => (x % m) switch 
{ 
    < 0 and var r => r + m, var r => r 
}

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

0

这是我针对正整数的一行代码,基于这个答案

用法:

(-7).Mod(3); // returns 2

实现:

static int Mod(this int a, int n) => (((a %= n) < 0) ? n : 0) + a;

3
我觉得这个有些晦涩,以至于一开始我认为它是错误的。将其转化为单个表达式没有任何好处。它仍然包含一个条件语句,因此不会带来显著的性能优势。在我看来,更好的做法是使用更易于理解的等效表达式:{ a %= n; if (a < 0) a += n; }。或者如果要使用条件语句,最好还是采用 Evgeni 提供的早期链接答案——避免在两个条件分支中进行加操作(并且在我看来更易于理解)。 - ToolmakerSteve

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接