位运算简介与应用例子
简介
位运算:数据在计算机中以01二进制的形式储存,位运算就是直接对这二进制数据进行运算
几种位运算及运算示例如下:
首先是与、或、异或、取反:
对于bool:
&
与0&0=0 0&1=0 1&0=0 1&1=1
|
或0|0=0 1|0=1 1|0=1 1|1=1
^
异或0^0=0 1^0=1 0^1=1 1^1=0
~
取反:~0=1; ~1=0
对于int的运算:(按int 占四个字节32比特来看
对于int的与/或/异或/取反(此处均为正整数且数字较小,前边的28位二进制位均为0不影响运算略去)就是对两个int型数据的二进制的对应的每一位进行上面的运算
对于(signed )int:
3 & 5 = 0b0011 & 0b0101 = 0b0001= 1
3 | 5 = 0b0011 | 0b0101 = 0b0111 = 7
3 ^ 5 = 0b0011 ^ 0b0101 = 0b0110 = 6
对于unsigned int:
3 & -5 = 0x00000003 & 0xfffffffb = 0x00000003 = 3
3 | -5 = 0xfffffffb = -5
3 ^ -5 = 0xfffffff8 = -8
对于(signed )int: ~0=-1; ~1=-2
~0=~0x00000000=0xffffffff=-1
~1=~0x00000001=0xfffffffe=-2
~3=~0x00000003=0xfffffffc=-4
~(-5)=~0xfffffffb=0x00000004=4
(正的int型变量n取反相当于-(n+1)
对于unsigned int :~0=4294967295;~1=4294967294
~0=~0x00000000=0xffffffff=4294967295
~1=~0x00000001=0xfffffffe=4294967294
(正的unsigned int型变量n取反相当于(4294967295-n)
然后是左移、右移运算符:
5. <<
左移(符号前的数的二进制整体向左移动符号后的数位,左侧超出范围的忽略,右侧补零) 1<<0=1 1<<1=2 1<<2=4
(0b0001<<1=0b0010=2)
6. >>
右移(左侧补零) 4>>0=4 4>>1=2 4>>2=1 4>>3=0 4>>4=0
(同左移)
经测试:
对于(signed )int:当0<=n<=30时,1<<n就是2的n次幂。而1<<31=0x00000001<<31=0x80000000=-2147483648
,说明左移运算是会把‘1’移动到符号位上的。当n>=32时,1<<n = 0。
上述运算对于char, short int, long, long long类似,只是二进制位数不同
位运算不适用于float,double等
一些应用示例
对于一些简单的函数,可以加上inline;或者在程序中直接用,不再封装成函数
- 判断数字奇(odd)偶(even)
如果数字二进制的最后一位是1,数字就是奇数,是0就是偶数。负数也可以,-1&1=0xffffffff & 0x000001 = 0x000001 = 1
;-2&1=0;-3&1=1;…
inline bool isOdd(int n){return n&1;} // 是否是奇数
inline bool isEven(int n){return !(n&1);} // 是否是偶数
- 大小写字母转换
比如
‘A’=65=0b1000001
‘a’=97=0b1100001
因为同一个字母大小写的ASCII码值正好差了97-65=32=2^5=0x20,二进制就只有一位不同。
如果确定输入为大写或小写字母,可以用下面的:
inline char _lower(char c){return c+0x20;}
inline char _upper(char c){return c-0x20;}
或者
inline char _lower(char c){return c|0x20;}
inline char _upper(char c){return c&0xdf;}
不确定的话:
// 对于不正确的输入,原样返回不做处理
char tolower(char c){return (c>='A' && c<='Z')?(_lower(c)):c;}
char toupper(char c){return (c>='a' && c<='z')?(_upper(c)):c;}
- “字节翻转”
比如int n=0x12345678;想要int m=0x78563412;或者想要int l=0x87654321;
甚至char c=0b01011011;想要char h=0b11011010;
//注 下面的12345678代表char二进制的八位,不代表二进制的值
// 注: 不考虑最高位表示正负,连带着最高位一起翻转
char revChar(char c){ //c=0b12345678;
c=(c<<4)|(c>>4); //c=0b56781234;
c=((c&0x33)<<2)|((c&0xcc)>>2);//c=0b78563412;
c=((c&0x55)<<1)|((c&0xaa)>>1);//c=0b87654321;
return c;
}
// 其他如int,long等同理,按int占4字节算
int revInt(int n){ // n=0x12345678;
n=(n<<16)|(c>>16); //0x56781234;
n=((n&0x00ff00ff)<<8)|((n&0xff00ff00)>>8); //0x78563412;
// 注:到这里是按字节翻转,把四个字节反着来了
// 注:根据需要还可以继续翻转,如下
// 注: 0x3=0b0011,0x5=0b0101,0xa=0b1010,0xc=0b1100
// n=((n&0x0f0f0f0f)<<4)|((n&0xf0f0f0f0)>>4); //0x87654321;
// n=((n&0x33333333)<<2)|((n&0xcccccccc)>>2);
// n=((n&0x55555555)<<1)|((n&0xaaaaaaaa)>>1);
// 注:到这里是每个比特都翻转了
return n;
}
可根据自己需要,自行更改变量类型和翻转几次翻转到什么程度。
- 对int,设置某一位,取某一位的值
// 对于int占四字节,0b00000000 00000000 00000000 00000000
// 从右到左的32个比特依次设为int的第0到第31位,当然也可以设为1-32
// 下面的ind应该大于等于0,小于32;(这样写总是可以运行,
void setBite(int n, unsigned int ind, bool f){
// f为真:把n的第ind位设为1
// f为假:把n的第ind位设为0
if(f) n |= 1<<ind;
else n &= ~(1<<ind);
}
inline bool getBite(int n, unsigned int ind){
// 获取n的第ind位
return (n>>ind)&1; // 括号可以省略
}
inline void revBite(int n, unsigned int ind){
// 对n的第ind位取反
n ^= 1<<ind;
}
可以把int n改成long n等等;也可以写到#define那里
5. 统计二进制中‘1’的个数
(记得有个前面带”_“的内置函数可以的,但找不到了)
// 取最后面一个'1'
inline int getLastOne(int n){return n&(-n);}
// 比如n=12=0x0000000c -n=-12=0xfffffff4 12&(-12)=0x00000004=4
// 其中0xc & 0x4 = 0b1100 & 0b0100 = 0b0100 = 4
// 去掉最后面一个'1'
inline return f1(int n){return n&(n-1);}
inline return f2(int n){return n-n&(-n);}
// 统计n中'1'的个数,一个示例。(方法很多,
int numOfOne(int n){
int ret = 0;
while(n){n=n&(n-1);++ret;}
return ret;
}
下面的代码来自:维基百科:汉明权重
来自:Colin丶c语言:统计整数二进制表示中1的个数(汉明重量)
把维基百科里64bit的改成了4字节int的32bit的
int numOfOne(int n){
n = (n&0x55555555) + ((n>>1)&0x55555555);
n = (n&0x33333333) + ((n>>2)&0x33333333);
n = (n&0x0f0f0f0f) + ((n>>4)&0x0f0f0f0f);
n = (n&0x00ff00ff) + ((n>>8)&0x00ff00ff);
n = (n&0x0000ffff) + ((n>>16)&0x0000ffff);
return n;
}
类似分治,把32bit每个比特为一组,两两相加
0b0 0 1 1 0 1 1 0 - 0 0 0 0 0 0 0 0 00000000 0000000 0
--1 2 3 4 5 6 7 8 - 910111213141516-................ 32
从左到右依次编号1-32
先把,1和2,3和4,5和6,…31和32处的,0或者1相加,结果存入相应位置
比如,0+0=00存入编号1和2的位置(表示12位置没有1),1+1=10(二进制)存入3和4的位置,0+1=01存入5和6的位置。变为这样:
0b0 0 1 0 0 1 0 1 - 0 0 0 0 0 0 0 0 00000000 0000000 0
--1 2 3 4 5 6 7 8 - 910111213141516-................ 32
然后两个比特一组,两两相加
比如00+10=0010存入编号1234的位置(表示1234位置有两个1),01+01=0010存入编号5678的位置…
然后四个比特一组,两两相加
比如0010+0010=00000100存入12345678的位置(表示12345678位置有4个1)
然后八个比特一组,两两相加;最后16个比特一组两两相加,存入n
n的大小就是这32比特中1的个数。
7.
比如取int的低4位 n&0xf
;低8位 n&0xff
;低9-16位 (n&0xff00)>>8
8. 最后,float和double类型也不是不能进行位运算,但是数字的表示方法不一样
对于4字节的float是:1比特符号位,8比特指数位,23比特的小数位
float f=1.0;
// 0b-0-01111111-00000000000000000000000
// 符号位0表示正数,指数位为127-127=0,小数位为1.0,
// 指数要减2^7-1=127,double的指数要减2^10-1=1024
// 因为都是 1.???,省去了前面的1
// +1*1.0*2^0=1.0
int * n = (int *)&f;
// 如果直接输出printf("%x",*n);可以得到 0x3f800000=0b00111111 10000000 ...
(*n) |= (1<<31); // 这样f就变成了-1.0,因为将符号位设为了1
(*n) |= (0xf<<23); // -1*1.0*2^(255-127) --> inf 指数位设为了11111111
(*n) &= ~(0b01111110<<23); // -1*1.0*2^2=-4.0 指数位设为了10000001
对于小数位,从紧挨着指数位的那一位算起,那一位为1表示要加2^(-1)=0.5变为1.5,往远离指数位的方向依次计算,
比如0b 0 01111111 11010000000000000000000
表示+1*(1.0+0.5+0.25+0+0.0625)*2^0=1.8125
以上,就知道了float32位的每一位都代表什么,可以进行操作实现自己想要的效果了。
有一个例子:
以下来自https://h14s.p5r.org/2012/09/0x5f3759df.html
虽然有小误差,但运算速度比较快。
// 计算一个float的二分之一次方的倒数,即1/sqrt(x)
float FastInvSqrt(float x) {
float xhalf = 0.5f * x;
int i = *(int*)&x;
i = 0x5f3759df - (i >> 1);
x = *(float*)&i;
x = x*(1.5f-(xhalf*x*x));
return x;
}
对于8字节的double:1比特符号位,11位指数位,52位小数位。类似float
如有错误,欢迎指正