位数组
位数组(英语:bit array),是一种能够紧凑地存储位的数组。位数组可以被用来实现简单的有限集合。它能够通过硬件中位级别的并行运算快速操作。通常情况下,一个位数组可以存储位信息(w是硬件中单个存储单元的位数,如字节或字,而k是一非负整数),如果w不能被计算机中存储单位的字节数整除,就会由于内存碎片化浪费一些内存空间。
定义
位数组可以看作某个集合到 的映射。其中的每个值可以解释做灯泡的明暗,元素的有无等等。因为每一个值只有两种可能性,所以能够把每个值存储进一位的内存。与其他种类的数组对应地,可以通过对数组应用索引的方式操作单个比特。假设某个位数组的大小是 位,那么这个数组就可以用来表示一个包含 个元素的集合(比如 )的子集,在每个位置上用 表示对应元素存在, 表示其不存在。这种数据结构占用 的空间( 表示计算机里一个字的位数)。使用最高位还是最低位代表最小索引不会造成什么影响,但通常在小端序的机器上的实现偏向前者。
基本操作
尽管大多数的机器不能在内存中定位单个的位,也没有操作单个位的指令,字里的每一位仍然可以用位运算分离并单独操作。比如:
- 按位或可以用来设定某位为1:
0b11101010 | 0b00000100
等于0b11101110
- 按位与可以用来设定某位为0:
0b11101010 & 0b11111101
等于0b11101000
- 用按位与和非零检测可以用来确定某位是否存在
0b11101010 & 0b00000001
是0b00000000
,就是false0b11101010 & 0b00000010
是0b00000010
,就是true
- 按位异或可以被用来对某一位取反(这里用
^
表示异或:0b11101010 ^ 0b00000100
等于0b11101110
0b11101110 ^ 0b00000100
等于0b11101010
- 按位非可以用来对所有位取反:
~0b10110010
等于0b01001101
为了获得这些操作所需的掩码,可以用移位运算符,把数字1
左移合适的位数,若需,再把结果按位取反。
给定两个长度相同的位数组,我们可以简单地用 次运算计算他们的并集,交集与差集,和任意一个的补集:
for i from 0 to n/w-1
complement_a[i] := not a[i]
union[i] := a[i] or b[i]
intersection[i] := a[i] and b[i]
difference[i] := a[i] and (not b[i])
假如我们希望遍历某个位数组里所有的位,我们可以高效地通过一个遍历位数组里每个字的二层循环,只需要 次内存访问:
for i from 0 to n/w-1
index := 0 // if needed
word := a[i]
for b from 0 to w-1
value := word and 1 ≠ 0
word := word shift right 1
// do something with value
index := index + 1 // if needed
这几个代码示例都展示了理想的访问局部性,这又将会获得极大的缓存访问性能提升。如果一个缓存行有k个字,仅会出现大约 次缓存不命中。
更复杂的运算
像字符串一样,我们可以很方便的定义位数组的长度,子串,字典序比较,链接,反转等概念。这些概念中有一部分对字节序敏感。
如果希望查出位数组中1的个数(有时叫人口数或汉明重量),可以采用一些通过简单位运算实现的计算每个字里1的数量的无分支的算法。只需要对位数组的每一个字运行这样的算法并求和,就能得到位数组的汉明重量。这样的算法也能用来算出数组中0的个数。
翻转
一像素一位的图片的垂直翻转,或一些快速傅里叶变换,需要对字里的每一位进行翻转(这样像b31 b30 ... b0
这样的序列就变成了b0 ... b30 b31
)。
当这处理器没法运行这种字内反转操作的时候,仍然可以完成这样的需求,这里举一个32位的例子
exchange two 16bit halfwords
exchange bytes by pairs (0xddccbbaa -> 0xccddaabb)
...
swap bits by pairs
swap bits (b31 b30 ... b1 b0 -> b30 b31 ... b0 b1)
The last operation can be written ((x&0x55555555)<<1) | (x&0xaaaaaaaa)>>1)).
寻找第一位1
“找第一位一”的操作可以确认数组里的第一个1,而且有广泛的硬件支持(对于最长是一个字的数组)和高效的用于计算的算法。当先序队列储存在位数组里,这种操作可以用来确定队列中的最优先元素。为拓展一字长度的“找第一位一”操作使得其能被用于更长的序列,我们可以找第一个非零字,然后施用这个操作。“找第一位一”,“查前导零”,“查前导一”,“查后导零”,“查后导一”和“以二为底的对数”等相关操作也可以直接延伸到位数组上。
压缩
位数组能最紧密地储存随机比特序列,即每一位是0和1的概率相等且任意两位之间无关联的比特序列。但是大多数的数据不是随机的,所以有时候可以存储得更紧密。比如,一个典型的传真图像不是随机的,并且可以压缩。游程编码在压缩这些长流上常常被应用。然而,大多数的压缩信息格式没这么容易随机访问;而且大幅度地压缩位数组也会带来失去位数组位级别并行运算优势的风险(矢量化)。因此,相对于按比特流的方式压缩位数组,我们可能按字节流(或字流)的方式压缩(参见Bitmap index#Compression)。
优势和劣势
尽管位数组十分简单,但对于特定的使用场景,它仍然能表现出相对于其他数据结构的优越性:
- 位数组是唯一能把 个独立数据存进 个字里的数据结构
- 位数组允许在不进行内存访问的情况下将小的比特序列长期存进寄存器集并在其中操作。
- 由于位数组利用位级别并行性的能力,有限的内存访问,和对缓存的最大限度应用,在实际数据集上,它的能力通常超过其他数据结构,甚至是一些复杂度更小的数据结构
然而,位数组也不能用来解决所有问题。比如:
应用
由于位数组的紧凑性,它们在格外需要空间或效率的地方有许多应用。它们通常用在表示一单组布尔标记位或者布尔值的有序序列的情况中。
位数组也可以用在先序队列里面。这种情况下,当且仅当k在队列里时,第k位才被设置为1. 比如在Linux内核里就使用了这种数据结构,而且它的能力可以从硬件里的“找首位0”操作中得到很大的提升。
位数组也可以用在内存页,inode,磁盘分区等的分配上。这种情况下,可能会用“bitmap”这个术语。(然而bitmap这个词更常用于指代位图)
位数组的另一个应用是布隆过滤器,一种用小概率产生错误换取很小的空间开销的概率性集合数据结构。也可以建立基于位数组的可接受正向错误和反向错误的概率性散列表。
位数组和它们上的操作对于构造希望使用尽可能最小空间的简洁数据结构也很重要。在这种情况下,像找第n个1或者在某个范围内查1的个数的操作就变得十分重要了。
位数组也是用来检查压缩过的数据流的有用的抽象,压缩数据流中经常包含占用字节部分或未按字节对齐的元素。比如,一个8位字符的哈夫曼编码的长度可能是1到255位之间的任意值。
在信息检索中,位数组是常见短语的邻接列表的很好的表示方式。在一个严格递增的整数序列中,如果我们求每两个相邻值之间的差,并且用一元编码将其编码,结果就是一个当且仅当列表里有n时第n位为1的位数组。一个n位宽的间隔出现的概率是 。这也是格伦布编码在参数M为1时的一种特例,这个参数通常只当 时被选择,否则文档中就至少会出现38%的这个短语。
语言支持
APL语言提供了对任意形状与大小的位数组的支持,作为一种不同于整形的布尔数据类型。全部主要的实现(Dyalog APL, APL2, APL Next, NARS2000, Gnu APL等等)都会把比特紧凑打包成机器一个字的大小。通过通常的索引表达(如A[3]
)或通过常见的基本函数和运算符,可以单独访问每个位,这里通常用像对字节表里比特数求和的特例算法对其进行操作。
C语言的位段,结构体中存在的大小是几位的伪对象,事实上就是位数组。位段的长度限制在一字一下。尽管它们提供了一个方便的表达式,位数组在多数的机器上仍然是用位运算符操作的,而且它们也只能被静态定义(像C的静态数组,编译的时候数组的大小就被固定了)。用字来当做小的位数组并且用位运算符对其操作也是C程序员的一种常见用法。在X窗口系统中有一个广泛可用的头文件,叫做xtrapbits.h,它是一个“对系统定义位数组的位段操作的可移植的方式”。在comp.lang.c 常见问题解答[失效链接]里有一份关于上述方法的更详细的描述
在C++里,尽管单个的布尔变量通常占据一字节或者一个整数的空间,标准模板库里面的类型vector<bool>
就是一个模板部分特化,这里比特为空间效率优化被打包。由于C++中内存的最小单元是字节而不是比特,因此索引运算符并不返回对某个元素的引用,相反,它返回的是一个代理引用。这可能看起来没什么大事,但是这意味着vector<bool>
不是标准STL容器,也使得它的使用通常不被鼓励。另一个不同的STL类bitset
[1],提供了一个长度在编译时固定的位数组,而且它的接口和表达式更贴合C程序员的对位数组的通常用法。位数组也有一些附加的功能,像高效查位数组中1的个数的功能。Boost C++ Libraries提供了一个dynamic_bitset
类[2],dynamic_bitset
的大小在运行时被指定。
D语言在标准库Phobos里提供了位数组std.bitmanip
。像在C++里一样,因为单独的比特在大多数的硬件上没法寻址,索引运算符不会返回引用,但是返回的是一个布尔值。
在Java里,可以通过BitSet
构造位数组,这种位数组可以通过C程序员熟悉的位运算的名字操作。不像C++里的bitset,Java里的BitSet不限制大小,在初始化的时候就有无穷位初始为0的比特;可以在任意的索引设定或取值。附加地,Java里还有一个EnumSet
类,EnumSet作为位数组表示一个枚举里元素的集合,是位段的一个较安全的选择。
.NET框架提供了一个BitArray
收集类。它储存布尔值,支持随机访问和位运算符,可遍历,并且长度可增可减。
尽管Standard ML没有提供位数组的支持,New Jersey的Standard ML在SML/NJ库里有一个扩展,结构体BitArray
。它的大小不固定,而且支持设定操作和位运算,通常包含位移运算。
Haskell,类似的,目前也缺少位运算的标准支持,但是GHC和Hugs都提供了带有分类的位函数和位运算的Data.Bits
模块,模块包含了位移和旋转操作,还有一个可以用来实现位数组的包含布尔值的“未封装的”数组,尽管它缺乏先前提到的模块的支持。
在Perl语言里,字符串可以被用作可拓展的位数组。这可以通过通常的位运算符操作(按位非,按位或,按位与,按位异或等)[3],并且单独的比特可以通过函数vec
检查并设定值[4]。
在Ruby里,可以用方括号运算符([]
)像位数组一样访问(但不能设定)一个整数(Fixnum
或Bignum
)的一位。
苹果的Core Foundation库包含了CFBitVector(页面存档备份,存于互联网档案馆)和CFMutableBitVector(页面存档备份,存于互联网档案馆)结构体。
PL/I支持任意长度的位字符串数组,长度可能固定可能变化。数组的元素可能是对齐的(每个元素在字节或字的开始对齐)或者不对齐(元素之间紧挨着,没有间隔)
PL/pgSQL和PostgreSQL's SQL支持位字符串作为内置类。SQL有两种比特类:bit(n)
和bit varying(n)
(n是正整数)[5]。
像VHDL, Verilog和SystemVerilog的硬件描述语言内置了用于实现像触发器一样的内存元素的位数组,普遍是硬件总线和硬件信号。在像OpenVera,E语言和SystemVerilog之类的硬件验证语言,位数组悲用来从硬件模型中抽样,并表示模拟时转移到硬件的数据。
参见
参考
- ^ SGI.com Tech Archive Resources now retired. SGI. 2 January 2018 [2020-06-25]. (原始内容存档于2018-01-01).
- ^ dynamic_bitset<Block, Allocator> - 1.66.0. www.boost.org. [2020-06-25]. (原始内容存档于2008-09-06).
- ^ perlop - perldoc.perl.org. perldoc.perl.org. [2020-06-25]. (原始内容存档于2012-07-17).
- ^ vec - perldoc.perl.org. perldoc.perl.org. [2020-06-25]. (原始内容存档于2020-06-30).
- ^ 存档副本. [2020-06-25]. (原始内容存档于2020-05-14).