首先看下Wiki百科对位运算的定义:
位运算是一种通过算法来处理比特或比词短的数据片段的操作。需要位运算的计算机程序任务包括低级设备控制、错误检测和校正算法、数据压缩、加密算法和优化。对于大多数其他任务,现代程序语言允许编程者直接在抽象层面上工作,而不是去操作那些抽象的比特位(即我们现在编程都是用代码写程序,而不是0和1)。在某些情况下,位运算可以避免或者减少在一个数据结构上需要进行循环的次数,并且可以成倍的提升效率,因为位运算是并行处理的,但是缺点是位运算的代码比较难以编写及维护。
在C/C++中可以用的位运算包括:AND、OR、XOR、NOT和位移。
与运算操作符为&,其运算规则为:
操作数一 | 操作数二 | RESULT |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
或运算操作符为|,其运算规则为:
操作数一 | 操作数二 | RESULT |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
异或运算操作符为^,其运算规则为:
操作数一 | 操作数二 | RESULT |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
取反运算操作符为~,其运算规则为:
操作数 | RESULT |
---|---|
0 | 1 |
1 | 0 |
注意事项:
由于在计算机中,二进制的存储形式为补码形式,事实上,计算机中有且仅有补码一种码,而我们所说的原码、反码都是所定义出的为了与补码形成转换关系的格式。正数的原码、反码、补码都是其本身,负数的原码最高位符号位为1,反码除符号位外取反,补码在反码基础上加一得到。例如,正整数5的原码、反码、补码都为0x00000101,对5取反就有0x11111010,由于这是补码表示,转换为反码是0x11111001,转换为原码是0x10000110,即-6为对5取反后的值。
位移运算包括左移(<<)与右移(>>),其运算规则为:
当进行左移操作时,若高位溢出则舍弃,低位空出用0填补;当进行右移操作时,若低位溢出则舍弃,高位空出,正数用0填补,负数用1填补。
如果想将一个指定单元清零,可以通过&0实现,即:
int num = 1; /* Clear the num. */
int res = num & 0; /* Now num is zero. */
如果想取某一个数的指定二进制位,可以通过&1操作实现,例如要保存最低两位的二进制位:
int num = 10; /* num = 00001010 */
int res = num & 3; /* 3 = 00000011 res = 00000010 */
如果想要判断一个数的奇偶性,可以利用其最低位是0还是1来判断:
int num = 10; /* num = 00001010 */
if((num & 1) == 0) printf("这是偶数!");
如果需要判断一个数是否为2的次方数,那么只要该数的二进制形式中1的个数为1:
int num = 8; /* num = 00001000 */
if(num > 0 && (num & (num - 1)) == 0) printf("这是2的次方数!");
需要注意的是,这种用法n&(n-1)十分常见,其作用是将n二进制中最右一位1置0,例如寻找某数二进制表示中有几个1,可以通过n&(n-1)将最右的1置0直到值为0时求得结果。
如果想将一个数的某一二进制位置1,可以通过|1操作实现,例如要将最高两位二进制位置1:
int num = 2; /* num = 00000010 */
int res = num | 24; /* 24 = 00011000 res = 00011010 */
如果想将一个数的某些二进制位翻转,可以通过^1操作实现,例如要将偶数位翻转:
int num = 51; /* num = 01100110 */
int res = num ^ 42; /* 42 = 01010101 res = 00110010 */
异或运算有一个特性为,当某数^0时,仍为原值:
int num = 18; /* num = 00100100 */
int res = num ^ 0; /* res = 00100100 */
异或运算有一个特性为,当某数^自身时,其值为0:
int num = 18; /* num = 00100100 */
int res = num ^ num; /* res = 00000000 */
异或支持交换律:a ^ b = b ^ a
异或支持结合律:(a ^ b) ^ c = a ^ (b ^ c)
通过异或特性2、3及运算律可以推出其自反性:a ^ b ^ b = a ^ (b ^ b) = a ^ 0 = a
我们通常会声明一个中间变量辅助完成两数交换的操作,但是也可以不使用中间变量来做:
int a = 18; /* a = 00100100 */
int b = 27; /* b = 00110110 */
a = a ^ b; /* a = 00010010 */
b = b ^ a; /* b = 00100100 */
a = a ^ b; /* a = 00110110 Now a = 27 b = 18. */
补充一个非位运算的交换方法:
int a = 18;
int b = 27;
a = a + b; /* a = a + b */
b = a - b; /* b = a + b - b = a */
a = a - b; /* a = a + b - a = b */
通常我们变换符号会使用0减去某数的方式,但是通过位运算也可以做到:
int num = 5;
int res = ~num + 1; /* res = -5 */
按位取反的快捷公式为:~num=-(num+1)
通过取反操作可以取绝对值:
int num_one = 12;
int num_two = -15;
if ((num_one >> 31) == 0 ? res = num_one : res = (~num_one + 1));
if ((num_two >> 31) == 0 ? res = num_two : res = (~num_two + 1));
对任何数,与0异或不会变化,与-1异或相当于取反(注意,计算机中用补码存储),因此,num与mask异或(正数不变,负数取反)后再减去mask(正数减0,负数减-1),即另一种方法:
int num = -10;
int mask = num >> 31;
int res = (num ^ mask) - mask;
若左移时舍弃的高位不包含1,则每左移一位,相当于乘2:
int num = 5;
int res = num << 2; /* res = 20 */
每右移一位,相当于除2:
int num = 20;
int res = num >> 2; /* res = 4 */
给定一个数组,有且仅有一个值出现了1次,其余均出现了2次,找出其中出现1次的值:
int singleNumber(vector<int>& nums) {
int res = 0;
for(int i = 0; i < nums.size(); i++){
res ^= nums[i];
}
return res;
}
题目链接
给定一个值为0到n的数组,找出其中缺失的某个数字:
int missingNumber(vector<int>& nums) {
int res = 0;
for(int i = 0; i < nums.size(); i++){
res ^= (i + 1) ^ nums[i];
}
return res;
}
题目链接
实现一个函数,可以返回无符号整型数中比特位为1的个数(又称:Hamming Weight):
int hammingWeight(uint32_t n) {
int num = 0;
while(n != 0) {
n &= (n - 1);
num++;
}
return num;
}
题目链接
实现一个函数用于判断某数是否为2的次方:
bool isPowerOfTwo(int n) {
if(n <= 0) return false;
if(n == 1) return true;
int res = n & (n - 1);
if(res == 0) return true;
else return false;
}
题目链接
不使用+或-号完成两数相加的操作:
int getSum(int a, int b) {
return b == 0 ? a : getSum(a ^ b, (unsigned int)(a & b) << 1);
}
题目链接
按位取反运算解析
按位取反快捷公式推理证明
LeeCode-LHearen
位运算总结
位运算小结
位运算在算法中的应用小结
位运算一些例题总结
优秀程序员不得不知道的20个位运算技巧
位运算常用技巧总结
算法技巧-带你领略位运算的魅力
位运算技巧总结
位运算简介及实用技巧一:基础篇
位运算简介及实用技巧二:进阶篇上
位运算简介及实用技巧三:进阶篇下
位运算简介及实用技巧四:实战篇
知乎话题:位运算有什么奇技淫巧
位运算求两数平均值