一、基础
1.1 概述
位运算是一种在计算机编程中直接操作二进制位的操作方式。它可以高效地处理数据,并且在某些场景下比普通的算术运算更快。位运算通常用于低级编程、硬件编程、性能优化和加密算法等领域。
优势:1.速度快;2.节省寄存器/内存空间
1.2 原码、反码、补码
1.2.1 原码
原码是一种简单的表示方法,使用最高位(符号位)表示正负号,剩余位表示数值的绝对值。
符号位:0表示正数,1表示负数。
数值位:表示数值的绝对值。
1.2.2 反码
反码通过将原码的数值位按位取反得到,符号位保持不变。
符号位:与原码相同。
数值位:正数的反码与原码相同,负数的反码是将原码的数值位按位取反。
1.2.3 补码
补码是现代计算机系统中最常用的有符号整数表示方法。它通过将反码加1得到。
符号位:与原码相同。
数值位:正数的补码与原码相同,负数的补码是将原码的数值位按位取反再加1。
1.2.4 示例对比
十进制 | 原码 | 反码 | 补码 |
---|---|---|---|
+5 | 00000101 | 00000101 | 00000101 |
-5 | 10000101 | 11111010 | 11111011 |
1.3 基本运算符
1.3.1 按位与(&)
对每个二进制位执行逻辑与操作。
对应位置都为1,结果才为1,否则为0。
示例:13 & 11 = 9
十进制 | 二进制 | |||||||
---|---|---|---|---|---|---|---|---|
13 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
11 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
9 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
运算律:
- 交换律:A & B = B & A
- 结合律:A & (B & C) = (A & B) & C
- 恒等律:A & 0 = 0
- 同一律:A & A = A
- 消去律:A & ~A = 0
- 分配律:A & (B | C) = (A & B) | (A & C)
1.3.2 按位或(|)
对每个二进制位执行逻辑或操作。
对应位置只要有一个为1,结果就为1。
示例:13 | 11 = 15
十进制 | 二进制 | |||||||
---|---|---|---|---|---|---|---|---|
13 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
11 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
15 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
运算律:
- 交换律:A | B = B | A
- 结合律:A | (B | C) = (A | B) | C
- 恒等律:A | 0 = A
- 同一律:A | A = A
- 消去律:A | ~A = 1
- 分配律:A | (B & C) = (A | B) & (A | C)
1.3.3 按位异或(^)
对每个二进制位执行逻辑异或操作。
对应位置不同,结果为1,相同为0。
示例:13 ^ 11 = 6
十进制 | 二进制 | |||||||
---|---|---|---|---|---|---|---|---|
13 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
11 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
6 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 |
运算律:
- 交换律:A ^ B = B ^ A
- 结合律:A ^ (B ^ C) = (A ^ B) ^ C
- 恒等律:A ^ 0 = A
- 自反律:A ^ A = 0
- 消去律:A ^ ~A = 1
1.3.4 按位取反(~)
对每个二进制位执行逻辑取反操作。
对每一个位进行取反操作。
用途:按位取反。
运算律:
- 双重否定律:~~A = A
- 德摩根定律:~(A & B) = ~A | ~B
~(A | B) = ~A & ~B
1.3.5 左移(<<)
将二进制位左移指定的位数。
将一个数(有、无符号数)的二进制位左移指定的位数,右边补0。
注意,对于int类型的变量,左移超过31位(假设int为32位)会导致未定义行为。
1.3.6 右移(>>)
将二进制位右移指定的位数。
将一个数的二进制位右移指定的位数。
算术右移(对于有符号数):用符号位填充左侧。
逻辑右移(对于无符号数):用0填充左侧。
注意,在某些编译器中,右移负数可能会保留符号位,这取决于具体的实现(算术右移和逻辑右移)。
尽量不要使用带符号的整数参加位运算!
1.3.7优先级(从上到下,由高到低)
运算符 | 功能 |
---|---|
+ + | 递增 |
- - | 递减 |
~ | 取反 |
- | 一元负号 |
+ | 一元正号 |
* | 解引用 |
& | 取地址 |
* | 乘法 |
/ | 除法 |
% | 取模(取余) |
+ | 加法 |
- | 减法 |
<< | 向左移位 |
>> | 向右移位 |
< | 小于 |
<= | 小于等于 |
> | 大于 |
>= | 大于等于 |
== | 相等 |
!= | 不相等 |
& | 位与 |
^ | 位异或 |
| | 位或 |
&& | 逻辑与 |
|| | 逻辑或 |
? : | 条件 |
= | 赋值 |
完整版见《C++ Primer》
二、常用技巧
位运算 | 功能 | 示例 |
---|---|---|
& | 清零末尾连续的1 | n & n + 1 |
清零最右边的1(判断是否是2的幂) | n & n - 1 | |
仅保留最右边的1 | n & -n | |
判断奇偶 | n & 1 | |
| | 置1最右边的0 | n | n + 1 |
置1末尾连续的0 | n | n - 1 | |
^ | 交换两个数而无需临时变量 | a = a ^ b; b = a ^ b; a = a ^ b; |
~ | 取相反数 | ~n + 1 |
<< | 快速乘以2的k次幂(代替对应乘法) | n << k |
>> | 快速除以2的k次幂(代替对应除法) | n >> k |
复合 | 将第k+1位设为0 | n & ~(1 << k) |
将第k+1位设为1 | n | 1 << k | |
翻转第k+1位 | n ^ 1 << k | |
取第k+1位 | n >> k & 1 | |
将末尾k位设为0 | n & ~((1 << k) - 1) | |
将末尾k位设为1 | n | (1 << k) - 1 | |
翻转末尾k位 | n ^ (1 << k) - 1 | |
取末尾k位 | n & (1 << k) - 1 | |
取绝对值 | mask = x >> 31; x = (x + mask) ^ mask; |
三、面试题型
3.1 位1的个数
原理:等同于清除最右边的1的次数
int count = 0;
while(n)
{
n = n & n - 1;
count++;
}
return count;
扩展:
- 求两个数的汉明距离(对应位不同的数量)——思路:计算两个数异或结果中1的个数
- 计算一个数的二进制中0的个数——思路:int型占用位数 - 二进制中1的个数
- 计算1~n的二进制中1的总个数——思路:dp[i] = dp[i & (i - 1)] + 1;
- 寻找重复数——思路:数组第i位1的总个数 > 1~n的第i位1的总个数,说明重复数在第i位为1
- 只出现一次的数字——思路:数组第i位1的总个数 % 2 即为答案的第i位
- 只出现一次的数字 II——思路:数组第i位1的总个数 % 3 即为答案的第i位
3.2 只出现一次的数字
原理:异或的恒等律和结合律
int num = 0;
for (int i = 0; i < length;i++ ) {
num ^= arr[i];
}
return num;
扩展:
int ones = 0, twos = 0;
for (int num : nums) {
//ones和twos分别记录出现一次和两次的数字
tie(ones, twos) = pair{ones ^ num, twos | (ones & num)};
//清除出现三次的数字
int common_bit_mask = ~(ones & twos);
ones &= common_bit_mask;
twos &= common_bit_mask;
}
return ones;
3.3 位运算实现加减乘除
原理:
- a + b:和为a ^ b,进位为(a & b ) << 1
- a - b == a + (-b) == a + (~b + 1)
- a * b:重复加法结合移位
- a / b: 重复减法结合移位
#include <stdexcept>
int add(int a, int b) {
while (b != 0) {
//对应位都为1则会产生进位
int carry = (unsigned int)(a & b) << 1;
//无进位时加法相当于异或
a = a ^ b;
//无进位加法与进位相机
b = carry;
}
return a;
}
int subtract(int a, int b) {
return add(a, add(~b, 1));
}
int multiply(int a, int b) {
int result = 0;
//二进制竖式乘法
while (b != 0) {
if (b & 1) {
result = add(result, a);
}
a <<= 1;
b >>= 1;
}
return result;
}
int divide(int dividend, int divisor) {
if (divisor == 0) {
throw std::overflow_error("Division by zero");
}
bool isNegative = (dividend < 0) ^ (divisor < 0);
unsigned int a = abs(dividend);
unsigned int b = abs(divisor);
unsigned int result = 0;
//二进制竖式除法
for (int i = 31; i >= 0; i--) {
if ((a >> i) >= b) {
a -= b << i;
result += 1 << i;
}
}
//限制范围
if(!isNegative && ans > INT_MAX)
ans--;
return isNegative ? -result : result;
}
PS:本文为学习总结,方便个人复习
参考
[1]C++位运算常见应用
[2]C++面试八股文之位运算问题详解
[3]【位运算】常用方法 & 技巧
[4]位运算 学习笔记【C++ 算法竞赛】
[5]C++:常用的位运算技巧
[6]C++:位操作基础篇之位操作全面总结
[7]力扣刷题c++位运算总结
[8]C++ Primer