前言

这几天连着做了不少位运算的题目,那么这篇博客,就来总结一下这些位运算的题目吧。

位运算的常用小技巧

基础位运算

按位与 & :有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位
……
依次类推即可

具体代码

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的所有数字和数组里面的所有数字,全部按位异或一遍,最后得到的结果就是丢失的数字。

具体代码

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

题目

在这里插入图片描述

链接

137. 只出现一次的数字 II

思路分析

上一道题是单身狗最基础的版本,现在这道题来了一个单身狗的进阶版本。
那么这道题目该怎么做呢?

观察题目,我们可以发现:
3次,1次,那么所有数字的同一位上加起来也肯定是3的倍数啊,
如果不是三个倍数,说明啥,说明1次的这个数字的这一位上也被加进来了,也就是说,出现一次的这个数字的该位上是1

所以,现在,思路已经很明确了,就是把每一位上1出现的次数全部加起来,看看是不是三的倍数,如果不是三的倍数,就把该位设置为1,
也就是 x |= (1<<pos)

最后遍历完32位,就可以得出只出现一次的这个数字了。

具体代码

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

这有啥用啊?

不是很显然吗?
我们就可以根据这一位上的不同,把所有的元素分为两个部分
每一个部分都仅有一个元素只出现了一次,这不就又回到了基础单身狗吗?
再按位异或一次呗。

具体代码

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. 两整数之和

题目

在这里插入图片描述

链接

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,也就获得了进位信息

直接 无进位相加的信息和进位信息一起相加,不就是最后的结果吗?

但是还是没有加法啊,所以“进位相加的信息和进位信息一起相加” 中的相加,依然是^ 和 &配合执行,
直到最后没有进位信息,就得到了真正的结果。

具体代码

int getSum(int a, int b) {
while(b)//用b来存储进位信息
{
int carry = (a & b) << 1;//获取进位信息,并保存
a ^= b;//无进位相加,用a来存储结果
b = carry;
}
return a;
}