【问题标题】:Rotate right by n only using bitwise operators in C仅使用 C 中的按位运算符向右旋转 n
【发布时间】:2014-02-20 00:25:11
【问题描述】:

我正在尝试仅使用位运算符在 C 中实现 rotateRight by n 函数。

到目前为止,我已经决定使用它。

y = x >> n
z = x << (32 - n)

g = y | z

所以以值11010011为例

如果我尝试使用 `rotateRight(5):

y 变为 11111110

z 变为 01100000

那么g变成111111110

不过正确答案应该是10011110

这几乎可行,但问题是当我需要它来执行逻辑移位时,右移会复制符号位,所以我的一些答案是否定的。我该如何解决这个问题?

注意 我无法使用 castingunsigned types

【问题讨论】:

  • 为什么不能使用无符号? “老师”是我唯一可以接受的理由……
  • 请记住 &gt;&gt; 是为负数 signed int 定义的实现 - 即您不能假设它是算术移位还是逻辑移位,除非您可以保证代码永远不会看到不同的编译器或平台。
  • 0 的转变是否重要?
  • 1) 复制符号位的右移不是逻辑“按位运算符”而是“算术运算符”,因为其功能取决于“符号”2) 当 int 为 32 位时,未定义左移或右移 32。 3) 为了便于携带,int 位大小为sizeof(int)*CHAR_BIT
  • 备案:以编译器友好的方式表达旋转的最佳实践,避免未定义的行为:stackoverflow.com/questions/776508/…

标签: c rotation bit-manipulation


【解决方案1】:

您可以移动无符号值:

y = (int)((unsigned)x >> n);
z = x << (32 - n);
g = y | z;

或者,您可以适当地屏蔽:

y = (x >> n) & ~(-1 << (32 - n));
z = x << (32 - n);
g = y | z;

【讨论】:

  • 谢谢,但很抱歉我应该在原帖中包含,我不能转换或使用无符号类型
  • 你的回答是正确的,正是我想说的,但我想请你详细说明,因为他似乎是一个初学者。我会假设他不理解 shift rightshiftarith algorithm right 之间的区别,并倾向于提供有关它们的文章的链接,并在答案中包含解释intunsigned int 如何在那里发挥作用。
  • 或者,在您允许的情况下,我很乐意在编辑中将此添加到您的答案中。
  • 好点;如果您已经有此类文章的良好链接,请继续进行编辑。谢谢!
  • 值得一提的是 x86 中有一个旋转指令,我查看了 MSVC 在提供这样的代码时生成的程序集,它确实解析为一个旋转指令。
【解决方案2】:

虽然@jlahd 的答案是正确的,但我会尝试简要说明logical shift rightarithmetic shift right 之间的区别(可以找到另一个很好的区别图表here)。

请先阅读链接,如果您仍然感到困惑,请阅读以下内容:

两种不同右移的简要说明

现在,如果您将变量声明为int x = 8;,C 编译器就会知道这个数字是有符号的,并且当您使用这样的移位运算符时:

int x = 8;
int y = -8;
int shifted_x, shifted_y;

shifted_x = x >> 2; // After this operation shifted_x == 2
shifted_y = y >> 2; // After this operation shifted_y == -2

原因是右移表示除以2的幂

现在,我很懒,所以让我在假设的机器上将int 设为 8 位,这样我就可以节省一些写作时间。在二进制 8 和 -8 中看起来像这样:

 8 = 00001000
-8 = 11111000 ( invert and add 1 for complement 2 representation )

但在计算二进制数时,11111000 是十进制的 248。如果我们记得那个变量有一个符号,它只能表示 -8...

如果我们想要保留移位的良好属性,其中移位表示除以 2 的幂(这非常有用)并且我们现在想要有符号数,我们需要进行两种不同类型的右移因为

 248 >> 1 = 124 = 01111100
 -8  >> 1 = -4  = 11111100
// And for comparison
  8  >> 1 =  4  = 00000100

我们可以看到,第一个移位在前面插入了一个 0,而第二个移位插入了一个 1。这是因为有符号数和无符号数之间的差异,在二进制补码表示中,除以 2 的幂时.

为了保持这种精确性,我们为有符号和无符号变量提供了两种不同的右移运算符。在汇编中,您可以显式声明您希望使用哪个,而在 C 中,编译器会根据声明的类型为您决定。

代码泛化

我会以稍微不同的方式编写代码,以使自己至少与平台无关。

#define ROTR(x,n) (((x) >> (n)) | ((x) << ((sizeof(x) * 8) - (n))))
#define ROTR(x,n) (((x) >> (n)) | ((x) << ((sizeof(x) * 8) - (n))))

这稍微好一点,但您仍然必须记住在使用此宏时保持变量无符号。我可以尝试像这样投射宏:

#define ROTR(x,n) (((size_t)(x) >> (n)) | ((size_t)(x) << ((sizeof(x) * 8) - (n))))
#define ROTR(x,n) (((size_t)(x) >> (n)) | ((size_t)(x) << ((sizeof(x) * 8) - (n))))

但现在我假设您永远不会尝试旋转大于size_t的整数...

为了摆脱可能是 1 或 0 的右移的高位,具体取决于编译器选择的移位类型,可以尝试以下操作(满足您的无强制转换要求):

#define ROTR(x,n) ((((x) >> (n)) & (~(0u) >> (n))) | ((x) << ((sizeof(x) * 8) - (n))))
#define ROTR(x,n) ((((x) >> (n)) & (~(0u) >> (n))) | ((x) << ((sizeof(x) * 8) - (n))))

但是对于 long 类型,它不会像预期的那样工作,因为 ~(0u) is of type unsigned int (first type which zero fits in the table) 并因此将我们限制为小于 sizeof(unsigned int) * 8 位的旋转。在这种情况下,我们可以使用~(0ul),但这使它成为unsigned long 类型,这种类型在您的平台上可能效率低下,如果您想传入long long,我们该怎么办?我们需要它与x 的类型相同,我们可以通过像~((x)^(x)) 这样的更神奇的表达式来实现它,但我们仍然需要将它转换为unsigned 版本,所以我们不要去那里。

@MattMcNabb 在 cmets 中还指出了另外两个问题:

  1. 我们的左移操作可能溢出。在对signed 类型进行操作时,即使在实践中它通常是相同的,我们也需要将左移操作中的 x 转换为 unsigned 类型,because it is undefined behavior when an arithmetic shift operation overflows(参见this answer's reference to the standard)。但是如果我们转换它,我们将再次需要为转换选择一个合适的类型,因为它的字节大小将作为我们可以旋转的上限......

  2. 我们假设字节有 8 位。 Which is not always the case, and we should use CHAR_BIT 而不是 8

在这种情况下为什么要打扰?为什么不回到之前的解决方案,只使用the largest integer type, uintmax_t (C99),而不是size_t。但这现在意味着我们可能会在性能上受到惩罚,因为我们可能使用大于处理器字的整数,并且每个算术运算可能只涉及一条以上的汇编指令......不过这里是:

#define ROTR(x,n) (((uintmax_t)(x) >> (n)) | ((uintmax_t)(x) << ((sizeof(x) * CHAR_BIT) - (n))))
#define ROTR(x,n) (((uintmax_t)(x) >> (n)) | ((uintmax_t)(x) << ((sizeof(x) * CHAR_BIT) - (n))))

所以说真的,可能没有完美的方法来做到这一点(至少没有我能想到的)。您可以让它适用于所有类型,也可以通过仅处理等于或小于处理器字的事物来使其快速(消除long long 等)。但这很好而且通用,应该遵守标准......

如果您想要快速算法,那么您需要知道您正在为哪台机器编写代码,否则您将无法优化。

所以最后@jlahd 的解决方案会更好,而我的解决方案可能会帮助您使事情更通用(有代价)。

【讨论】:

  • 这似乎是最干净的,但是我建议在每种情况下都将括号放在“n”周围;因为这会消除一些编译器警告。
  • 是的,你是对的。也可以保持一致。谢谢:)
  • 所有的 8 应该是 CHAR_BIT 。我假设您在这里寻求便携性!
  • 需要将左移中的x设为无符号,否则会导致有符号算术溢出导致未定义行为。
  • @MattMcNabb 你对左移是正确的,但我不喜欢CHAR_BIT。尽管根据this answer,它相当于C99中的一个字节,但它仍然暗示这段代码与char有关,用简单的英语来说,它没有。它处理字节。
【解决方案3】:

我已经在 x86 Linux 上使用 gcc 4.6.3 尝试过您的代码。

y = x >> n
z = x << (32 - n)

g = y | z

这是正确的。如果 x 等于 11010011,那么 rotateRight(5) 将使 y 变为 00000110。">>" 不会添加 1

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-24
    • 2017-03-05
    • 1970-01-01
    • 2021-09-09
    相关资源
    最近更新 更多