29

对于任何受范围R = [ x , y ] 限制的整数输入W,由于没有更好的术语,W超过R的“溢出”是。如果W超过y,这将导致它回绕。W % (y-x+1) + x

作为这个原则的一个例子,假设我们遍历一个日历的月份:

int this_month = 5;
int next_month = (this_month + 1) % 12;

其中两个整数都在 0 到 11 之间,包括 0 和 11。因此,上面的表达式将整数“钳制”在R = [0,11] 的范围内。这种使用表达式的方法简单、优雅且有利,因为它省略了分支

现在,如果我们想做同样的事情,但反过来呢?以下表达式有效:

int last_month = ((this_month - 1) % 12 + 12) % 12;

但它很深奥。怎样才能美化?


tl;dr - 表达式((x-1) % k + k) % k可以进一步简化吗?

注意:指定 C++ 标记是因为其他语言以不同方式处理模运算符的负操作数。

4

6 回答 6

37

你的表情应该是((x-1) + k) % k。这将正确地将 x=0 环绕到 11。通常,如果您想后退超过 1,则需要确保添加足够多的内容,以使模运算的第一个操作数 >= 0。

这是 C++ 中的一个实现:

int wrapAround(int v, int delta, int minval, int maxval)
{
  const int mod = maxval + 1 - minval;
  if (delta >= 0) {return  (v + delta                - minval) % mod + minval;}
  else            {return ((v + delta) - delta * mod - minval) % mod + minval;}
}

这也允许使用标记为从 0 到 11 或从 1 到 12 的月份,min_valmax_val相应地进行设置。

由于这个答案非常受欢迎,这里有一个没有分支的改进版本,它也处理初始值v小于的情况minval。我保留另一个示例,因为它更容易理解:

int wrapAround(int v, int delta, int minval, int maxval)
{
  const int mod = maxval + 1 - minval;
  v += delta - minval;
  v += (1 - v / mod) * mod;
  return v % mod + minval;
}

剩下的唯一问题是 ifminval大于maxval。如果需要,请随意添加断言。

于 2016-09-28T06:51:55.527 回答
7

k % k将始终为 0。我不是 100% 确定您要做什么,但似乎您希望将上个月限制在 0 到 11 之间(含)。

(this_month + 11) % 12

应该够了。

于 2013-02-09T06:09:03.087 回答
5

一般的解决方案是编写一个函数来计算你想要的值:

//Returns floor(a/n) (with the division done exactly).
//Let ÷ be mathematical division, and / be C++ division.
//We know
//    a÷b = a/b + f (f is the remainder, not all 
//                   divisions have exact Integral results)
//and
//    (a/b)*b + a%b == a (from the standard).
//Together, these imply (through algebraic manipulation):
//    sign(f) == sign(a%b)*sign(b)
//We want the remainder (f) to always be >=0 (by definition of flooredDivision),
//so when sign(f) < 0, we subtract 1 from a/n to make f > 0.
template<typename Integral>
Integral flooredDivision(Integral a, Integral n) {
    Integral q(a/n);
    if ((a%n < 0 && n > 0) || (a%n > 0 && n < 0)) --q;
    return q;
}

//flooredModulo: Modulo function for use in the construction
//looping topologies. The result will always be between 0 and the
//denominator, and will loop in a natural fashion (rather than swapping
//the looping direction over the zero point (as in C++11),
//or being unspecified (as in earlier C++)).
//Returns x such that:
//
//Real a = Real(numerator)
//Real n = Real(denominator)
//Real r = a - n*floor(n/d)
//x = Integral(r)
template<typename Integral>
Integral flooredModulo(Integral a, Integral n) {
    return a - n * flooredDivision(a, n);
}
于 2013-02-09T06:16:02.480 回答
4

Easy Peasy,不要使用第一个模块运算符,它是多余的:

 int last_month = (this_month - 1 + 12) % 12;

这是一般情况

在这种情况下,您可以编写11,但我仍然会这样做,-1 + 11因为它更清楚地说明了您想要实现的目标。

于 2017-10-11T13:53:02.653 回答
3

请注意,普通 mod 会导致模式0...11在 , 等处重复12...2324...35但不会在-11...-1. 换句话说,它有两组行为。一个来自-infinity...-1,一组不同的行为来自0...infinity

该表达式((x-1) % k + k) % k修复-11...-1但与普通 mod 具有相同的问题-23...-12。即,虽然它修复了 12 个额外的数字,但它不会无限循环。它仍然具有来自 的一组行为-infinity...-12和来自 的不同行为-11...+infinity

这意味着如果您使用该函数进行偏移,它可能会导致错误的代码。

如果你想要一个真正的环绕式模组,它应该-infinity...infinity以完全相同的方式处理整个范围。

可能有更好的方法来实现这一点,但这里有一个易于理解的实现:

// n must be greater than 0
func wrapAroundMod(a: Int, n: Int) -> Int {
    var offsetTimes: Int = 0

    if a < 0 {
        offsetTimes = (-a / n) + 1
    }

    return (a + n * offsetTimes) % n
}
于 2020-02-14T00:42:24.567 回答
1

不确定你是否和我有同样的问题,但我的问题本质上是我想将所有数字限制在一定范围内。假设范围是 0-6,因此使用 %7 意味着任何高于 6 的数字都将回绕到 0 或更高。实际的问题是小于零的数字没有回到 6。我有一个解决方案(其中 X 是数字范围的上限,0 是最小值):

if(inputNumber <0)//If this is a negative number
{
(X-(inputNumber*-1))%X; 
}
else
{
inputNumber%X;
}
于 2016-07-26T14:53:50.353 回答