在最近学习C语言,接触到不少底层知识。之前一直用Java开发,很少会用到像位运算这样的操作。
通过C语言的学习,才发现位运算真是奇妙,除了简单的类似位反转等基本操作,还可以加密编码,
交换变量值,甚至在磁盘阵列RAID中都有它的身影。每个位运算的问题,都像在设计一套集成电路
一样有趣。让我们一起来领略下有趣的位运算吧!
1. 从布尔代数说起
布尔代数定义了与、或、非等基本运算,是位运算的基础。
但位运算要更加复杂,因为涉及到了多位运算,并且分为逻辑运算与算术运算。
位运算可以简单地看做布尔代数中的逻辑运算。
转载一段不错的对位运算与布尔代数关系的描述:
“位运算是计算机最擅长的计算方式,尽管从广义上说,位运算仅仅是布尔代数中的一小部分,但是现实中,两者却不能画等号,
根本的原因是计算机中的位运算是多位的,而且是逻辑运算和算术运算混合的,而常规的布尔代数只研究真值和假值的逻辑运算
长期以来,数学专家对位运算是不屑一顾的,认为已经没有什么可研究的了,只有少数的计算机专家才对这个问题无比的着迷,
因为这就是计算机的思考方式.这里说的少数确实是相当的少,在我的印象中,也只有<<Hacker's Delight>>这一本专门的著作,
不过说实在的,就这一本书也更象是一本笔记之类的东西,只有很少的证明,更多的只是结论,这到也是程序员的思考方式:)
根本的原因是计算机中的位运算是多位的,而且是逻辑运算和算术运算混合的,而常规的布尔代数只研究真值和假值的逻辑运算
长期以来,数学专家对位运算是不屑一顾的,认为已经没有什么可研究的了,只有少数的计算机专家才对这个问题无比的着迷,
因为这就是计算机的思考方式.这里说的少数确实是相当的少,在我的印象中,也只有<<Hacker's Delight>>这一本专门的著作,
不过说实在的,就这一本书也更象是一本笔记之类的东西,只有很少的证明,更多的只是结论,这到也是程序员的思考方式:)
计算机只认识两种数字,0和1,由0和1组成的映射就是布尔代数,基本上它相当于f(x,y)=z;其中x,y,z=0或者1,其中的f就是布尔函数,
因为取值范围仅仅是0和1,数学上f也称之为2度布尔函数。因为x可以取0或者1,y也可以取0或者1,x和y的组合是4种00,01,10,11,
因为z也可以取0或者1,因此2度布尔函数的组合数目是2的2次方的2次方=16种,也就是说有16个映射关系,刚好是16进制的从0到F。
但是我们熟悉的布尔运算其实只有三种,与,或,非。可以证明仅仅用这三种运算关系就可以表达16种映射关系,数学上这称之为完备集。”
2. C语言中的位运算
C语言提供了6种位运算符:
& 按位与
| 按位或
^ 按位异或
<< 左移
>> 右移
~ 按位求反
这些运算符只能作用于整型操作数。有符号的或无符号的。
&经常用于屏蔽某些二进制位,|常用于将某些位置为1。
^当两个操作数的对应位不同时将该位设置为1。
<<和>>
对有符号的负整数进行右移操作要注意:
如果是负数,那么高位移入1还是0不一定。对于x86平台的
gcc
编译器,最高位移入1,也就是仍保持负数的符号位,这种处理方式对负数仍然保持了“右移1位相当于除以2”的性质。
~求整数的二进制反码,~0可获得与机器字长无关的一串1。
3. 位运算的简单应用
经典的《C程序设计语言》中的一道例题。
3.1 实现函数getbits(x, p, n),p=4,n=3,返回x中第4、3、2三位的值。(最低位是第0位)
分析:要想只返回这三位的值,要利用&运算屏蔽作用。
为了方便提取,将所需的n位右移至最右端(低位),x >> (p-n+1)。
然后要产生与平台无关的掩码,~(~0 << n)。
这样通过&运算后,除低n位外,其他位均变成了0。
答案:return (x >> (p-n+1)) & ~(~0 << n);
3.2 再来看今天从网上看到的这道搜狗的笔试题。要求完成decode函数,实现解码功能。
- public static void encode(byte[] in, byte[] out, int password) {
- int len = in.length;
- int seed = password ^ 0x3e1e25e6;
- for (int i = 0; i < len; ++i) {
- byte a = (byte) ((in[i] ^ seed) >> 3);
- byte b = (byte) (((((int) in[i]) << 18) ^ seed) >>> (18 - 5));
- a &= 0x1f;
- b &= 0xe0;
- out[i] = (byte) (a | b);
- seed = (seed * 84723701 ^ seed ^ out[i]);
- }
- }
- public static void decode(byte[] in, byte[] out, int password) {
- int len = in.length;
- int seed = password ^ 0x3e1e25e6;
- for (int i = 0; i < len; ++i) {
- // fill the code here
- }
- }
- public static void main(String[] args) throws Exception {
- int password = 0xfdb4d0e9;
- byte[] buf1 = { -5, 9, -62, -122, 50, 122, -86, 119, -101, 25, -64,
- -97, -128, 95, 85, 62, 99, 98, -94, 76, 12, 127, 121, -32,
- -125, -126, 15, 18, 100, 104, -32, -111, -122, 110, -4, 60, 57,
- 21, 36, -82, };
- byte[] buf2 = new byte[buf1.length];
- decode(buf1, buf2, password);
- System.out.println(new String(buf2, "GBK"));
- }
通过分析encode函数可以看出,明文的高5位经过与seed异或后成了结果的低5位。
需要通过相反的运算来还原它们,以及明文中的低3位。
注意seed也要随着循环一起变化,而且要与encode中的变化保持一致。
在encode中,seed = (seed * 84723701 ^ seed ^ out[i]) 其中out[i]是密文。
因此在decode中,seed = (seed * 84723701 ^ seed ^ in[i])
out[i]是还原出的明文,in[i]才是密文。
- public static void decode(byte[] in, byte[] out, int password) {
- int len = in.length;// encode中的out[i]是这里decode中的in[i]
- int seed = password ^ 0x3e1e25e6;
- for (int i = 0; i < len; ++i) {
- byte a = (byte) (in[i] & 0x1f);
- byte b = (byte) (in[i] & 0xe0);
- a = (byte) (((a <<3) ^ seed) & 248);
- b = (byte) ((((((int) b) << (18 - 5)) ^ seed) >> 18) & 7);
- out[i] = (byte) (a | b);
- seed = (seed * 84723701 ^ seed ^ in[i]);
- }
- }
- // 答案是“真双核引擎是全球最快的浏览器内核!!!!”
经过这一段对底层和C语言学习,对Java的理解加深了不少,我也能做出这道题了,小有成就感,:)
看到这样的题目不要被吓到,掌握了基础知识,只需简单的分析encode中的位运算,然后写成反向过程即可。
所以还是要打好基础!
3.3 一些很不错的归纳总结
(1) 判断int型变量a是奇数还是偶数
a&1 = 0 偶数
a&1 = 1 奇数
(2) 取int型变量a的第k位 (k=0,1,2……sizeof(int)),即a>>k&1 (先右移再与1)
(2) 取int型变量a的第k位 (k=0,1,2……sizeof(int)),即a>>k&1 (先右移再与1)
(3) 将int型变量a的第k位清0,即a=a&~(1<<k) (10000 取反后为00001 )
(4) 将int型变量a的第k位置1,即a=a|(1<<k)
(5) int型变量循环左移k次,即a=a<<k|a>>16-k (设sizeof(int)=16)
(6) int型变量a循环右移k次,即a=a>>k|a<<16-k (设sizeof(int)=16)
(7)整数的平均值
对于两个整数x,y,如果用 (x+y)/2 求平均值,会产生溢出,因为 x+y 可能会大于INT_MAX,但是我们知道它们的平均值是肯定不会溢出的,我们用如下算法:
- int average(int x, int y) //返回X、Y的平均值
- {
- return (x & y) + ( (x^y)>>1 );
- }
(8)对于一个数 x >= 0,判断是不是2的幂。
- boolean power2(int x)
- {
- return ( (x&(x-1))==0) && (x!=0);
- }
(9)不用temp交换两个整数
- void swap(int x , int y)
- {
- x ^= y;
- y ^= x;
- x ^= y;
- }
(10)计算绝对值
- int abs( int x )
- {
- int y ;
- y = x >> 31 ;
- return (x^y)-y ; //or: (x+y)^y
- }
(11)取模运算转化成位运算 (在不产生溢出的情况下)
a % (2^n) 等价于 a & (2^n - 1)
a % (2^n) 等价于 a & (2^n - 1)
(12)乘法运算转化成位运算 (在不产生溢出的情况下)
a * (2^n) 等价于 a<< n
(13)除法运算转化成位运算 (在不产生溢出的情况下)
a / (2^n) 等价于 a>> n
例: 12/8 == 12>>3
(14) a % 2 等价于 a & 1
(15) if (x == a)
x= b;
else
x= a;
等价于 x= a ^ b ^ x;
等价于 x= a ^ b ^ x;
(16) x 的 相反数 表示为 (~x+1)
(17)输入2的n次方:1 << 19
(18)乘除2的倍数:千万不要用乘除法,非常拖效率。只要知道左移1位就是乘以2,右移1位就是除以2就行了。比如要算25 * 4,用25 << 2就好啦
4. 神奇的异或运算
异或运算有些不错的特性,《Linux C编程一站式学习》中有些不错的总结。
4.1 一个数和自己做异或的结果是0。很简单,因为每一位都一定相同。
x86平台的编译器可能会生成这样的指令:
xorl %eax, %eax
。不管eax
寄存器里的值原来是多少,做异或运算都能得到0,这条指令比同样效果的movl
$0, %eax
指令快,直接对寄存器做位运算比生成一个立即数再传送到寄存器要快一些。4.2 从异或的真值表可以看出,不管是0还是1,和0做异或保持原值不变,和1做异或得到原值的相反值。
可以利用这个特性配合掩码实现某些位的翻转。
《C程序设计语言》中练习2-7 编写一个函数invert(x, p, n),返回对x执行下列操作后的结果值:
将x中从第p位开始的n位求反,x的其余各位保持不变。
便可以利用这条特性,产生000..01110..000这样的掩码:x & (~(~0 << n)) << (p-n+1);
4.3 如果a1 ^ a2 ^ a3 ^ ... ^ an的结果是1,则表示a1、a2、a3...an之中1的个数为奇数个,否则为偶数个。这条性质可用于奇偶校验(Parity Check),比如在串口通信过程中,每个字节的数据都计算一个校验位,数据和校验位一起发送出去,这样接收方可以根据校验位粗略地判断接收到的数据是否有误。
特意查阅了下RAID的资料,发现异或运算真是神通广大,从微观的对位进行反转、交换等运算,到宏观上的磁盘阵列都有它的身影。
以RAID3为例,使用硬盘D作为独立的奇偶盘。保存着其他硬盘异或的结果。
硬盘 A B C 奇偶盘 (A, B, C 异或的结果)
数据 1 0 1 0
数据 1 0 1 0
假设A, B, C中B盘故障,此时可将A, C和奇偶数据XOR起来,得到B盘失去的数据0;同样如C盘故障,我们可将A, B盘和奇偶盘的数据XOR,得到C盘原先的数据1。
4.4 x ^ x ^ y == y,因为x ^ x == 0,0 ^ y == y。这个性质有什么用呢?我们来看这样一个问题:交换两个变量的值,不得借助额外的存储空间,所以就不能采用
temp = a; a = b; b = temp;
的办法了。利用位运算可以这样做交换:a = a ^ b;
b = b ^ a;
a = a ^ b;
经典的题目,不懂位运算的话真是难以想象可以不通过中间变量来交换两个变量的值,神奇呀!
参考书目
《离散数学及其应用(第五版)》 第10章 布尔代数
《C程序设计语言(第二版)》
《Linux C编程一站式学习》 第16章 运算符详解
RAID磁盘阵列数据恢复--硬盘分段和数据冗余 http://www.vstcn.net/FuWuQiShuJuHuiFuShow.asp?ID=27