【问题标题】:Python-style integer division & modulus in CC中的Python样式整数除法和模数
【发布时间】:2010-10-24 02:34:37
【问题描述】:

在 Python 和 Ruby 中,有符号整数除法向负无穷大截断,有符号整数模数与第二个操作数的符号相同:

>>> (-41) / 3
-14
>>> (-41) % 3
1

但是,在 C 和 Java 中,有符号整数除法会向 0 截断,并且有符号整数模与第一个操作数具有相同的符号:

printf("%d\n", (-41) / 3); /* prints "-13" */
printf("%d\n", (-41) % 3); /* prints "-2" */

在 C 语言中执行与 Python 和 Ruby 相同类型的除法和模数的最简单和最有效的方法是什么?

【问题讨论】:

  • 同样的事情发生在 JavaScript 中:(-41) % 3 === -2

标签: java c++ c modulo division


【解决方案1】:

这个问题是关于如何模拟 Python 风格的整数除法和取模。这里给出的所有答案都假设这个运算的操作数本身就是整数,但是 Python 也可以使用浮点数进行模运算。因此,我认为以下答案可以更好地解决问题:

#include <stdlib.h>
#include <stdio.h>
#include <math.h>

int pydiv(double a, double b) {
    int q = a/b;
    double r = fmod(a,b);
    if ((r != 0) && ((r < 0) != (b < 0))) {
        q -= 1;
    }
    return q;
}

int main(int argc, char* argv[])
{
    double a = atof(argv[1]);
    double b = atof(argv[2]);
    printf("%d\n", pydiv(a, b));
}

对于模数:

#include <stdlib.h>
#include <stdio.h>
#include <math.h>

double pymod(double a, double b) {
    double r = fmod(a, b);
    if (r!=0 && ((r<0) != (b<0))) {
        r += b;
    }
    return r;
}

int main(int argc, char* argv[])
{
    double a = atof(argv[1]);
    double b = atof(argv[2]);
    printf("%f\n", pymod(a, b));
}

我使用以下测试代码针对 Python 的行为方式测试了上述两个程序:

#!/usr/bin/python3
import subprocess
subprocess.call(["cc", "pydiv.c", "-lm", "-o", "cdiv"])
subprocess.call(["cc", "pymod.c", "-lm", "-o", "cmod"])
def frange(start, stop, step=1):
    for i in range(0, int((stop-start)/step)):
        yield start + step*i
for a in frange(-10.0, 10.0, 0.25):
    for b in frange(-10.0, 10.0, 0.25):
        if (b == 0.0):
            continue
        pydiv = a//b
        pymod = a%b
        cdiv = int(subprocess.check_output(["./cdiv", str(a), str(b)]))
        cmod = float(subprocess.check_output(["./cmod", str(a), str(b)]))
        if pydiv != cdiv:
            exit(1)
        if pymod != cmod:
            exit(1)

上面将比较 Python 除法和取模的行为与 C 我在 6320 个测试用例上展示的实现。由于比较成功, 我相信我的解决方案正确地实现了 Python 的行为 各自的操作。

【讨论】:

    【解决方案2】:

    这个问题的解决方案比已经提出的解决方案要短得多(在代码中)。我将使用 Ville Laurikari 的回答格式:

    int py_div(int a, int b)
    {
        return (a - (((a % b) + b) % b)) / b);
    }
    
    int py_mod(int a, int b)
    {
        return ((a % b) + b) % b;
    }
    

    不幸的是,上述解决方案似乎效果不佳。当将此解决方案与 Ville Laurikari 的解决方案进行基准比较时,很明显该解决方案的执行速度只有一半。

    教训是:虽然分支指令使代码变慢,但除法指令更糟糕!

    我认为我发布这个解决方案只是为了它的优雅。

    【讨论】:

    • 任何指向为什么有效的指针,无论是证明还是直观的解释?
    • @EmilioMBumachar 尝试将值插入表达式,您会看到。例如,对于a=-1b=3((-1%3)+3)%3=2,但对于a=1b=3((1%3)+3)%3=1,正如预期的那样。
    【解决方案3】:

    下面是 C89 中下限除法和模数的简单实现:

    #include <stdlib.h>
    
    div_t div_floor(int x, int y)
    {
        div_t r = div(x, y);
        if (r.rem && (x < 0) != (y < 0)) {
            r.quot -= 1;
            r.rem  += y;
        }
        return r;
    }
    

    这里使用div,因为它有well-defined behavior

    如果您使用的是 C++11,这里是下除法和模数的模板化实现:

    #include <tuple>
    
    template<class Integral>
    std::tuple<Integral, Integral> div_floor(Integral x, Integral y)
    {
        typedef std::tuple<Integral, Integral> result_type;
        const Integral quot = x / y;
        const Integral rem  = x % y;
        if (rem && (x < 0) != (y < 0))
            return result_type(quot - 1, rem + y);
        return result_type(quot, rem);
    }
    

    在 C99 和 C++11 中,您可以避免使用 div,因为 C 中的除法和取模行为不再依赖于实现。

    【讨论】:

      【解决方案4】:

      旧的 C 标准中没有指定带符号整数除法的舍入方向。但是,在 C99 中,它被指定为向零舍入。

      这是适用于所有版本的 C 标准和 CPU 架构的可移植代码:

      int py_div(int a, int b)
      {
        if (a < 0)
          if (b < 0)
            return -a / -b;
          else
            return -(-a / b) - (-a % b != 0 ? 1 : 0);
        else if (b < 0)
            return -(a / -b) - (a % -b != 0 ? 1 : 0);
          else
            return a / b;
      }
      
      int py_mod(int a, int b)
      {
        if (a < 0)
          if (b < 0)
            return -(-a % -b);
          else
            return -a % b - (-a % -b != 0 ? 1 : 0);
        else if (b < 0)
            return -(a % -b) + (-a % -b != 0 ? 1 : 0);
          else
            return a % b;
      }
      

      我做了一些肤浅的测试,它似乎给出了与 Python 相同的结果。这段代码可能效率不高,但一个好的 C 编译器可能会对其进行充分优化,尤其是当您将代码作为静态函数放在头文件中时。

      您可能还想看看这个密切相关的问题:Integer division rounding with negatives in C++

      【讨论】:

      • 如果你想高效地使用查找表。如果这段代码是一个效率问题,唯一真正的选择是使用常规的 / 和 % 运算符并使用它们的舍入。
      • 这很整洁。在这段代码中有一些大括号会很有帮助(有那么多条件嵌套,很难说出发生了什么……)
      • 当被除数或除数为负时,此代码不会产生正确的模数。
      • @VilleLaurikari:不应该是b : 0,而不是1 : 0吗?这似乎对我有用:` int py_mod(int a, int b) { if ((a >= 0) == (b >= 0)) return a % b;否则返回 (a % -b) + ((a % -b) != 0 ? b : 0); }`
      • 注意! py_moda = -1611640551, b = 2 给出错误答案。 Python 返回1,而答案中的 sn-p 给出0
      【解决方案5】:

      对于模,我发现以下最简单。实现的符号约定是什么并不重要,我们只是将结果强制转换为我们想要的符号:

      r = n % a;
      if (r < 0) r += a;
      

      显然这是为了积极的 a。对于负数,您需要:

      r = n % a;
      if (r > 0) r += a;
      

      哪个(可能有点令人困惑)结合起来给出以下内容(在 C++ 中。在 C 中,用 int 做同样的事情,然后冗长地写一个 long long 的副本):

      template<typename T> T sign(T t) { return t > T(0) ? T(1) : T(-1); }
      
      template<typename T> T py_mod(T n, T a) {
          T r = n % a;
          if (r * sign(a) < T(0)) r += a;
          return r;
      }
      

      我们可以使用cheapskate 二值“符号”函数,因为我们已经知道a!=0,否则% 将是未定义的。

      对除法应用相同的原则(看输出而不是输入):

      q = n / a;
      // assuming round-toward-zero
      if ((q < 0) && (q * a != n)) --q;
      

      可以说,乘法可能比必要的成本更高,但如果需要,以后可以在每个架构的基础上进行微优化。例如,如果您有一个除法运算,它可以为您提供商和余数,那么您将按除法排序。

      [编辑:可能有一些边缘情况会出错,例如,如果商或余数是 INT_MAX 或 INT_MIN。但是为大值模拟 python 数学无论如何都是另一个问题;-)]

      [另一个编辑:标准的python实现不是用C写的吗?您可以搜索他们所做的事情的来源]

      【讨论】:

        【解决方案6】:

        它深入研究了浮点数的丑陋世界,但这些在 Java 中给出了正确的答案:

        public static int pythonDiv(int a, int b) {
            if (!((a < 0) ^ (b < 0))) {
                return a / b;
            }
            return (int)(Math.floor((double)a/(double)b));
        }
        
        public static int pythonMod(int a, int b) {
            return a - b * pythonDiv(a,b);
        }
        

        我不断言它们的效率。

        【讨论】:

        • 虽然这个特定实例可能会为所有可能的输入产生正确的结果,但我仍然会避免它,因为它使用浮点函数进行积分运算。如果将float 替换为doublelong 替换int,则会对某些输入产生错误的结果。此外,如果您将此实例移植到 int 为 64 位宽的平台上的 C 或 C++,它同样会为某些输入产生错误的结果。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-07-19
        • 2021-12-14
        • 2012-12-30
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多