1、什么是位运算?程序中的所有数在计算机内存中都是以二进制的形式储存的。位运算说穿了,就是直接对整数在内存中的二进制位进行操作。比如,and 运算本来是一个逻辑运算符,但整数与整数之间也可以进行 and 运算。举个例子,6 的二进制是 110,11 的二进制是 1011,那么6 and 11 的结果就是 2,它是二进制对应位进行逻辑运算的结果(0 表示 False,1 表示True,空位都当 0 处理):110AND 1011-0010 2由于位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。当然有人会说,这个快了有什么用,计算 6 and 11 没有什么实际意义啊。这一系列的文
2、章就将告诉你,位运算到底可以干什么,有些什么经典应用,以及如何用位运算优化你的程序。Pascal 和 C 中的位运算符号下面的 a 和 b 都是整数类型,则:C 语言 | Pascal 语言-+-a a = a;printf( “%dn“, a ); return 0;如果 not 的对象是有符号的整数,情况就不一样了,稍后我们会在“整数类型的储存”小节中提到。= 5. shl 运算 =a shl b 就表示把 a 转为二进制后左移 b 位(在后面添 b 个 0) 。例如 100 的二进制为1100100,而 110010000 转成十进制是 400,那么 100 shl 2 = 400。可以
3、看出,a shl b 的值实际上就是 a 乘以 2 的 b 次方,因为在二进制数后添一个 0 就相当于该数乘以 2。通常认为 a shl 1 比 a * 2 更快,因为前者是更底层一些的操作。因此程序中乘以 2 的操作请尽量用左移一位来代替。定义一些常量可能会用到 shl 运算。你可以方便地用 1 shl 16 - 1 来表示 65535。很多算法和数据结构要求数据规模必须是 2 的幂,此时可以用 shl 来定义 Max_N 等常量。= 6. shr 运算 =和 shl 相似,a shr b 表示二进制右移 b 位(去掉末 b 位) ,相当于 a 除以 2 的 b 次方(取整) 。我们也经常用
4、 shr 1 来代替 div 2,比如二分查找、堆的插入操作等等。想办法用 shr代替除法运算可以使程序效率大大提高。最大公约数的二进制算法用除以 2 操作来代替慢得出奇的 mod 运算,效率可以提高 60%。位运算的简单应用有时我们的程序需要一个规模不大的 Hash 表来记录状态。比如,做数独时我们需要 27个 Hash 表来统计每一行、每一列和每一个小九宫格里已经有哪些数了。此时,我们可以用 27 个小于 29 的整数进行记录。例如,一个只填了 2 和 5 的小九宫格就用数字 18 表示(二进制为 000010010) ,而某一行的状态为 511 则表示这一行已经填满。需要改变状态时我们不
5、需要把这个数转成二进制修改后再转回去,而是直接进行位操作。在搜索时,把状态表示成整数可以更好地进行判重等操作。这道题是在搜索中使用位运算加速的经典例子。以后我们会看到更多的例子。下面列举了一些常见的二进制位的变换操作。功能 | 示例 | 位运算-+-+-去掉最后一位 | (101101-10110) | x shr 1在最后加一个 0 | (101101-1011010) | x shl 1在最后加一个 1 | (101101-1011011) | x shl 1+1把最后一位变成 1 | (101100-101101) | x or 1把最后一位变成 0 | (101101-101100)
6、| x or 1-1最后一位取反 | (101101-101100) | x xor 1把右数第 k 位变成 1 | (101001-101101,k=3) | x or (1 shl (k-1)把右数第 k 位变成 0 | (101101-101001,k=3) | x and not (1 shl (k-1)右数第 k 位取反 | (101001-101101,k=3) | x xor (1 shl (k-1)取末三位 | (1101101-101) | x and 7取末 k 位 | (1101101-1101,k=5) | x and (1 shl k-1)取右数第 k 位 | (11
7、01101-1,k=4) | x shr (k-1) and 1把末 k 位变成 1 | (101001-101111,k=4) | x or (1 shl k-1)末 k 位取反 | (101001-100110,k=4) | x xor (1 shl k-1)把右边连续的 1 变成 0 | (100101111-100100000) | x and (x+1)把右起第一个 0 变成 1 | (100101111-100111111) | x or (x+1)把右边连续的 0 变成 1 | (11011000-11011111) | x or (x-1)取右边连续的 1 | (1001011
8、11-1111) | (x xor (x+1) shr 1去掉右起第一个 1 的左边 | (100101000-1000) | x and (x xor (x-1)最后这一个在树状数组中会用到。Pascal 和 C 中的 16 进制表示Pascal 中需要在 16 进制数前加$符号表示,C 中需要在前面加 0x 来表示。这个以后我们会经常用到。整数类型的储存我们前面所说的位运算都没有涉及负数,都假设这些运算是在 unsigned/word 类型(只能表示正数的整型)上进行操作。但计算机如何处理有正负符号的整数类型呢?下面两个程序都是考察 16 位整数的储存方式(只是语言不同) 。vara,b:
9、integer;begina:=$0000;b:=$0001;write(a, ,b, );a:=$FFFE;b:=$FFFF;write(a, ,b, );a:=$7FFF;b:=$8000;writeln(a, ,b);end.#include int main()short int a, b;a = 0x0000;b = 0x0001;printf( “%d %d “, a, b );a = 0xFFFE;b = 0xFFFF;printf( “%d %d “, a, b );a = 0x7FFF;b = 0x8000;printf( “%d %dn“, a, b );return 0;
10、两个程序的输出均为 0 1 -2 -1 32767 -32768。其中前两个数是内存值最小的时候,中间两个数则是内存值最大的时候,最后输出的两个数是正数与负数的分界处。由此你可以清楚地看到计算机是如何储存一个整数的:计算机用$0000 到$7FFF 依次表示 0 到 32767 的数,剩下的$8000 到$FFFF 依次表示-32768 到-1 的数。32 位有符号整数的储存方式也是类似的。稍加注意你会发现,二进制的第一位是用来表示正负号的,0 表示正,1 表示负。这里有一个问题:0 本来既不是正数,也不是负数,但它占用了$0000 的位置,因此有符号的整数类型范围中正数个数比负数少一个。对一个有符号的数进行 not 运算后,最高位的变化将导致正负颠倒,并且数的绝对值会差 1。也就是说,not a 实际上等于-a-1。这种整数储存方式叫做“补码”。