引言
利用CPU使用二进制表示和处理数据这种特性,将整数的二进制表示法用作数据结构的方法就是位掩码(bitmask),严格来说,位掩码算不上数据结构,但可以作为一个很有用的工具。
使用位掩码的代码的优点:
(1)更快的执行速度。
(2)更简洁的代码。
(3)更少的内存占用量。
(4)用数组替代关联数组。
术语定义:计算机将所有的整数变量表示为二进制的形式,此时,一个二进制位就称为位bit,位只能取0或1,是计算机表示数据的基础。
位运算符:
(1)位单位进行的AND运算按照位比较输入的两个整数,两整数的当前位都是1的时候结果才是1。代码表示为:a&b。
(2)位单位OR运算按照位比较输入的两个整数,两整数有一位是1,结果就是1。代码表示为:a|b。
(3)位单位XOR运算按照位比较输入的两个整数,两整数有一位是1一位是0,结果才是1。代码表示为:a^b。
(4)位单位NOT运算会对输入的整数各位状态进行取反。代码表示为:~a。
(5)位移运算符(shift)能都将整数a按照位向左或向右移动。代码表示为:将整数a左移b位a<<b,将a右移b位a>>b。
注意事项:
(1)&、|、^位运算符的优先级低于==或!=等比较运算符,注意使用括号。
(2)使用左移右移时,注意位数是否打开,否则会出现溢出的情形。
(3)有符号和无符号数的左移右移操作会动即符号位,出现错误。
(4)把N位二进制数向左移动N位以上,C++中没有定义这种操作,会出现错误。
利用位掩码实现集合:
这种应用中,N位整数变量表示可以拥有0到N-1整数元素的集合,要判断某个元素i是否包含在集合里,只要查看表示2i是否已打开即可。
这里有个示例,披萨店有0到19这20种配料,当然可以使用大小20的布尔类型数组,也可以使用位掩码表示集合进行运算。
如果不加任何配料,即空集:
int emptyPizza = 0;
如果添加所有配料,即紧致集:
int fullPizza = (1 << 20) - 1;
如果添加第p种配料:
toppings |= (1 << p);
确认是否已包含元素:
if(toppings & (1 << p)) cout << “pepperoni is in” << endl;
这里需要注意一下,&运算的结果值是0或1<<p。如果将结果返回值想成1就是错误的,例如错误示例:
if(toppings & (1 << p) == 1) cout << “pepperoni is in” << endl;
删除元素:
当确定有这一位时可以这样删除:
toppings -= (1 << p);
当然这个删除方式,第p位不存在时就不可以使用了。
可以使用这样的删除方法:
toppings &= ~(1 << p);
切换元素:
如果已经添加第p个配料就删去,如果没有添加第p个配料就加上,可以使用XOR运算:
toppings ^= (1 << p);
对两个集合的运算:
int added = (a | b); \\a和b的和集
int intersection(a & b); \\a和b的交集
int removed = (a & ~b); \\从a减去b的差集
int toggled = (a ^ b); \\只被a或b一个集合包含的元素集合
求集合大小:
利用递归函数遍历所有位,从而算出打开位的个数:
int bitcount(int x)
{
if(x\==0) return 0;
return x % 2 + bitcount(x / 2);
}
也可以使用位运算优化一下:
int bitcount(int x)
{
if(x\==0) return 0;
return x & 1 + bitcount(x >> 1);
}
g++给出了一个内部命令,可以使用__builtin_popcount(toppings) 代替这个函数,而且这些命令经过优化,时间效率很高。
找出最小元素:
这个问题也可以这样表述,求低位有几个0。这里g++也有一个内部命令__builtin_ctz(toppings),求得最小元素。
也可以直接把第i位的数拿出来,也就是树状数组的一个操作。
int firstToppings = (toppings & -toppings);
删除最小元素
toppings &= (toppings - 1);
这个方法可以判断这个数是否是2的多次方。
遍历所有子集:
for(int subset = pizza; subset; subset = ((subset - 1) & pizza))
{
//subset是pizza的子集
}
位掩码的应用示例
指数时间动态规划法:简化制表方法。
埃拉托色尼的筛:使用位掩码优化的埃拉托色尼的筛可以把空间优化到原来的1/8。
实现:
int n;
unsigned char sieve[(MAX_N + 7) / 8];
//确定k是否为素数
inline bool isPrime(int k){
return sieve[k >> 3] & (1 << (k & 7));
}
//标记为k不是素数
inline void setComposite(int k){
sieve[k >> 3] &= ~(1 << (k & 7));
}
//实现使用位掩码的埃拉托色尼筛选法
//执行此函数后,利用isPrime()就能判断各数值是否为素数
void eratosthenes(){
memset(sieve, 255, sizeof(sieve));
setComposite(0);
setComposite(1);
int sqrtn = int(sqrt(n));
for(int i=2;i<=sqrtn;++i)
//如果该数值尚未删除
if(isPrime(i))
//对i的倍数j赋予isPrime[j]=false值
//小于i*i的倍数已被删除,不必考虑
for(int j=i*i;j<=n;j+=i)
setComposite(j);
}
“十五拼图”的状态表示法
位掩码不仅能表示布尔类型的数组,还可以把多个位并在一块,作为一个单元用作数组。
实现:
typedef unsigned long long uint64;
//返回mask中index位置的值
int get(uint64 mask, int index){
return (mask >> (index << 2)) & 15;
}
//返回将mask中的index位置的值设置为value后的结果值
uint64 set(uint64 mask, int index, uint64 value){
return mask & ~(15LL << (index << 2)) | (value << (index << 2));
}
O(1)优先级队列:优先级队列是从元素中快速找到最高优先级元素的数据结构。向优先级队列添加数据、删除数据,如果有N个元素,那么将耗费O(lgN)的时间。
当优先级队列限定在特殊范围内时,可以使用位掩码生成能够在O(1)时间内完成运算的优先级队列,这种概念能实际应用于Linux内核的进程管理。