从零掌握位运算:十大经典问题详解
简单介绍和计算:
1.基础位运算
包括:<< >> ~ & | ^ 这些运算符的使用方法
2.给一个数n,确定它的二进制表示中的第x位是0还是一
if((n >> x) & 1) return 1;
else return 0;
3.将一个数的二进制表示中的第x位修改为0或1
//修改为0
n = n & (~(1 << x))
//修改为1
n = n | (1 << x)
4.位图的思想
将一个变量的每一个二进制位看成一个标志位,这就像STM32中的寄存器一样,每一位存放不同的数代表不同的状态
5.提取一个数(n)二进制中最右边的1
n & (-n);
示例:
0110101000//n
1001010111//n的反码
1001011000//加1之后的补码
&0110101000
0000001000
6.干掉一个数(n)二进制表示中最右侧的1
n & (n - 1);
7.位运算的优先级
对于位运算的优先级,能用括号就用括号
9,异或运算
//1. a ^ 0 = a;
//2. a ^ a = 0;
//3. a ^ b ^ c = a ^ (b ^ c);
概念题
位1的个数
题目链接:
要点:
- 核心目标:统计二进制表示中1的个数。
- 位操作技巧:逐位检查最低位是否为1,然后右移消去已处理的位。
我的代码:
class Solution {
public:
int hammingWeight(int n) {
int sum = 0;
for(int i = 0; i < 31; i++)
{
if(n & 1) sum++;
n = n >> 1;
}
return sum;
}
};
我的思路:
- 循环处理:遍历32位(
int
类型位数),每次取最低位判断是否为1。 - 优化点:使用
n & 1
代替取余操作,效率更高。
我的笔记:
- 易错点:未处理负数时可能陷入死循环(但题目约束为无符号整数)。
- 进阶方法:使用
n &= n-1
直接消去最低位的1,循环次数等于1的个数。
比特位计数
题目链接:
要点:
- 动态规划优化:利用已知结果计算当前值(如
i & (i-1)
消除最低位的1)。 - 核心公式:
bits[i] = bits[i & (i-1)] + 1
。
我的代码:
class Solution {
private:
vector<int> arr;
public:
vector<int> countBits(int n) {
arr.push_back(0);
for(int i = 1; i <= n; i++)
{
int sum = 0, j = i;
while(j & (-j))
{
j &= j - 1;
sum++;
}
arr.push_back(sum);
}
return arr;
}
};
我的笔记:
- 注意不能直接在while循环里面改写i,不然会死循环
- 使用 j & -j 来判断 i 的二进制是否有1,用j &= j - 1来消除二进制位中最右边的1
汉明距离
题目链接:
要点:
- 问题转化:两数异或后统计1的个数即为不同位的数量。
- 异或性质:
x ^ y
的结果中1的位数即为汉明距离。
我的代码:
class Solution {
public:
int hammingDistance(int x, int y) {
int z = x ^ y;
int sum = 0;
while(z & -z)
{
z &= z - 1;
sum++;
}
return sum;
}
};
我的笔记:
- 位运算优化:用
z &= z-1
快速消去最低位的1,减少循环次数。 - 代码简洁性:结合异或和消位操作,实现高效计算。
只出现一次的数组
题目链接:
要点:
- 异或性质:
a ^ a = 0
,0 ^ a = a
,异或所有元素后剩余值为唯一出现元素。
我的代码:
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for(auto& x : nums)
{
ret ^= x;
}
return ret;
}
};
我的笔记:
- 线性复杂度:遍历数组一次,异或所有元素。
- 局限性:仅适用于其他元素出现偶数次的情况。
算法题
判定字符是否唯一
题目链接:
要点:
- 位图法:用26位二进制表示字符是否出现过(
a-z
映射到0-25
位)。 - 鸽巢原理:字符串长度超过26直接返回
false
。
老师代码:
class Solution
{
public:
bool isUnique(string astr)
{
// 利⽤鸽巢原理来做的优化
if(astr.size() > 26) return false;
int bitMap = 0;
for(auto ch : astr)
{
int i = ch - 'a';
// 先判断字符是否已经出现过
if(((bitMap >> i) & 1) == 1) return false;
// 把当前字符加⼊到位图中
bitMap |= 1 << i;
}
return true;
}
};
老师思路:
利⽤「位图」的思想,每⼀个「⽐特位」代表⼀个「字符,⼀个 int 类型的变量的 32 位⾜够表⽰所有的⼩写字⺟。⽐特位⾥⾯如果是 0 ,表⽰这个字符没有出现过。⽐特位⾥⾯的值是 1 ,表⽰该字符出现过。那么我们就可以⽤⼀个「整数」来充当「哈希表」。
我的代码:
class Solution {
public:
bool isUnique(string astr) {
if(astr.size() > 26) return false;
int flag = 0;
for(int i = 0; i < astr.size(); i++)
{
if(flag & (1 << astr[i] - 'a'))
{
return false;
}
flag |= 1 << astr[i] - 'a';
}
return true;
}
};
我的笔记:
-
使用位图的思想,其实就像是单片机里面的状态寄存器
-
优化:鸽巢原理,如果整个字符串的大小大于26,就一定是有字母重复
-
位操作
-
关键点:ASCII字符范围限制为小写字母,故可用26位表示。
-
扩展思考:若字符范围扩大(如Unicode),需改用哈希表。
丢失的数字
题目链接:
要点:
- 异或性质:将数组元素与
0~n
异或,结果即为缺失数字。 - 高斯求和法:计算
n(n+1)/2 - sum(nums)
。
老师代码:
class Solution
{
public:
int missingNumber(vector<int>& nums)
{
int ret = 0;
for(auto x : nums) ret ^= x;
for(int i = 0; i <= nums.size(); i++) ret ^= i;
return ret;
}
}
老师思路:
设数组的⼤⼩为 n ,那么缺失之前的数就是 [0, n] ,数组中是在 [0, n] 中缺失⼀个数形成的序列。如果我们把数组中的所有数,以及 [0, n] 中的所有数全部「异或」在⼀起,那么根据「异或」运算的「消消乐」规律,最终的异或结果应该就是缺失的数
我的代码:
class Solution {
public:
int missingNumber(vector<int>& nums) {
int ret = 0;
for(int i = 0; i < nums.size(); i++)
{
ret ^= i;//0 ~ n-1
ret ^= nums[i];//nums[0] ~nums[n-1]
}
ret ^= nums.size();//n
return ret;
}
};
我的笔记:
-
其他方法:
高斯求和:先把0~n数相加,再依次减去数组中的元素,最后剩下的就是缺失的数字
哈希表:不多赘述
排序+遍历:将整个数组进行一次排序,之后从前往后遍历与数组下标作比较,出现第一个位置下标不一样就是缺失对应下标的数
两个整数之和
题目链接:
要点:
- 异或 ^ 运算本质是「⽆进位加法」;
- 按位与 & 操作能够得到「进位」;
- 然后⼀直循环进⾏,直到「进位」变成 0 为⽌。
老师代码:
class Solution
{
public:
int getSum(int a, int b)
{
while(b != 0)
{
int x = a ^ b; // 先算出⽆进位相加的结果
unsigned int carry = (unsigned int)(a & b) << 1; // 算出进位
a = x;
b = carry;
}
return a;
}
}
老师这里用unsigned int是因为当a&b == -1时,我们对-1<<1的操作是无定义的,所以我们要对int类型强转为unsigned int
我不知道为什么我的代码没有出现这种情况
我的代码:
class Solution {
public:
int getSum(int a, int b) {
int x = 0;
while(b)//当计算没有进位值时出循环
{
x = a;//b要用到a改变之前的值,这里保存一下
a = a ^ b;//异或也就是无进位加1
b = (x & b) << 1;//这一步就是用于求进位的
}
return a;
}
};
我的笔记:
- (x & b) << 1可以算出x和b的进位,注意一定要向前移动一位,这样在之后的异或操作和与运算的时候才是正确的
只出现一次的数字
题目链接:
要点:
-
位统计法:统计每位出现次数,模3后剩余位即为目标数。
-
推广思路:若其他数出现n次,模n即可。
-
只有一个数只出现一次,其余的数都在数组中出现三次
-
算法的思想就是把数组中的每个数的每一个二进制位分别相加,之后再把这个相加之后的数%3就可以得到那个与众不同的数的二进制位了,之后再把它转换为int类型的数就行了说实话这个想法我是想不到的
老师代码:
class Solution
{
public:
int singleNumber(vector<int>& nums)
{
int ret = 0;
for(int i = 0; i < 32; i++) // 依次去修改 ret 中的每⼀位
{
int sum = 0;
for(int x : nums) // 计算nums中所有的数的第 i 位的和
if(((x >> i) & 1) == 1)
sum++;
sum %= 3;
if(sum == 1) ret |= 1 << i;
}
return ret;
}
};
老师思路:
设要找的数位 ret 。 由于整个数组中,需要找的元素只出现了「⼀次」,其余的数都出现的「三次」,因此我们可以根据所有数的「某⼀个⽐特位」的总和 %3 的结果,快速定位到 ret 的「⼀个⽐特位上」的值是 0 还是 1 。 这样,我们通过 ret 的每⼀个⽐特位上的值,就可以将 ret 给还原出来
我的代码:
class Solution {
public:
int singleNumber(vector<int>& nums) {
int hash[32] = { 0 };//储存对应二进制位的和
for(int i = 0; i < nums.size(); i++)
{
for(int j = 0; j < 32; j++)
{
hash[j] += (nums[i] & (1 << j)) ? 1 : 0;
}
}
int ret = 0;
for(int i = 0; i < 32; i++)
{
if(hash[i] % 3 == 1)
{
ret |= 1 << i;
}
}
return ret;
}
};
我的笔记:
- 代码比较好编写,就是思路我想不到
- 通过这个思路,我们可以求比如一个数组中只有一个数只出现一次,其余的数都在数组中出现n次,我们就可以转换为%n
消失的两个数字
题目链接:
要点:
- 异或分组法:
- 异或所有元素和完整范围,得到
a^b
。 - 找到
a
和b
不同的某一位,分组异或求解。
- 异或所有元素和完整范围,得到
老师代码:
class Solution
{
public:
vector<int> missingTwo(vector<int>& nums)
{
// 1. 将所有的数异或在⼀起
int tmp = 0;
for(auto x : nums) tmp ^= x;
for(int i = 1; i <= nums.size() + 2; i++) tmp ^= i;
// 2. 找出 a,b 中⽐特位不同的那⼀位
int diff = 0;
while(1)
{
if(((tmp >> diff) & 1) == 1) break;
else diff++;
}
// 3. 根据 diff 位的不同,将所有的数划分为两类来异或
int a = 0, b = 0;
for(int x : nums)
if(((x >> diff) & 1) == 1) b ^= x;
else a ^= x;
for(int i = 1; i <= nums.size() + 2; i++)
if(((i >> diff) & 1) == 1) b ^= i;
else a ^= i;
return {a, b};
}
}
我的代码:
class Solution {
private:
vector<int> ret;
public:
vector<int> missingTwo(vector<int>& nums) {
//把数组中的数和1~N全部异或在一个数中N = nums.size() + 2
int sum = 0;
for(int i = 0; i < nums.size(); i++)
sum ^= nums[i];
for(int i = 1; i <= nums.size() + 2; i++)
sum ^= i;
//找出sum二进制中第一个1的位置
int x = sum & -sum,ret1 = 0, ret2 = 0;
//根据这个1的位置将数组中的数和1~N分为两部分分别异或求出结果
for(int i = 0; i < nums.size(); i++)
{
if(nums[i] & x)
ret1 ^= nums[i];
else
ret2 ^= nums[i];
}
for(int i = 1; i <= nums.size() + 2; i++)
{
if(i & x)
ret1 ^= i;
else
ret2 ^= i;
}
ret.push_back(ret1);
ret.push_back(ret2);
return ret;
}
};
我的笔记:
-
算法思路:如何分离已经异或在一起的两个数?—> 异或数中的二进制1表示这两个原来的数这一个二进制位是不一样的,将原来的数按这个二进制位是0还是1分为两组数分别异或
-
注意数组和1~N的边界问题,数组下标是从0开始的,缺了两个数,所以i <= nums.size() + 2,有等号,也要加2
-
位运算技巧:
sum & -sum
快速找到最低位的1。 -
分组异或:按某一位是否为1将数分为两组,分别异或得到结果。
-
关键点:分组依据是两数在该位不同,确保每组各含一个结果。
-
扩展应用:适用于缺失多个数字的问题(需调整分组逻辑)。
注意事项
C++语法注意
- 位移操作:右移负数时补符号位,需用
unsigned int
避免未定义行为。 - 位运算符优先级:
&
和|
优先级低于比较运算符,需加括号。 - 整数溢出:高斯求和法可能溢出,优先用异或法。
算法思路注意
- 异或性质:灵活应用
a ^ a = 0
和a ^ 0 = a
简化问题。 - 位统计法:将问题分解到每个二进制位,独立处理。
- 正难则反:如最小删除和转化为最大公共和。
- 分组思想:通过某一位的不同将问题拆分为子问题。
常见错误
- 未处理负数导致死循环(如
n >> 1
循环终止条件)。 - 位操作符漏写括号(如
(x >> i) & 1
)。 - 忽略鸽巢原理等优化,暴力求解超时。