文章目录
位运算是把数字用 二进制表示,对每一位上的 0 或 1 进行运算。位运算只有五种类型: 与(&)、或(|)、异或(^)、左移(<<)和右移(>>)。
左移运算和右移运算
左移运算符 m<<n
表示把 m 左移 n 位,最左边的 n 位丢弃,同时在最右边补 n 个 0。
右移运算符 m>>n
表示把 m 右移 n 位,最右边的 n 位丢弃,如果 m 是无符号数,则用 0 填补最左边的 n 位;如果 m 是有符号数,则用符号位填补最左边的 n 位。
对于一个正整数,数字左移一位就是将数值乘 2,数字右移一位就是讲数值除以 2。位操作的效率要比运算符操作效率高,在实际编程过程中应尽量使用位移运算。
与、或、异或操作
一个数和另一个数进行两次异或操作后的结果仍为这个数,因此异或操作可以用来实现两个变量的置换。
x ^= y;
y ^= x; // y == x;
x ^= y; // x == y;
异或运算还有一个性质:任何一个数字异或自己都等于 0。对于数组中出现数字次数的题目,我们可以通过依次异或数组中每一个元素,得到最后不为 0 的数就是数组中出现次数为 1 的数。
使用 n & (n-1) 操作可以把最后一位的 1 变为 0。通过这一操作可以解决很多问题:
1. 判断 n 是否为 2 的幂
bool powerTow(int n)
{
return n & (n-1) == 0;
}
2. 判断 n 是否为 4 的幂
如果一个数是 4 的幂,那么它必然是 2 的幂。因此我们可以先判断是否为 2 的幂。2 的幂中如果 1 的位置在奇数位上则是 4 的幂,所以我们跟 0x55555555
按位与。
bool powerFour(int n)
{
if (n & (n-1) == 0)
return (n & 0x55555555) != 0; // 返回非 0 则是 4 的幂
return false;
}
3. 求二进制中 1 的个数
题目来源:《剑指 offer》面试题 15
我们首先能想到的就是判断最右位是否为 1,如果是就讲计数变量加一,然后将该数右移一位,直到整个数变为 0。但是这样的思路有一个缺陷,当数值为负时,右移操作为保证该数仍为负,则会在符号位置 1,最后会陷入死循环。
解法一:为避免死循环我们不能右移输入的数。首先把 n 和 1 做与运算,判断 n 的最低位是不是为 1,然后把 1 左移一位得到 2,在和 n 做与运算就能判断 n 的次低位是否为 1。这样反复左移,每次都能判断 n 中的一位是否为 1.
int NumberOfOne(int n)
{
int count = 0;
unsigned int flag = 1;
while (flag) {
if (n & flag) count++;
flag = flag << 1;
}
return count;
}
解法二:利用 (n & n-1)
的性质来求二进制中 1 的个数
int NumberOfOne(int n)
{
int count = 0;
while (n) {
++count;
n = n & (n-1);
}
return count;
}
使用位操作的数值计算
1. 计算将一个数 m 改变为 n 所需要改变的二进制位数
只要计算两个数不同二进制位的个数即可,对于不同位我们很容易就能想到异或操作,异或操作就是将相同的位置 0,不同的位置 1。所以在异或操作之后只要统计 1 的个数就能得到两个数不同二进制位的个数。
void countDiffer(int m, int n)
{
m = m ^ n;
int count = 0;
while (m) {
++count;
m = m & (m-1);
}
return count;
}
2. 不使用运算符实现加法运算**
int Add(int num1, int num2)
{
int sum, carry;
do {
sum = num1 ^ num2; // 异或结果为相加但不进位
carry = (num1 & num2) << 1; // 只考虑进位的情况
num1 = sum;
num2 = carry;
} while (num2 != 0); // 重复上述过程直到进位为 0
return sum;
}
3. 整数的平均数
直接使用 (x+y)/2 可能导致溢出,因为 x+y 可能会大于 INT_MAX,但是平均值是不会溢出的
int average(int x, int y)
{
return (x & y) + ((x ^ y) >> 1);
}
4. 计算绝对值
int abs(int x)
{
int y = x >> 31;
return (x ^ y) - y; // (x + y) ^ y
}
其他应用
1. 字符串转换成小写
使用位运算:
- 大写字母和小写字母交换:
c ^= 32;
- 将字符转换为小写:
c |= 32;
- 将字符转换为大写:
c &= -33;
class Solution {
public:
string toLowerCase(string str) {
string res = str;
if (str.length() <= 0)
return res;
for (auto &c : res)
c |= 32;
return res;
}
};