编程珍珠,第2版中的位向量实现

时间:2012-07-09 17:40:40

标签: algorithm

在编程珍珠第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是向右移动5位。

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

3 个答案:

答案 0 :(得分:56)

位字段和您

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

[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 < sup> 0 )= 0xE

因此,每个光存在于等式的每个项的指数中。如果指示灯亮,则其术语旁边有一个1 - 如果指示灯熄灭,则表示零。花点时间说服自己,在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>&gt; 0或x&lt;&lt; 0只是x是不难的。不会在任何地方转移。

这可能解释了Shift操作符对任何不熟悉它们的人的命名方案。

按位操作

这种二进制数字的表示也可以用来阐明整数上按位运算符的运算。第一个数字中的每个位都是xor-ed,-ed或or-ed及其对应的数字。花一点时间冒险到维基百科并熟悉这些布尔运算符的功能 - 我将解释它们如何在数字上起作用,但我不想非常详细地重述这个概念。

...

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

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

坚韧!这与布尔OR运算符的真值表非常相似。请注意,每列忽略相邻列,只需在结果列中填入第一位和第二位OR的结果。请注意,在该特定列中,任何内容或&#39; d与1的值均为1。任何或零都保持不变。

AND(&amp;)表很有意思,虽然有些倒置:

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

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

有关此问题的重要教训,我邀请您使用上图验证:任何与零编号的AND-ed为零。此外,同样重要的是 - 与一个AND编号的数字没有任何关系。他们保持不变。

决赛桌,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

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

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

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

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

让我们做4_bit_lightbulb_integer&amp; 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

嗯。有趣。我在这里提到否定运算符(〜),因为它以类似的方式用于为位域中的AND运算产生所需的位值。

[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)

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

规范的bithift运算符

如上所述,我们有一个有趣的通用方法,可以通过上面的位移位器打开和关闭特定的灯光。

要打开灯泡,我们使用位移生成1在正确的位置,然后将其与当前灯泡位置进行OR运算。假设我们想打开灯3,而忽略其他一切。我们需要进行ORs

的位移操作
 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!

我们可以保证第3个灯泡的位设置为1并且没有其他任何内容发生变化。

清除有点类似的工作 - 我们将使用上面的否定位表来清除灯光。

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

4_bit_lightbulb_integer&amp; 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 <&lt; 2&amp; 4_bit_lightbulb_integer:

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

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

(你可以选择将4_bit_lightbulb_integer的整个值移动i比特,然后将它转换为1.如果一个比另一个快,我不记得我的头顶但是我怀疑它。)< / p>

所以规范检查功能:

检查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变为零。这种技术称为 Bit Masking 。有趣的是,这里的边界将结果值限制在0到31之间......这正是我们对32位整数的位位置数!

x [0x32&gt;&gt; 5] | =(1&lt;&lt;(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位。

答案 1 :(得分:12)

理解正在发生的事情的关键是要认识到BITSPERWORD = 2 SHIFT 。因此,x[i>>SHIFT]查找数组x的哪个32位元素具有与i对应的位。 (通过将i 5位向右移动,您只需要除以32.)找到x的正确元素后,i的低5位就可以了用于查找x[i>>SHIFT]的哪个特定位对应i。这就是i & MASK的作用;通过将1乘以该位数,您将对应于1的位移动到x[i>>SHIFT]内与ix th 位对应的精确位置

这是一个更多的解释:

想象一下,我们希望在位向量中容纳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的哪个元素。由于每int个值为32位,因此很容易:

subscript for x = i / 32

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

x[i >> SHIFT]

(将其视为我们的N位向量的32位宽窗口。)现在我们必须找到与i对应的特定位。查看内存布局,不难发现窗口中的第一个(最右边)位对应于位索引32 * (i >> SHIFT)。 (窗口在i >> SHIFT中的x个槽之后开始,每个槽都有32位。)由于那是窗口中的第一个位(位置0),那么我们感兴趣的位是位置

i - (32 * (i >> SHIFT))
在窗口中

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

从这里开始,其余部分非常简单。我们从窗口最低位的一个位开始(即常量1),并将其向左移动i & MASK位以使其到达窗口中的位置对应于位向量中的位i。这就是表达式

1 << (i & MASK)

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

答案 2 :(得分:3)

如果您将位存储在n 的数组中,您可以将它们想象成一个包含n行和32列({{1}的矩阵}}):

BITSPERWORD

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

只需将 3 0 1 0 0 xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx 1 xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx 2 xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx .... n xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx xxxxxxxxxx 位置向右移动,就可以除以2^p。可以通过获得最右边的p位(即按位AND和(2 ^ p-1))来获得提醒。

用C表示:

p

希望它有所帮助。