绪论:冲击蓝桥杯一起加油!!
每日激励:"不设限和自我肯定的心态:I can do all things。 --- Stephen Curry"
**绪论:
今天总结了下位操作中常见的使用的方法,并且附加许多训练,通过方法 + 训练,位操作算法就将有很大的提升,后续将持续更新前缀和算法。敬请期待~早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。**
位运算
首先了解基本的位运算操作(必备):
<<
:左移,所有位数左移(如:1111 << 1 = 1110)>>
:右移,所有位数右移(如:1111 >> 1 = 0111)~
:取反,将所有位数全部取反(0 -> 1/ 1 -> 0)、如: ~1111 = 0000- 至于 & 、| 、^ 看下图左边运算(001 与 011进行位运算操作,得到的结果)
&
:按位与,有0就是0|
:按位或,有1就是1^
:按位异或,相同为0,相异位1(或者可以看成无进位的加法)
-
约定下标从右往左的0开始:
-
这样某位置移动到0位置的移动值就是自己的下标
-
这里的下标,并不是数组中的真下标,而是我们在记忆位数时它的位置的标识,从0开始到31(并且注意是从右往左的记忆)
-
对于位运算的优先级,就不记忆了,就经量使用括号
常用的基础位运算操作
- 判断一个数的二进制中第x位为0/1
- 方法很简单 结合上面的
>> 、 &
位运算符 - 其中注意在位运算中 会经常用到 1,来辅助位运算(因为他的二进制中只有1位:000... 0001,这样就能通过这一位进行操作,也能避免很多复杂问题)
- 此处的话我们可以将 某个数进行右移
>>
x位,再 按位与x>>
上1(因为对与&来说,有0就是0,这样的话,按位与上1的话,若第x位上是0则为0,反之为1的话和按位与的1都没有0,就为1,而对于其他位来说不同管因为1的除了第一位为1其他都是0,按位与后都为0,所以结果只用两种可能 000...0000 / 000...0001) - 具体如下图假设
- n为:...0110101001、
- 1: ...000...0001
- 通过将n右移x位,将x位的值移动第一位,在与000...01进行按位与,即可得到答案
- 方法很简单 结合上面的

对于后面的就不再细讲了,建议写草稿模拟情况就能很好的理解了,具体有问题可以评论。
- 将数 n 的二进制修改成1:
n |= (1 << x)
训练题目:leetcode 191 338 461
- 将一个数的二进制表示的第x位修改成 0:
n &= (~(1<<x))
- 位图思想:位图中我们就是通过 位操作进行管理其中的数据,位图内部主要通过0/1来判断该数据是否存在,我们可以用于存储一已知的容器数据但它访问速度可能较慢,那么可以将他在存放到位图中,这样通过 常量级的位操作就能快速的知道数据的状态。
- 提取一个二进制数中的最右侧的1:
n &= -n
- 干掉二进制数中最右侧的1:
n &= (n-1)
- 一些按位异或的运算律
a ^ 0 = a
a ^ a = a
a ^ b ^ c = a ^ (b ^ c)
- 对于
a ^ b ^ c
来说他的本质是:可以理解为按位异或操作是有交换律的,我们能同时的看三个数,它们的顺序无所谓,那么也就能看成:抵消1的形式快速的算出答案(具体如下图)
相关训练:LeetCode136、260
如何将 a ^ b 划分出来
其中就260比较特别和困难:这里讲一下
- 想通过 按位与操作得到两个单独的数据 a ^ b
- 现在需要将他给区分开
- 对于 a ^ b来说,若要区分可以通过找1,因为按位异或的本质是:相同为0,相异为1
- 所以可以找到第一个不同的地方,也就是最右边的1
- 通过这个1的进行区分两个数,并将这两个数给分别通过按位异或的方式放到一个数组中
- 这样最终就能将这两个数放到两个数组中,也就能单独获得了
cpp
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
int res = 0;
for(auto c :nums){
res ^= c;//利用到 res ^ res = 0 & res & 0 = res 这两个按位异或技巧
//当res 在按位异或的过程中会将出现两次的给异或为0,而res 与 0 异或则还是res
}
//此时因为会有两个单独的值,所以res = a ^ b
// 1001
// 0011
// 1010:a ^ b
//此时找到 a ^ b 的最右边的1(这个代表他们不同的位)
//通过这个区分 a b 这两个数,然后再次遍历nums中的元素
//不过此时加上条件:通过 右移最右边这个数的下标i,后判断是否为1进行区分两数,将两数有分成了两种情况
//这样 对于 右移最右边这个数的下标i 为1 / 不为1 中,肯定最终只会留下 a 和 b(因为其他的还是重复的然后抵消了)
int i = 0;
for(; i < 32;i++){
if(((res >> i) & 1) == 1){
break;//代表找到最右边的1了
}
}
int res1 = 0,res2 =0;
//得到i的位置
for(auto c : nums){
if((c >> i) & 1 == 1){
//一份是:右移i位等于1的,同时也是 a ^ b中的某一位
res1 ^= c;
}
else{
res2 ^= c;
}
}
vector<int> v{res1,res2};
return v;
}
};
将上面8个常见的情况记住并练习,后在进行下面的练习进一步巩固
具体训练:
1. 判定字符是否唯一
题目:

分析题目并提出,解决方法:
-
本题不难想到,通过一个hash来快速的确定元素出现的个数
-
但题目给了一个小小的提升:能不能不使用额外的空间
-
那么本题仅仅26位,我们是不是可以通过位图的思想:用一个整形来直接代替hash
-
结合前面的位操作,实现判断某个字符是否已经存在过
-
加上鸽巢原理:当字符串长度超过 26 时必定会有重复的字符!!
-
修改x位为1:n|= (1 << i)
题解核心逻辑:
cpp
class Solution {
public:
bool isUnique(string astr) {
if(astr.size() > 26 ) return false;//利用鸽巢原理进行优化
//使用位图 代替hash
int bit = 0;
for(auto c : astr){
int i = c - 'a';//得到字符的位置: 'a' - 'a' = 0、'b' - 'a' = 1
//判断字符是否已经出现过:
if((bit >> i & 1) == 1){
//进来代表第 i 位为1,并且代表已经出现过一次了
//所以就错误了,直接返回
return false;
}
bit |= (1 << i);//将第i位设置为 1
}
return true;
}
};

2. 丢失的数字
题目:

分析题目并提出,解决方法:
方法1:hash(空间复杂度:O(N))
- 先遍历填充hash
- 在遍历hash看哪个位置空了
高斯求和法:((首项 + 尾项) * 项数)/ 2 等于所有元素的和,再减去数组中的和,这样就能得到缺失的和(空间复杂度:O(1))

方法3(本题解法):位运算 a ^ a = 0
(空间复杂度:O(1))
题解核心逻辑:
本题就主要写位运算的操作:
cpp
class Solution {
public:
int missingNumber(vector<int>& nums) {
int n = nums.size();
int res = 0;
for(int i = 0; i <= n;i++){
res ^= i;//得出 从 0 ~ n 的所以情况 0 ^ 1 ^ .... ^ n
}
for(auto i : nums){
res ^= i;// res ^ nums = 缺失的
}
return res;
}
};

3. 两整数之和
题目:

分析题目并提出,解决方法:
对于这种不能使用 + - 法的一般就是位运算了
- 首先回顾之前的按位异或操作又称无进位相加
按位异或的无进位相加
- 那么就需要找到他的进位值:发现当 a & b 时就能表示他是否需要进位(下图右边)
a & b 查看否需要进位
- 但这只是找到了他是否要进位的地方,我们还需要将他左移 1 位 从而完成进位后的低位++
- 然后再一次进行加法操作(按位异或:无进位加法)将进位值加上得到最终值
- 但注意:此时还需要再一次判断是否还有进位的地方:
- 若为0则表示不存在进位了
- 若不为0则代表仍然有进位,那么就需要再次重复2、3、4、5步(下图黑框框)
题解核心逻辑:
cpp
class Solution {
public:
int getSum(int a, int b) {
//使用 ^ 的无进位相加
//再使用 & 判断是否要进位(为0则代表不进位、不为0则代表有进位)
while((a & b) << 1){
int tmp = a;
a = a ^ b;
b = (tmp & b) << 1;
}
return a ^ b;
}
};
4. 只出现一次的数字 II
题目:

分析题目并提出,解决方法:

题解核心逻辑:
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
//将 nums 中的每个数的每一位 都相加并最终 模3,最终得到的每一位就是 只出现一次的每一位:为0 就是0,为1就是1
//此处 还能 %n 这样就能 看出就能排除重复出现n次 得到只出现1次的
int res = 0;
// n * 32
for(int i = 0 ; i < 32;i++){
//使用一个sum计算,判断在该位置的和
int sum = 0;
//3n0 + 0 = 0 -%3-> 0
//3n0 + 1 = 1 -%3-> 1
//3n1 + 0 = 3n -%3-> 0
//3n1 + 1 = 3n + 1 -%3-> 1
for(auto c : nums){
//判断c位置上 i 下标下的值
if(((c >> i) & 1) == 1){//判断是否为1,为1则++
sum++;
}
}
sum %= 3;//判断只出现一次的那个数的下标
if(sum == 1){
res |= (1 << i);//给结果下标添加算出来的值
}
}
return res;
}
};

5. 消失的两个数字
题目:

分析题目并提出,解决方法:
分析题目不难通过前面的铺垫想出:
- 通过创建下同大小的数组与缺失数组进行 按位异或 操作,最终会得到缺失两个数的 a ^ b
- 此时通过 a ^ b 找到它最后一位1的位置
- 将原数组和缺失数组都分成两份,并且这两份中会各自有a和b(获取没a和b)
- 这样再将分开的数组 相互 按位异或 就得到了分别缺失的两个a和吧
题解核心逻辑:
cpp
class Solution {
public:
vector<int> missingTwo(vector<int>& nums) {
int n = nums.size() + 2;
// 创建原数组,这里本质不用创建,直接不断 ^ 存储即可!
int sum = 0;
for(int i = 1 ; i <= n ;i++){
sum ^= i;
}
//先与缺数数组相 按位异或
int absent_value = sum;
for(auto c : nums) {
absent_value ^= c;
}
//得到了 a ^ b 缺失的值,现在找到第一个不同的位置
int i = 0;
for(; i < 32 ;i++){
if((absent_value >> i) & 1 == 1){
break;
}
}
//找到了i的位置,将原数组和缺失数组再次分成两份,再分别相异或
vector<int> arr(2,0);
for(int j = 1;j <= n;j++){
if(((j >> i) & 1) == 1){
arr[0] ^= j;
}else{
arr[1] ^= j;
}
}
for(auto c : nums) {
if(((c >> i) & 1) == 1){
arr[0] ^= c;
}else{
arr[1] ^= c;
}
}
return arr;
return arr;
}
};
