前言
这几天连着做了不少位运算的题目,那么这篇博客,就来总结一下这些位运算的题目吧。
位运算的常用小技巧
基础位运算
按位与 & :有0为0,无0为1
按位或 | :有1为1,无1为0
按位异或 ^ :相同为1,不同为0
按位取反 ~ :0变成1,1变成0
左移 << : 二进制序列向左移
右移 >> :二进制序列向右移
判断一个数n的二进制表示中的第x位是0还是1
n & (1 << x)
n >> x 将n的第x位放到最右边第0位
与1 按位与(1的二进制序列为000000...... 1)
即可判断第x位是0还是1
将一个数n的二进制表示中的第x位变成1
n |= (1 << x)
要将第x位变成1,需要第x位按位或1
1 << x 造出了第x位为1,其他位为0的二进制序列
将一个数n的二进制表示中的第x位变成0
n &= ~(1 << x)
要将第x位变成0,需要第x位按位与0
1 << x 造出了第x位为1,其他位为0的二进制序列
对该序列取反,就早出了第x位为0,其他位为1的二进制序列
提取一个数n的二进制表示中的最右边的1
n & (-n)
-n 的本质是将一个二进制数最右边的1的左边区域全部变成相反的数
-n = ~n + 1
eg:
n = 11010001010101100000
-n = 00101110101010011111 + 1
= 00101110101010100000
干掉一个数n的二进制表示中的最右边的1(变成0)
n & (n - 1)
n - 1 的本质是将一个二进制数的最右边的1的右边区域(包括自己)全部取反
eg
n = 11010001010101100000
n - 1 = 11010001010101011111
按位异或(^) 运算律
a ^ a = 0
a ^ 0 = a
a ^ b ^ c = a ^ c ^ b
题目练习
题目一:判定字符是否唯一
题目

链接:
思路分析
大家第一眼,这不就hash吗,一分钟搞定,
but,请看限制的第三点:如果不使用额外的数据结构
我们来想想,不使用额外的数据结构,
emm,那就来暴力查找,绷不住了,肯定要想一个时间复杂度优秀的算法啊!
OK,正式开始解决这道题目:
要想时间复杂度优秀,必然是要使用某些数据结构来帮忙的,但是又不允许使用额外的数据结构,
所以,我们可以尝试使用位图的思想
我们把一个比特位作为信息的载体,一个比特位可以存0或1,
我们就用0或1来代表字符串中的字符有没有在之前出现过,
这不就相当于模拟实现了一个hash吗?
一旦想到了这个思想,那这道题就无比简单了
细节:这道题目中只有小写字母,也就是只会出现26种情况,那一个int32位肯定足够存储,
所以位图就选择int,
a --- 第0位
b --- 第1位
......
依次类推即可
具体代码
c
bool isUnique(string astr) {
int a = 0;
for(auto& e:astr)
{
int i = e - 'a';
cout << i << endl;
cout << (a & (1 << i)) << endl;
if(a & (1 << i)) // 判断第i位是否为1
{
return false;
}
else
a |= (1 << i); // 注意这个地方,我想要修改,肯定是 |= 而不是|
}
return true;
}
题目二:丢失的数字
题目

链接:
思路分析
经典位运算的题目 --- 单身狗。
直接根据按位异或的规律,就可以很简答的解决这道题目。
a ^ a = 0
a ^ 0 = a
a ^ b ^ c = a ^ c ^ b
我们把0~n的所有数字和数组里面的所有数字,全部按位异或一遍,最后得到的结果就是丢失的数字。
具体代码
c
int missingNumber(vector<int>& nums) {
int n = nums.size();
int ret = 0;
for(int i = 0;i < nums.size();++i)
{
ret ^= i;
ret ^= nums[i];
}
return ret ^= n;
}
题目三:137. 只出现一次的数字 II
题目

链接
思路分析
上一道题是单身狗最基础的版本,现在这道题来了一个单身狗的进阶版本。
那么这道题目该怎么做呢?
观察题目,我们可以发现:
3次,1次,那么所有数字的同一位上加起来也肯定是3的倍数啊,
如果不是三个倍数,说明啥,说明1次的这个数字的这一位上也被加进来了,也就是说,出现一次的这个数字的该位上是1,
所以,现在,思路已经很明确了,就是把每一位上1出现的次数全部加起来,看看是不是三的倍数,如果不是三的倍数,就把该位设置为1,
也就是 x |= (1<<pos)
最后遍历完32位,就可以得出只出现一次的这个数字了。
具体代码
c
int singleNumber(vector<int>& nums) {
int ret = 0;
for(int i = 0;i < 32;++i)
{
int sum = 0;
for(auto& e:nums)
{
if(e & (1 << i)) sum++;
}
//说明这一位是仅出现一次
if(sum % 3 != 0) ret |= (1 << i);
}
return ret;
}
题目四:消失的两个数字
题目

链接
思路分析
诶,单身狗又升级了,单身狗2.0版本来了。
这道题又很麻烦了,如果我依旧是把所有的元素全部按位异或,那最后得到的结果不就是两个数的按位异或值吗?
这咋分开啊?
不用担心,肯定是有办法的!
最后两个数肯定是不相同的,所以最后剩下的异或值一定不是0,也就是说最后剩下的异或值最少有一位上是1,
这说明啥?
说明最后的两个数该位上的比特位值不相同,一个0,一个1,
这有啥用啊?
不是很显然吗?
我们就可以根据这一位上的不同,把所有的元素分为两个部分,
每一个部分都仅有一个元素只出现了一次,这不就又回到了基础单身狗吗?
再按位异或一次呗。
具体代码
c
vector<int> missingTwo(vector<int>& nums) {
if(nums.size() == 0) return {1,2};
int n = nums.size() + 2;
int tmp = 0;
for(int i = 0;i < nums.size();++i)
{
tmp ^= (i + 1);
tmp ^= nums[i];
}
tmp ^= n;
tmp ^= (n - 1);
//首先获得特殊的两个数字的异或值,就能知道两者哪一位不同
//进而就能把数据分为两类
int pos = 0;
for(int i = 0;i < 32;++i)
{
if(tmp & (1 << i) )
{
pos = i;
break;
}
}
cout << pos << endl;
int ret1 = 0,ret2 = 0;
for(int i = 0;i < nums.size();++i)
{
if((i + 1) & (1 << pos)) ret1 ^= (i + 1);
else ret2 ^= (i + 1);
//i和1不要写反了
if(nums[i] & (1 << pos)) ret1 ^= nums[i];
else ret2 ^= nums[i];
}
if(n & (1 << pos)) ret1 ^= n;
else ret2 ^= n;
if((n-1) & (1 << pos)) ret1 ^= (n-1);
else ret2 ^= (n-1);
return {ret1,ret2};
}
题目五:371. 两整数之和
题目

链接
思路分析
这道题目,说实话得对硬件比较了解,
要是笔试真遇到了,同学们还是见仁见智吧,直接不讲武德,return a + b;
好了,开个玩笑,玩归玩,闹归闹,真到了学习的时候,还是认真学习吧。
首先,我们思考一下,按位异或扮演了一个什么样子的角色呢?
1 ^ 1 = 0,
0 ^ 0 = 0,
1 ^ 0 = 1,
0 ^ 1 = 1
嗯?得到的结果不就是不考虑进位的结果吗?
所以按位异或的本质就是无进位相加。
a ^ b,也就获得了无进位相加的信息。
于是,我们现在有了加法了,但是我们还缺少进位信息,这咋办。
别慌,我们再来看看&运算
1 & 1 = 1,
0 & 0 = 0,
1 & 0 = 0,
0 & 1 = 0
进位这不就来了吗?只有两位都是1,才得到了1,这个1就是进位的1,
当然,由于是进位,所以,肯定要往高位上加,
所以 (a & b) << 1,也就获得了进位信息
直接 无进位相加的信息和进位信息一起相加,不就是最后的结果吗?
但是还是没有加法啊,所以"进位相加的信息和进位信息一起相加" 中的相加,依然是^ 和 &配合执行,
直到最后没有进位信息,就得到了真正的结果。
具体代码
c
int getSum(int a, int b) {
while(b)//用b来存储进位信息
{
int carry = (a & b) << 1;//获取进位信息,并保存
a ^= b;//无进位相加,用a来存储结果
b = carry;
}
return a;
}