位操作的个人总结
在计算机中所有数据都是以二进制形式进行存储,而位运算就是直接对内存中的二进制数据进行操作,因此处理速度非常快。
1. 基本操作
运算符 | 用法示例 | 运算规则 |
按位与 AND | a & b | 只有两个操作数相应的比特位都为1时,结果才为1,否则为0 |
按位或 OR | a | b | 只有两个操作数相应的比特位都为0时,结果才为0,否则为1 |
按位异或 XOR | a ^ b | 两个操作数相应比特位不相同时,结果为1,否则为0 |
按位取反 NOT | ~a | 操作数相应比特位取反,0变为1, 1变为0 |
左移 | a << b | 将a的二进制形式向左移b个bit,右侧填充0 |
右移 | a >> b | 将a的二进制形式向右移b个bit,有符号数逻辑移位,无符号数算术移位 |
C/C++中移位运算包含逻辑移位(Logical shift)和算术移位(Arithmetic shift)两种,其中逻辑移位的意思是,移出去的位直接舍弃,空缺位用0填充;算术移位的意思是,移出去的位直接舍弃,空缺位用符号位填充。
- 对于无符号数,无论是左移还是右移都是逻辑移位,如0011 0110 左移两位结果为1101 1000,右移两位结果为0000 1101。
- 对于有符号数,左移仍然是逻辑移位,右移则略有不同,进行的是算术移位,如1011 0110 左移两位结果为1101 1000,右移两位结果为1110 1101(前面两位填充的是符号位1)。
2. 常见应用
对n的指定位进行操作,其它位保持不变。
1 n &= ~(1 << 5); // 1左移5位得到 0010 0000, 按位取反得 1101 1111,与n按位与使其第6位被清零,其它位不变 2 n |= (1 << 5); // 1左移5位得到 0010 0000, 与n按位或使其第6位被置1,其它位不变 3 n ^= (1 << 5); // 将n第6位取反,其它位不变
判断给定正整数n的奇偶
1 1 bool flag = a & 1; // a对应二进制数末位为0则为偶数,否则为奇数。相比 bool flag = (a % 2 == 0); 运算速度快了很多。
判断n是否为2的正整数次幂
1 bool isPowerOfTwo(int n) { 2 return (n & (n - 1) == 0); // 将2的次幂写成二进制容易发现,二进制中只有一个1,后面跟了n个0。如果将这个数减1,仅有的一个1变成了0,后面的n个0变成了1。时间复杂度O(1)。 3 }
1 bool isPowerOfTwo(int n) { 2 while (n % 2 ==0) { // 常规思路:利用循环判断n是否能被2整除,如果是除以2,继续循环。最终结果若为1则表明其是2的正整数次幂,否则不是。时间复杂度O(log n)。 3 n /= 2; 4 } 5 return (n == 1); 6 }
统计给定正整数的二进制中1的个数
1 int count1Bit(int n) { 2 int countOneBit = 0; 3 while (n) { 4 countOneBit ++; 5 n &= n -1; // 与上述判断n是否为2的正整数次幂时相似,此处n&(n -1) 的作用是将n的二进制中最右边的1置位0,如此循环从而统计n中1的个数
6 } 7 return countOneBit; 8 }
1 int count1Bit(int n) { 2 n = ((n & 0xAAAA) >> 1) + (n & 0x5555); // 另一种四步分组法,以34520(1000011011011000)为例,第一步:每2位为一组,组内高低位相加。10 00 01 10 11 01 10 00 -> 01 00 01 01 10 01 01 00(10高位为1,低位为0,相加得01;11高位为1,低位也为1,相加得10……)。 3 n = ((n & 0xCCCC) >> 2) + (n & 0x3333); // 第二步:将第一步得到的结果每4位分为一组,组内高低位相加。0100 0101 1001 0100 -> 0001 0010 0011 0001(0100高位为01低位为00,相加得01;1001高位为10低位为01,相加得11……)。 4 n = ((n & 0xF0F0) >> 4) + (n & 0x0F0F); // 第三步:将第二步得到的结果每8位分为一组,组内高低位相加。00010010 00110001 -> 00000011 00000100。 5 n = ((n & 0xFF00) >> 8) + (n & 0x00FF); // 第四步:将第三步得到的结果每16位分为一组,组内高低位相加。0000001100000100 -> 00000111。这样最后得到的结果7即为给定整数中1的个数 6 return n; 7 }
不需要额外变量交换两个整数的值
1 void Swap(int &m, int &n) { 2 if ( m != n ) { 3 m ^= n; // m = (m ^ n) 4 n ^= m; // n = n ^ (m ^ n) = n ^ m ^ n = n ^ n ^ m =m, 一个数和自身异或结果为0,一个数和0异或结果为其自身 5 m ^= n; // m = m ^ n = m ^ n ^ m = n 6 } 7 }
1 void Swap(int &m, int &n) { 2 if ( m != n ) { 3 int temp = a; // 常规思路:借由中间变量实现交换。 4 a = b; 5 b = temp; 6 } 7 }
16位无符号整型数高低位交换
unsigned int lowHighExchange(unsigned int n) { return ((a >> 8) + (a << 8)); // 对于16位无符号整型数据,分为高8位和低8位,高八位右移时高位填充0,低八位左移时末位填充0,只需将两者相加即可得到所求结果。 }
二进制逆序操作
int binaryReverse( int n ) { n = ((n & 0xAAAA) >> 1) | ((a & 0x5555) << 1); // 通过四步分组法得到16位整型数据的二进制逆序。以34520(1000 0110 1101 1000)为例,第一步:每2位为一组,组内高低位交换。10 00 01 10 11 01 10 00 -> 01 00 10 01 11 10 01 00。
// 第二步:每4位为一组,组内高低位交换。0100 1001 1110 0100 -> 0001 0110 1011 0001。
// 第三步:每8位为一组,组内高低位交换。00010110 10110001 -> 01100001 00011011。
// 第四步:每16位为一组,组内高低位交换。0110000100011011 -> 00011011 01100001。完成逆序。 n = ((n & 0xCCCC) >> 2) | ((a & 0x3333) << 2); // 改进:对第一步先分别取原数据的奇数位和偶数位,空位以下划线表示:1_0_0_1_1_0_1_0_, _0_0_1_0_1_1_0_0, 将下划线填充0得原数 1000 0110 1101 1000, 奇数位 1000 0010 1000 1000, 偶数位 0000 0100 0101 0000
// 再将奇数位右移一位,偶数位左移一位,将移位后的两数按位或可使奇偶位数据交换,原数 1000 0110 1101 1000, 奇数位右移一位 0100 0001 0100 0100, 偶数位左移一位 0000 1000 1010 0000,按位或得 0100 1001 1110 0111 n = ((n & 0xF0F0) >> 4) | ((a & 0x0F0F) << 4); return (((n & 0xFF00) >> 8) | ((a & 0x00FF) << 8));}
3. 拓展应用
题目:不实用加减乘除四则运算实现两个正整数相加。
解析:首先理解十进制加法工作原理,主要分为三步,以4+9为例:1)相加相应位的值,不算进位,得3;2)计算进位值,得10,如果进位值为0则第一步得到的就是最终结果;3)把前两步的结果相加,重复此过程得结果13。
再来看二进制相加过程。1)相加相应位的值,不算进位,100 + 1001,得1101;2)计算进位值,得0000,如果进位为0则第一步得到的就是最终结果;3)把前两步的结果相加,重复此过程的结果1101。
1 int Add(int num1, int num2) { 2 return num2 ? Add(num1 ^ num2, (num1 & num2)<<1) : num1; // 相加相应位的值,可以使用按位异或,因为如果不考虑进位的和,只有0+1或者1+0才是1,这刚好符合异或的性质:0100 ^ 1001 = 1101
// 计算进位可以使用按位与,因为只有1+1才会发生进位并需要将这个进位左移1位:(0100 & 1001) << 1 = 0000,然后一直循环直到进位为0,此时的结果就是输入两数之和了。 3 }
题目:给定一个非空整数数组,其中有一个元素只出现一次,其它元素均出现两次,请找出只出现一次的元素。(要求实现算法具有线性时间复杂度,并且不实用额外空间)
示例:输入[2, 2, 1],输出1。输入[3, 1, 2, 3, 1],输出2。
解析:由于要求时间复杂度O(n),空间复杂度O(1),不能用排序,也不能使用map。考虑使用位操作运算求解。因为任何数与自身异或结果为0,任何数与0异或结果为其自身,将所有元素做异或运算,即a[1]⊕a[2]⊕…⊕a[n],那么结果就是只出现一次的元素,时间复杂度O(n)。过程如下图:
题目:给定一个非空整数数组,其中有两个元素只出现一次,其它元素均出现两次,请找出只出现一次的两个元素。(要求实现算法具有线性时间复杂度,而且只能开辟固定大小的内存空间,即与n无关)
示例:输入[1, 2, 2, 1, 3, 4],输出[3, 4]。
解析:根据前面找一个不同数的思路,如果这里再把所有元素异或,得到的结果是只出现一次的两个元素异或得到的值。然后由于这两个只出现一次的元素一定不相同,那么这两个元素的二进制形式肯定至少有一位不同,即1个为0,另一个为1,先在需要找出这一位。根据异或的性质:任何数与自身异或结果为0,得到这个数字二进制形式中任意一个为1的位都是我们要找的。之后以这一位是0还是1为标准,将数组的n个元素分成两部分。将这一位为0的所有元素异或得到的结果就是只出现一次的两个元素中的一个。将这一位为1的所有元素异或得到的结果就是只出现一次的两个元素中的另一个。如果忽略寻找不同位的过程,公遍历数组两次,时间复杂度O(n)。过程如下图:
Reference:
[1] https://www.cnblogs.com/zhoug2020/p/4978822.html
[2] https://blog.csdn.net/qq_16137569/article/details/82790378
[3] https://www.cnblogs.com/thrillerz/p/4530108.html
[4] https://www.cnblogs.com/fivestudy/p/10275446.html
[5] https://www.nowcoder.com/practice/59ac416b4b944300b617d4f7f111b215?tpId=13&tqId=11201&tPage=3&rp=3&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking