13

在 Programming Pearls,第 2 版的第 140 页上,Jon 提出了使用位向量实现集合。

我们现在将转向利用我们的集合表示整数这一事实的两个最终结构。位向量是第 1 列的老朋友。以下是它们的私有数据和函数:

enum { BITSPERWORD = 32, SHIFT = 5, MASK = 0x1F };
int n, hi, *x;
void set(int i)  {        x[i>>SHIFT] |=  (1<<(i & MASK)); }
void clr(int i)  {        x[i>>SHIFT] &= ~(1<<(i & MASK)); }
int  test(int i) { return x[i>>SHIFT] &=  (1<<(i & MASK)); }

正如我所收集的,如第 1 列所述,表示整数集的位向量的中心思想是,当且仅当整数 i 在集合中时,才打开第 i 位。

但是对于上面三个函数所涉及的算法,我实在是一头雾水。而且书上也没有解释。

我只能得到i & MASK是得到 i 的低 5 位,而i>>SHIFT将 i 向右移动 5 位。

有人会详细说明这些算法吗?位操作对我来说总是一个神话,:(

4

3 回答 3

57

位域和你

我将使用一个简单的示例来解释基础知识。假设您有一个四位无符号整数:

[0][0][0][0] = 0

您可以通过将其转换为基数 2 来表示 0 到 15 之间的任何数字。假设我们的右端是最小的:

[0][1][0][1] = 5

所以第一位加 1,第二位加 2,第三位加 4,第四位加 8。例如,这里是 8:

[1][0][0][0] = 8

所以呢? 假设您想在应用程序中表示二进制状态——如果启用了某个选项,是否应该绘制某个元素,等等。您可能不想对其中的每一个都使用一个完整的整数——它会使用一个 32 位整数来存储一位信息。或者,以四位继续我们的示例:

[0][0][0][1] = 1 = ON
[0][0][0][0] = 0 = OFF //what a huge waste of space!

(当然,这个问题在现实生活中更为明显,因为 32 位整数看起来像这样:

[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0] = 0

答案是使用位域。我们有一组属性(通常是相关的),我们将使用位操作打开和关闭这些属性。所以,比如说,你可能在一个硬件上有 4 个不同的灯,你想打开或关闭它们。

 3  2  1  0
[0][0][0][0] = 0

(为什么我们从光 0 开始?我稍后会解释。)请注意,这是一个整数,并且存储为整数,但用于表示多个对象的多个状态。疯狂的!假设我们打开灯 2 和 1:

 3  2  1  0
[0][1][1][0] = 6

您应该在这里注意的重要一点:灯 2 和 1 亮起应该等于 6 可能没有明显的原因,而且我们将如何使用这种信息存储方案做任何事情可能并不明显。如果添加更多位,它看起来并不明显:

 3  2  1  0
[1][1][1][0] = 0xE \\what?

为什么我们关心这个?对于 0 到 15 之间的每个数字,我们是否只有一个状态?如果没有一些疯狂的 switch 语句系列,我们将如何管理它?啊...

尽头的光

因此,如果您之前使用过二进制算术,您可能会意识到左边的数字和右边的数字之间的关系当然是以 2 为底的。即:

1*(2 3 ) + 1*(2 2 ) + 1*(2 1 ) +0 *(2 0 ) = 0xE

所以每束光都存在于方程每一项的指数中。如果灯亮,则其术语旁边有一个 1 - 如果灯熄灭,则有一个 0。花点时间说服自己,在 0 到 15 之间恰好有一个整数对应于这个编号方案中的每个状态。

位运算符

现在我们已经完成了,让我们花点时间看看在这个设置中移位对整数有什么作用。

[0][0][0][1] = 1

当您在整数中向左或向右移动位时,它实际上是向左和向右移动位。(注意:我 100% 否认这种对负数的解释!有龙!)

1<<2 = 4
[0][1][0][0] = 4
4>>1 = 2
[0][0][1][0] = 2

当移动不止一位表示的数字时,您会遇到类似的行为。此外,让自己相信 x>>0 或 x<<0 只是 x 应该不难。不会在任何地方转移。

这可能向不熟悉 Shift 运算符的任何人解释了它们的命名方案。

位运算

这种以二进制表示的数字也可用于阐明按位运算符对整数的操作。第一个数字中的每个位与其同伴编号进行异或运算、与运算或或运算。花点时间浏览一下维基百科,熟悉一下这些布尔运算符的功能——我将解释它们如何在数字上起作用,但我不想详细地重复这个总体概念。

...

欢迎回来!让我们首先检查 OR (|) 运算符对存储在四位中的两个整数的影响。

 OR OPERATOR ON:
 [1][0][0][1] = 0x9
 [1][1][0][0] = 0xC
________________
 [1][1][0][1] = 0xD

艰难的!这与布尔 OR 运算符的真值表非常相似。请注意,每一列都忽略了相邻的列,只是用第一位和第二位 OR'd 的结果填充结果列。请注意,在该特定列中,任何或 1 的值都是 1。任何带零的或'd 都保持不变。

AND (&) 的表格很有趣,尽管有些倒置:

 AND OPERATOR ON:
 [1][0][0][1] = 0x9
 [1][1][0][0] = 0xC
________________
 [1][0][0][0] = 0x8

在这种情况下,我们做同样的事情——我们对列中的每个位执行 AND 操作,并将结果放入该位。没有列关心任何其他列。

关于这一点的重要教训,我邀请您使用上图进行验证:任何与零相加的东西都是零。此外,同样重要的是 - 与 1 进行与运算的数字不会发生任何事情。他们保持不变。

最终表 XOR 具有我希望你们现在都可以预测的行为。

 XOR OPERATOR ON:
 [1][0][0][1] = 0x9
 [1][1][0][0] = 0xC
________________
 [0][1][0][1] = 0x5

每个位都与其列 yadda yadda 等进行异或运算。但是仔细看第一排和第二排。哪些位改变了?(其中一半。)哪些位保持不变?(回答这个没有积分。)

当(且仅当)第二行中的位为 1 时,结果中的第一行中的位正在更改!

一个灯泡的例子!

所以现在我们有了一组有趣的工具,可以用来翻转单个位。让我们回到灯泡示例,只关注第一个灯泡。

 0
[?] \\We don't know if it's one or zero while coding

我们知道我们有一个操作可以始终使该位等于一个 - OR 1 运算符。

0|1 = 1
1|1 = 1

所以,忽略其余的灯泡,我们可以这样做

4_bit_lightbulb_integer |= 1;

并且确定我们除了将第一个灯泡设置为 ON 之外什么也没做。

 3  2  1  0
[0][0][0][?] = 0 or 1? \\4_bit_lightbulb_integer
[0][0][0][1] = 1
________________
[0][0][0][1] = 0x1

同样,我们可以将数字与零相加。嗯 - 不完全为零 - 我们不想影响其他位的状态,所以我们将用 1 填充它们。

我将使用一元(一个参数)运算符进行位否定。~ (NOT) 位运算符翻转其参数中的所有位。〜(0X1):

[0][0][0][1] = 0x1
________________
[1][1][1][0] = 0xE

我们将把它与下面的 AND 位结合使用。

让我们做 4_bit_lightbulb_integer & 0xE

 3  2  1  0
[0][1][0][?] = 4 or 5? \\4_bit_lightbulb_integer
[1][1][1][0] = 0xE
________________
[0][1][0][0] = 0x4

我们在右侧看到很多整数,它们没有任何直接相关性。如果您经常处理位字段,您应该习惯这一点。看左边。右边的位始终为零,其他位不变。我们可以关掉灯 0 并忽略其他一切!

最后,您可以使用 XOR 位选择性地翻转第一位!

 3  2  1  0
[0][1][0][?] = 4 or 5? \\4_bit_lightbulb_integer
[0][0][0][1] = 0x1
________________
[0][1][0][*] = 4 or 5?

我们现在实际上不知道 * 的值是什么——只是从什么翻转过来的?曾是。

结合位移和按位运算

关于这两个操作的有趣事实是,当它们一起使用时,它们允许您操作选择性位。

[0][0][0][1] = 1 = 1<<0
[0][0][1][0] = 2 = 1<<1
[0][1][0][0] = 4 = 1<<2
[1][0][0][0] = 8 = 1<<3

唔。有趣的。我将在这里提到否定运算符 (~),因为它以类似的方式用于为位字段中的内容生成所需的位值。

[1][1][1][0] = 0xE = ~(1<<0)
[1][1][0][1] = 0xD = ~(1<<1)
[1][0][1][1] = 0xB = ~(1<<2)
[0][1][1][1] = 0X7 = ~(1<<3)

您是否看到移位值与移位位的相应灯泡位置之间存在有趣的关系?

规范的位移运算符

正如上面提到的,我们有一个有趣的通用方法,可以使用上面的位移器打开和关闭特定的灯。

要打开灯泡,我们使用位移在正确位置生成 1,然后将其与当前灯泡位置进行或运算。假设我们要打开灯 3,而忽略其他所有内容。我们需要进行 OR 的位移操作

 3  2  1  0
[?][?][?][?]  \\all we know about these values at compile time is where they are!

和 0x8

[1][0][0][0] = 0x8

多亏了位移位,这很容易!我们将选择灯的编号并切换值:

1<<3 = 0x8

接着:

4_bit_lightbulb_integer |= 0x8;

 3  2  1  0
[1][?][?][?]  \\the ? marks have not changed!

我们可以保证第三个灯泡的位设置为 1,并且没有其他任何变化。

清除位的工作方式类似——我们将使用上面的否定位表来清除灯 2。

~(1<<2) = 0xB = [1][0][1][1]

4_bit_lightbulb_integer & 0xB:

 3  2  1  0
[?][?][?][?] 
[1][0][1][1]
____________
[?][0][?][?]

翻转位的 XOR 方法与 OR 方法相同。

所以位切换的规范方法是这样的:

打开灯 i:

4_bit_lightbulb_integer|=(1<<i)

关灯 i:

4_bit_lightbulb_integer&=~(1<<i)

翻转灯 i:

4_bit_lightbulb_integer^=(1<<i)

等等,我怎么读这些?

为了检查位,我们可以简单地将所有位清零,除了我们关心的位。然后我们将检查结果值是否大于零——因为这是唯一可能为非零的值,所以当且仅当它非零时,它将使整个整数非零。例如,要检查位 2:

1<<2:

[0][1][0][0]

4_bit_lightbulb_integer:

[?][?][?][?]

1<<2 & 4_bit_lightbulb_integer:

[0][?][0][0]

还记得前面的例子中 的值吗?没有改变。还要记住,任何 AND 0 都是 0。因此,我们可以肯定地说,如果该值大于零,则位置 2 处的开关为真,灯泡为零。同样,如果该值关闭,则整个事物的值将为零。

(您可以将 4_bit_lightbulb_integer 的整个值交替移动 i 位,然后将其与 1 相加。我不记得如果一个比另一个快,但我对此表示怀疑。)

所以规范检查功能:

检查位 i 是否打开:

if (4_bit_lightbulb_integer & 1<<i) {
\\do whatever

}

具体情况

现在我们已经有了一套完整的按位运算的工具,我们可以看这里的具体例子。这基本上是相同的想法 - 除了执行它的更简洁和强大的方式。我们来看看这个函数:

void set(int i)  {        x[i>>SHIFT] |=  (1<<(i & MASK)); }

从规范实现中,我将猜测这是试图将一些位设置为 1!让我们取一个整数,看看如果我将值 0x32(十进制的 50)输入i时会发生什么:

x[0x32>>5] |= (1<<(0x32 & 0x1f))

好吧,那是一团糟。让我们在右边剖析这个操作。为方便起见,假设还有 24 个不相关的零,因为它们都是 32 位整数。

...[0][0][0][1][1][1][1][1] = 0x1F
...[0][0][1][1][0][0][1][0] = 0x32
________________________
...[0][0][0][1][0][0][1][0] = 0x12

看起来一切都在顶部的边界处被切断,1s 变成了零。这种技术称为位掩码。有趣的是,这里的边界将结果值限制在 0 到 31 之间……这正是 32 位整数的位数!

x[0x32>>5] |= (1<<(0x12)) 让我们看看另一半。

...[0][0][1][1][0][0][1][0] = 0x32

右移五位:

...[0][0][0][0][0][0][0][1] = 0x01

请注意,这种转换完全破坏了函数第一部分的所有信息——我们有 32-5 = 27 个剩余的位,这些位可能是非零的。这表明选择了整数数组中的 2 27个整数中的哪一个。所以现在简化的方程是:

x[1] |= (1<<0x12)

这看起来就像规范的位设置操作!我们刚刚选择了

所以这个想法是使用前 27 位来选择一个要移位的整数,最后 5 位指示该整数中 32 位中的哪一位要移位。

于 2012-07-09T18:28:23.360 回答
12

理解正在发生的事情的关键是认识到BITSPERWORD= 2 SHIFT。因此,x[i>>SHIFT]找出数组的哪个 32 位元素x具有对应的位i。(通过i向右移动 5 位,您只需除以 32。)一旦找到 的正确元素,就可以使用x的低 5 位来查找对应于的特定位。就是这样;通过将 1 移位该位数,您将对应于 1 的位移动到对应于 中的第 th位的确切位置。ix[i>>SHIFT]ii & MASKx[i>>SHIFT]ix

这里有更多的解释:

想象一下,我们想要N位向量中的位容量。由于每个都int包含 32 位,因此我们需要(N + 31) / 32 int存储值(即 N/32 向上舍入)。在每个int值中,我们将采用位从最低有效位到最高有效位排序的约定。我们还将采用向量的前 32 位在 中的约定,x[0]接下来的 32 位在 中x[1],依此类推。这是我们正在使用的内存布局(显示位向量中对应于每个内存位的位索引):

      +----+----+-------+----+----+----+
x[0]: | 31 | 30 | . . . | 02 | 01 | 00 |
      +----+----+-------+----+----+----+
x[1]: | 63 | 62 | . . . | 34 | 33 | 32 |
      +----+----+-------+----+----+----+
        etc.

我们的第一步是分配必要的存储容量:

x = new int[(N + BITSPERWORD - 1) >> SHIFT]

(我们可以为动态扩展这个存储做准备,但这只会增加解释的复杂性。)

现在假设我们想要访问位i(设置它,清除它,或者只是想知道它的当前值)。我们需要首先弄清楚x要使用哪个元素。由于每个值有 32 位int,这很容易:

subscript for x = i / 32

利用枚举常量,x我们想要的元素是:

x[i >> SHIFT]

(把它想象成一个 32 位宽的窗口进入我们的 N 位向量。)现在我们必须找到对应于 的特定位i。查看内存布局,不难发现窗口中的第一个(最右边)位对应于位索引32 * (i >> SHIFT)。(窗口在i >> SHIFTslot in之后开始x,每个 slot 有 32 位。)因为这是窗口中的第一位(位置 0),所以我们感兴趣的位是在 position

i - (32 * (i >> SHIFT))

在窗户里。通过一些实验,你可以说服自己这个表达式总是等于i % 32(实际上,这是 mod 运算符的一个定义),反过来,它总是等于i & MASK. 由于最后一个表达式是计算我们想要的最快的方法,这就是我们将使用的。

从这里开始,剩下的就很简单了。我们从窗口的最低有效位置(即常数1)中的单个位开始,并将其向左移动i & MASK位以使其到达与i位向量中的位对应的窗口中的位置。这是表达式

1 << (i & MASK)

来自。随着位现在移动到我们想要的位置,我们可以使用它作为掩码来设置、清除或查询该位置的位的值,x[i>>SHIFT]并且我们知道我们实际上是在设置、清除或查询该值我们的位i向量中的位。

于 2012-07-09T17:45:01.583 回答
4

如果您将位存储在一个n 字数组中,您可以想象它们被布置为具有n行和 32 列 ( BITSPERWORD) 的矩阵:

         3                                         0
         1                                         0
      0  xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx
      1  xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx
      2  xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx     
      ....
      n  xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx

要获得第 k 位,您将 k 除以 32。(整数)结果将为您提供该位所在的行(单词),提醒将为您提供单词中的哪一位。

除以2^p可以简单地通过p向右移动位置来完成。可以通过获取最右边的 p 位(即与 (2^p - 1) 的按位与)来获得提醒。

在 C 语言中:

#define div32(k) ((k) >> 5)
#define mod32(k) ((k) & 31)

#define word_the_bit_is_in(k) div32(k)
#define bit_within_word(k)    mod32(k)

希望能帮助到你。

于 2012-07-09T18:18:37.303 回答