21

来自编码上的“签名类型” -协议缓冲区-谷歌代码

ZigZag 编码将有符号整数映射到无符号整数,因此具有较小绝对值(例如 -1)的数字也具有较小的 varint 编码值。它以一种通过正整数和负整数来回“曲折”的方式来执行此操作,因此 -1 被编码为 1,1 被编码为 2,-2 被编码为 3,依此类推,就像你可以在下表中看到:

Signed Original  Encoded As
0                0
-1               1
1                2
-2               3
2147483647       4294967294
-2147483648      4294967295

换句话说,每个值 n 使用编码

(n << 1) ^ (n >> 31)

对于 sint32s,或

(n << 1) ^ (n >> 63)

对于 64 位版本。

表中(n << 1) ^ (n >> 31)的等于什么?我知道这对积极因素有用,但是对于-1来说,这如何工作?不会 -11111 1111(n << 1)1111 1110吗?(在任何语言中,对负数的移位是否格式正确?)

尽管如此,使用公式并做(-1 << 1) ^ (-1 >> 31),假设一个 32 位整数,我得到1111 111140 亿,而表格认为我应该有 1。

4

3 回答 3

31

将负符号整数右移复制符号位,因此

(-1 >> 31) == -1

然后,

(-1 << 1) ^ (-1 >> 31) = -2 ^ -1
                       = 1

这可能更容易以二进制形式可视化(此处为 8 位):

(-1 << 1) ^ (-1 >> 7) = 11111110 ^ 11111111
                      = 00000001
于 2010-12-26T07:36:52.597 回答
2

考虑 zig zag 映射的另一种方式是,它是对符号和幅度表示的轻微扭曲。

在 zig zag 映射中,映射的最低有效位 (lsb) 表示值的符号:如果为 0,则原始值为非负,如果为 1,则原始值为负。

非负值只需左移一位,以便为 lsb 中的符号位腾出空间。

对于负值,您可以对数字的绝对值(大小)执行相同的左移一位,并简单地让 lsb 指示符号。例如,-1 可以映射到 0x03 或 0b00000011,其中 lsb 表示它是负数,并且 1 的大小左移 1 位。

这种符号和幅度表示的丑陋之处在于“负零”,映射为 0x01 或 0b00000001。这种零变体“用尽”了我们的一个值,并将我们可以表示的整数范围移动了一个。我们可能希望将负零映射到 -2^63 的特殊情况,以便我们可以表示 [-2^63, 2^63) 的完整 64b 2 的补码范围。这意味着我们使用了一种有价值的单字节编码来表示一个值,该值将非常、非常、非常少地用于为小幅度数字优化的编码中,并且我们引入了一种特殊情况,这很糟糕。

这就是 zig zag 对这个符号和幅度表示的扭曲发生的地方。符号位仍在 lsb 中,但对于负数,我们从幅度中减去 1,而不是特殊的大小写负零。现在,-1 映射到 0x01 并且 -2^63 也具有非特殊情况表示(即 - 幅度 2^63 - 1,左移一位,设置 lsb / 符号位,所有位都设置为 1) .

因此,考虑 zig zag 编码的另一种方式是,它是一种更智能的符号和幅度表示:符号位存储在 lsb 中,从负数的幅度中减去 1,幅度左移一位。

使用您发布的无条件按位运算符来实现这些转换更快,而不是显式测试符号,特殊情况处理负值(例如 - 取反和减 1,或按位不),移动幅度,然后显式设置lsb 符号位。但是,它们在效果上是等效的,并且这个更明确的符号和幅度系列步骤可能更容易理解我们在做什么以及为什么要做这些事情。

我会警告你,虽然 C / C++ 中的位移负值是不可移植的,应该避免。左移负值具有未定义的行为,而右移负值具有实现定义的行为。即使左移一个正整数也可能有未定义的行为(例如 - 如果您移入符号位,可能会导致陷阱或更糟)。所以,一般来说,不要在 C/C++ 中对有符号类型进行位移。“拒绝吧。”

首先转换为类型的无符号版本,以根据标准获得安全、明确定义的结果。这确实意味着您将不会有负值的算术移位 - 只有逻辑移位,因此您需要调整逻辑来解决这个问题。

以下是 C 中 64b 整数的 zig zag 映射的安全且可移植的版本(注意算术否定):

#include <stdint.h>

uint64_t zz_map( int64_t x )
{
  return ( ( uint64_t ) x << 1 ) ^ -( ( uint64_t ) x >> 63 );
}

int64_t zz_unmap( uint64_t y )
{
  return ( int64_t ) ( ( y >> 1 ) ^ -( y & 0x1 ) );
}
于 2018-02-22T09:47:12.227 回答
1

让我在讨论中添加我的两分钱。正如其他答案所指出的,锯齿形编码可以被认为是符号幅度的扭曲。这一事实可用于实现适用于任意大小整数的转换函数。例如,我在我的 Python 项目中使用以下代码:

def zigzag(x: int) -> int:
    return x << 1 if x >= 0 else (-x - 1) << 1 | 1

def zagzig(x: int) -> int:
    assert x >= 0
    sign = x & 1
    return -(x >> 1) - 1 if sign else x >> 1

尽管 Pythonint没有固定的位宽,但这些函数仍然有效;相反,它动态扩展。但是,这种方法在编译语言中可能效率低下,因为它需要条件分支。

于 2018-04-04T20:30:55.910 回答