位运算详解及相关OJ题

x33g5p2x  于2022-02-07 转载在 其他  
字(9.2k)|赞(0)|评价(0)|浏览(837)

1.位运算基础

2.位运算对应奇淫技巧

3.常见面试和OJ题

3.不用加减乘除做加法减法乘法除法

1.位运算基础

&

1.按位

2.如果两个相应的二进制位中都为1则该位的结果才为1.可以记成有0出0

1.按位或

2.两个相应的二进制位中只要一个为1结果就为1.可以记成有1出1

~

1.取反

2.~是一种一元运算符,用来对一个二进制数按位取反即0变成1,1变成0.

^

1.异或

2.如果参与运算的两个二进制位相同则为0否则为1

1.左移

2.将一个数的各位二进制全部左移N位,右边补0

<<

1.右移

注意
首先右移运算分两种:

1.逻辑移位 左边用0填充,右边丢弃
2.算术移位 左边用原该值的符号位填充,右边丢弃

3.对与无符号数而言左边补0右边丢丢弃


下面我们来看几个例子来熟悉一下这些位运算

#include<iostream>
#include<algorithm>
#include<vector>
using  namespace std;

int main() {
	int a = 1;
	int b = 2;
	cout << (a & b) << endl;
	return 0;
}

我们首先来看这个例子 a&b的结果是多少呢?

画出对应二进制

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0

我们发现没有对应全为1的根据按位与的规则结果为32个全为0的序列对应值为0

对于按位或来说

#include<iostream>
#include<algorithm>
#include<vector>
using  namespace std;

int main() {
	int a = 1;
	int b = 2;
	cout << (a | b) << endl;
	return 0;
}

同样的我们画出a和b对应的二进制

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0

我们发现有两个位置是有一共1的 所以结果的二进制序列为:

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1

算出来就是3

^同样的我们看上面这分代码

#include<iostream>
#include<algorithm>
#include<vector>
using  namespace std;

int main() {
	int a = 1;
	int b = 2;
	cout << (a ^ b) << endl;
	return 0;
}

同样的我们画出对应的二进制:

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0

根据相同为0相异为1的规则我们可以得到答案的二进制序列:

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1

结果为3

#include<iostream>
#include<algorithm>
#include<vector>
using  namespace std;

int main() {
	int a = 1;
	int b = 2;
	cout << (b>>1) << endl;
	return 0;
}

首先我们先画出b的二进制信息

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0

按照上面的规则右边抛弃左边补符号位

我们得到答案的二进制序列:

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

结果为1

这给我么有一种除二的感觉事实上是的右移一位相于除2同理左移一位相当于乘2 

其他位运算留给老铁自行验证

2.位运算对应奇淫技巧

1.常用位运算
1.n&(n-1)消除数字n的二进制表示中的最后一个1

2.(x & 1) == 1 ---等价---> (x % 2 == 1)
3.(x & 1) == 0 ---等价---> (x % 2 == 0)
4.x / 2 ---等价---> x >> 1
5.x &= (x - 1) ------> 把x最低位的二进制1给去掉
6.x & -x -----> 得到最低位的1
7.x & ~x -----> 得到0

2.指定位置的位运算:

将X最右边的n位清零:x & (~0 << n)
获取x的第n位值:(x >> n) & 1
获取x的第n位的幂值:x & (1 << n)
仅将第n位置为1:x | (1 << n)
仅将第n位置为0:x & (~(1 << n))
将x最高位至第n位(含)清零:x & ((1 << n) - 1)
将第n位至第0位(含)清零:x & (~((1 << (n + 1)) - 1))

3.异或结合律

x ^ 0 = x, x ^ x = 0
x ^ (~0) = ~x, x ^ (~x) = ~0
a ^ b = c, a ^ c = b, b ^ c = a

(有没有点乘法结合律的意思)
字母表示:(a ^ b) ^ c = a ^ (b ^ c)
图形表示:(☆ ^ ◇) ^ △ = ☆ ^ (◇ ^ △)

4.字母大小写运算

1.大写变小写、小写变大写:字符 ^= 32 (大写 ^= 32 相当于 +32,小写 ^= 32 相当于 -32)
2.大写变小写、小写变小写:字符 |= 32 (大写 |= 32 就相当于+32,小写 |= 32 不变)
3.大写变大写、小写变大写:字符 &= -33 (大写 ^= -33 不变,小写 ^= -33 相当于 -32)

3.常见面试和OJ题

1.判断一个数是否为2的幂

对应letecode链接:
https://leetcode-cn.com/problems/power-of-two/

题目描述:

给你一个整数 n,请你判断该整数是否是 2 的幂次方。如果是,返回 true ;否则,返回 false。

如果存在一个整数 x 使得 n == 2x ,则认为 n 是 2 的幂次方。

示例 1:

输入:n = 1
输出:true
解释:20 = 1
示例 2:

输入:n = 16
输出:true
解释:24 = 16
示例 3:

输入:n = 3
输出:false
示例 4:

输入:n = 4
输出:true
示例 5:

输入:n = 5
输出:false

提示:

-2^31 <= n <= 2^31 - 1

解题思路:

如果一个数是2的幂那么他的二进制序列中肯定只有一个1

所以我们可以利用n&(n-1)来消去那个1如果为0则是2的幂否则不是2的幂

对应代码:

class Solution {
public:
    bool isPowerOfTwo(int n) {
       return n>0&&((n&(n-1))==0);//注意==的优先级大于按位与
    }
};

2.不使用临时变量交换两个数

先看代码:

int main() {
	int a = 1;
	int b = 2;
	a ^= b;
	b ^= a;
	a ^= b;
	cout << a << " " << b;
	return 0;
}

利用异或相同为0相异为1的规则及交换律

“a = a ^ b;”和“b = b ^ a;”相当于b = b ^ (a ^ b),而b ^ a ^ b等于a ^ b ^ b。b ^ b的结果为0,因为同一个数与相向相^,结果必为0。因此b的值等于a ^ 0,即a,其值为2。

再执行第三个赋值语句:“a = a ^ b”。由于a的值等于(a ^ b),b的值等于(b ^ a ^ b),因此,相当于a = a ^ b ^ b ^ a ^ b,即a的值等于a ^ a ^ b ^ b ^ b,等于b。

也可以合并成一句:

int main() {
	int a = 1;
	int b = 2;
	a ^= b ^= a ^= b;
	cout << a << " " << b;
	return 0;
}

3.二进制中1的个数

对应letecode链接:
https://leetcode-cn.com/problems/er-jin-zhi-zhong-1de-ge-shu-lcof/ 

题目描述

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为 汉明重量).)。

提示:

请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
在 Java 中,编译器使用 二进制补码 记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数 -3。

示例 1:

输入:n = 11 (控制台输入 00000000000000000000000000001011)
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
示例 2:

输入:n = 128 (控制台输入 00000000000000000000000010000000)
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
示例 3:

输入:n = 4294967293 (控制台输入 11111111111111111111111111111101,部分语言中 n = -3)
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。

提示:

输入必须是长度为 32 的 二进制串 。

解题思路:依然是利用n&(n-1)每次消去末尾的1我们可以定义一个变量Count计数n每次&=(n-1)直到n为0

对应代码:

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int Count=0;
        while(n){
            Count++;
            n&=n-1;
        }
        return Count;
    }
};

4.交换奇数位和偶数位

对应letecode链接:
https://leetcode-cn.com/problems/exchange-lcci/

题目描述:

配对交换。编写程序,交换某个整数的奇数位和偶数位,尽量使用较少的指令(也就是说,位0与位1交换,位2与位3交换,以此类推)。

示例1:

输入:num = 2(或者0b10)
 输出 1 (或者 0b01)
示例2:

输入:num = 3
 输出:3
提示:

num的范围在[0, 2^30 - 1]之间,不会发生整数溢出。

解题思路:

我们可以将奇数位和偶数位提取出来在把奇数位全部后移一位即可但是如何提取呢?

我们来看:

xaaaaaaaa 10101010101010101010101010101010 (偶数位为1,奇数位为0)
0x55555555 1010101010101010101010101010101 (偶数位为0,奇数位为1)

我么发现用这两个序列和对应的数字相与不就行了吗?让后再让偶数位左移一位奇数位右移一位即可

对应代码:

class Solution {
public:
    int exchangeBits(int num) {
        return ((num&0xaaaaaaaa)>>1)|((num&0x55555555)<<1);
    }
};

5.数组中只出现一次的数字

对应letecode链接:
剑指 Offer 56 - I. 数组中数字出现的次数 - 力扣(LeetCode) (leetcode-cn.com)

题目描述:

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

示例 1:

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]
示例 2:

输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]

限制:

2 <= nums.length <= 10000

解题思路:

当寻找数组中的一个只出现一次的数字时,我们可以想到用位运算中的异或,对整个数组数字进行异或,剩下的一定是那个出现一次的数字,但是当有两个出现一次的数字时,如何去查找呢?
如果能把这两个数字分别划分到两个数组中,那么就可以用上述方法实现查找。所以关键是如何划分这两个数组。

思路:将整个数组元素进行异或,最后得到的是这两个数的异或值,那么这两个数中二进制位的第一个不相同的位,其异或值一定为1,找到这个1的位置,将这个位置是1的分到一边,不是的分到另一边,就完成了包含两个不同数字的数组的划分。 

对应代码:

class Solution {
public:
    vector<int> singleNumbers(vector<int>& nums) {
             int res=0;
             int ans=0;
            for(auto x:nums){
               res^=x;
            }
         int RightOne=res&(-res);//取出最右边第一个为1的序列
          for(auto x:nums){
            if(x&RightOne){
              ans^=x;
            }
        }
        return{ans,res^ans};//返回结果
    }
};

6.数中只出现一次的元素II

对应letecode链接:

剑指 Offer 56 - II. 数组中数字出现的次数 II - 力扣(LeetCode) (leetcode-cn.com)

题目描述:

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例 1:

输入:nums = [3,4,3,3]
输出:4
示例 2:

输入:nums = [9,1,7,9,7,9,7]
输出:1

限制:

1 <= nums.length <= 10000
1 <= nums[i] < 2^31

解题思路:

如果对应32个比特位对应一个位置上出现的次数%3如果等于0说明只出现一次的数字在这一位上没有1如果不为0则说明在这一位上有1于是我么可以统计每一位1的数量来还原这个数字 

class Solution {
public:
    int singleNumber(vector<int>& nums) {
            int ans=0;
           for(int i=0;i<32;i++){
               int Count=0;
               for(auto x:nums){
                Count+=(x>>i)&1;//统计数组中第i位1出现的次数
               }
               if((Count%3)==0){
                   continue;//如果是0在这一位上没有1
               }

               if(Count%3==1){//有1
                     ans+=(Count%3)<<i;
               }
               else{
                      return -1;
                }
           }

           return ans;
    }
};

这道题又可以升级为数组中有一种数出现了k次其他数都出现了M次求出现k次的数,如果不存在出现k次数的数请返回-1

(k<m,m>1)

思路与上面是一样的

对应代码:

int onlyKTimes(vector<int>nums,int k,int m){
  int Count=0;
int ans=0;
 for(int i=0;i<32;i++){
    for(auto x:nums){
      Count+=(x>>i)&1;
      }
     if(Count%m==0){
        continue;
      }
      if(Count%m==k){
        ans|=(1<<i);
       }
     else{
       return -1;
       }
}
return ans;
}

3.不用加减乘除做加法减法乘法除法

对应letecode链接:
https://leetcode-cn.com/problems/add-without-plus-lcci/

题目描述:

设计一个函数把两个数字相加。不得使用 + 或者其他算术运算符。

示例:

输入: a = 1, b = 1
输出: 2

提示:

a, b 均可能是负数或 0
结果不会溢出 32 位整数

首先我们来看加法公式:

低位开始按位相加,逢10进一,累加到高位,依次进行得到最后结果。我们把 每一位 加法的输出分为:本位 和 进位
    6               3
 +  8             + 4
--------         -----
  1 4             0 7
  ^ ^             ^ ^
进位 本位        进位 本位
4 是本位         7 是本位
1 是进位         0 是进位

因此我们可以模拟实现加法。对应二进制数相加逢2就要向前进位也就是对应位置都为1的时候,进位完之后就变成了0.在这里我们可以使用^来实现此时得到的结果并没有加上进位。对应进位信息我们在小学进行加法计算时出现进位我们是向前进位在这里我么的进位信息要往前进。那么就等价于前面没有加进位的数+进位*2.

可能有些老铁还不太明白我们来举个例子:

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 1 

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1

比如说这两个数进行相加假设我们假设第一个数为a第二个数为b

我们先要计算没有加进位的结果即a^b我们得到的二进制序列为

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 1 0 0 0 0 0结果记做a’

进位信息为a&b对应二进制序列为:

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1结果记做b'但是进位是要向前进的所以在二进制序列中b'要乘2来向前进位

最后原来相加的结果为a'+b'*2(进位)但是我们不能够使用加法所以我么只能对应a'和b'*2进行上面的操作直到进位为0

对应代码:

class Solution {
public:
    int add(int a, int b) {
       int sum=a;
       while(b!=0){//进位不等于0
         sum=a^b;//没有加进位的结果
         b=(unsigned int)(a&b)<<1;//注意进位没有负数所以要强转为无符号整数
         a=sum;//迭代 继续计算 a'+b’*2;
       }
       return sum;
    }
};

递归代码:

class Solution {
public:
    int add(int a, int b) {
        return !b?a:add(a^b,(unsigned int)(a&b)<<1);
    }
};

如何实现减法呢?其实实现了加法减法就非常的简单,减去一个数就相当于加上他的相反数

在计算机中就相当于加上它的补码,如何求它的补码了?我们只需要将一个数的二进制全部取反然后在加1就可以了

对应代码:

#include<iostream>
using namespace std;
int add(int a, int b) {
	return !b ? a : add(a ^ b, (unsigned int)(a & b) << 1);
}
int Minus(int a, int b) {
	return add(a, add(~b, 1));//由于我们不能够使用加法所以~b+1要用add(~b,1)代替
}
int main() {
	cout << Minus(0, -1);
	return 0;
}

实现乘法

例如,对于11 × 12 = 132 ,用二进制绝对值表示为1011 × 1100 = 10000100 ,手动计算演示为:

1011×1100

−−−−−−−

0000

00000

101100

+1011000

−−−−−−−

10000100​​

对应代码:

#include<iostream>
using namespace std;
int add(int a, int b) {
	return !b ? a : add(a ^ b, (unsigned int)(a & b) << 1);
}
int multi(int a, int b) {
	int ans = 0;
	while (b) {
		if (b & 1) {
			ans = add(ans, a);//相加
		}
		a <<= 1;
		b = (unsigned int)b >> 1;//如果是负数不强转为无符号整型会死循环
	}
	return ans;
}
int main() {
	cout << multi(-10, -1);
	return 0;
}

实现除法:

首先让我们回忆一下小学就学过的长除法,不过这里我们用二进制来书写:
考虑除法 48 / 9 , 48 的二进制表示为 11 0000 ,9 的二进制表示为 1001

所以 48 / 9 = 5
现在我们现在考虑答案 5 的每一位是怎么得来的:

容易看出:
从左往右第一位的 1 是 48 / 36 = 1
从左往右第二位的 0 是 (48 - 1 * 36) / 9 = 0
从左往右第三位的 1 是 (48 - 1 * 36 - 0 * 18) / 9 = 1

即: 48 / 9 = (48 - 36) / 9 + 4 = (48 - 36 - 9) / 9 + 1 + 4 = 1 + 4 = 5

基本步骤:

1.将被除数与除数都转换为负数,便于统一计算;
2.当被除数大于除数时,继续循环比较判断被除数是否大于除数的2倍、4倍、2^k倍;
3.如果被除数最多大于除数的2^k被,那么将被除数减去除数的2^k倍;
4.将剩余的被除数重复前面的2、3步骤,由于每次将除数翻倍,因此时间复杂度是O(logN)

代码实现:

#include<iostream>
using namespace std;
int add(int a, int b) {
	return !b ? a : add(a ^ b, (unsigned int)(a & b) << 1);
}
int multi(int a, int b) {
	int ans = 0;
	while (b) {
		if (b & 1) {
			ans = add(ans, a);//相加
		}
		a <<= 1;
		b = (unsigned int)b >> 1;//如果是负数不强转为无符号整型会死循环
	}
	return ans;
}
int Minus(int a, int b) {
	return add(a, add(~b, 1));
}
int Div(int a, int b) {
	int x = a < 0 ? -a : a;
	int y = b < 0 ? -b : b;
	int res = 0;
	for (int i = 30; i >= 0; i = Minus(i, 1)) {
		if ((x >> i) >= y) {//注意x右移和y左移是一样的但是y左移可能会移到符号位变成负数从而使结果错误
			res |= (1 << i);//还原
			x = Minus(x, y << i);

		}
	}
	return (a < 0) ^ (b < 0) ? -res : res;
}

int Divide(int a, int b) {
	if (a == INT_MIN && b == INT_MIN) {
		return 1;
	}
	else if (b == INT_MIN) {
		return 0;
	}
	else if (a == INT_MIN) {
		if (b == -1) {
			return INT_MAX;
		}
		else {
			//注意整型最小无法取绝对值故我们可以先让整型最小加个1
			//取的值之后在乘以b在计算差值再去除以b,将得到的结果和之前得到的结果相加则是结果
			int ans = Div(add(a, 1), b);//等价于(a+1)/b;
			return add(ans, Div(Minus(a,multi(ans,b)), b));
		}
	}
	return Div(a, b);
}

int main() {
	cout << Divide(6, 3);
	return 0;
}

相关文章