负数如何取模?

599 阅读1分钟

偶然发现一个有意思的问题:负数如何取模?

乍一看: 这不是很简单?

但仔细一想,我觉得很简单的只是正数准确的说是正整数的取模。

也就是 7 % 3 = 1

(-7) % 37 % (-3) 以及 (-7) % (-3) 呢?

仔细一想,不会!

经过一番摸索之后发现:

In mathematics, the result of the modulo operation is an equivalence class, and any member of the class may be chosen as representative; however, the usual representative is the least positive residue, the smallest non-negative integer that belongs to that class (i.e., the remainder of the Euclidean division). However, other conventions are possible. Computers and calculators have various ways of storing and representing numbers; thus their definition of the modulo operation depends on the programming language or the underlying hardware.

what?

也就是说,模的取值还不止一种可能,一般是最小的正余数。

在几乎所有的编程语言中,a÷na \div n 的商 qq 和余数 rr 满足如下定义:

qZ,q \in Z,
a=nq+r,a = nq + r,
r<n|r| < |n|

其中, ZZ 是整数集合。

发现了吗?这个定义是有歧义的:

  • 如果余数为 0,自然皆大欢喜
  • 如果余数非 0,那么就有两种选择,正余数和负余数

这在被除数和除数都是正数时还不明显,一般没有人会强行将 7÷3=3..27 \div 3 = 3.. -2 的吧?

其实余数的关键在于商的选取,对于商 qq 的选取,不同的编程语言实现不一致,主要有以下几种:

  1. floored division,就是商采用向下取整,趋负无穷截尾
q=anq = \left \lfloor \frac{a}{n} \right \rfloor

其中,ab\left \lfloor \frac{a}{b} \right \rfloor 表示向下取整,因此

r=ananr = a - n \left \lfloor \frac{a}{n} \right \rfloor

余数符号与除数 nn 相同

  1. truncated division,就是商尽可能的靠近 0,因此又称截断取整
q=trunc(an)q = trunc(\frac{a}{n})

其中,trunctrunc 是截断函数。因此,

r=an trunc(an)r = a - n \space trunc(\frac{a}{n})

余数符号与被除数 aa 相同。

  1. Euclidean division,就是欧几里得除法
q=sgn(n)an={an,if n>0an,if n<0q = sgn(n)\left \lfloor \frac{a}{|n|} \right \rfloor = \begin{cases} \left \lfloor \frac{a}{n} \right \rfloor, & \text{if } n > 0 \\ \left \lceil \frac{a}{n} \right \rceil, & \text{if } n < 0 \end{cases}

其中,cd\left \lceil \frac{c}{d} \right \rceil 是向上取整 sgnsgn 是取符号函数, 因此,r0r \ge 0

r=an anr = a - |n| \space \left \lfloor \frac{a}{|n|} \right \rfloor
  1. 其他的像 rounded division(四舍五入) 和 ceiling division (向上取整)可以在这里了解

比如在 Rust 中,对于 % 就是采用的 truncated division

因此,在 Rust 中,

(-7) % 3 就是 (7)3×trunc(73)=1 (-7) - 3 \times trunc(\frac{-7}{3}) = -1

7 % (-3) 就是 7(3)×trunc(73)=1 7 - (-3) \times trunc(\frac{7}{-3}) = 1

当然,在 Rust 中,也有 Euclidean division 的求模方法:rem_euclid()

(-7i32).rem_euclid(3) 就是 (7)3×73=2(-7) - 3 \times \left \lfloor \frac{-7}{3} \right \rfloor = 2

7i32.rem_euclid(-3) 就是 73×73=17 - |-3| \times \left \lfloor \frac{7}{|-3|} \right \rfloor = 1

JavaScript 中,则只有 % 一种求模方法,跟 C++ 一样是 truncated division

Haskell 中, mod 是采用 floored division

(-7) `mod` 3   --  2
7 `mod` (-3)   --  -2

rem是采用 truncated division

(-7) `rem` 3   --  -1
7 `rem` (-3)   --  1

总结

主流编程语言中,除法主要有三种方式取余数,floored divisiontruncated division 以及 Euclidean division。不同的实现方式可能会产生不同的结果,在进行负数的模运算时尤其要注意。