位运算理论篇
位运算概述
计算机中的数在内存中都是以二进制形式进行存储的 ,而位运算就是直接对整数在内存中的二进制位进行操作,执行效率非常高,在程序中使用位运算进行操作,会大大提高程序的性能。
位运算符
名称 | 符号 | 描述 |
---|---|---|
按位与 | & | 如果两个数相应的二进制位为1,则该位的结果值为1,否则为0 |
按位或 | | | 两个数相应的二进制位中只要有一个为1,该位的结果值为1 |
按位亦或 | ^ | 两个二进制位相同则为0,否则为1 |
按位取反 | ~ | 用来对一个二进制数按位取反,即将0变1,将1 |
左移 | << | x<<n代表将一个数x的各二进制位全部左移n位,右补0,比如1<<3的值为8,因为它的二进制从0001,变成了1000 |
右移 | >> | x>>n代表将一个数x的各二进制位全部右移n位,左补0,如8>>3就是1,二进制从1000变成了0001 |
位运算符的优先级
可参考运算符优先级表
位运算符的运算律
名称 | 运算式 |
---|---|
交换律 | A&B==B&A,AB==BA |
结合律(同运算符下) | ABCA(BC),A&B&CA&(B&C) |
同一律 | A|0==A,A&1=A |
幂等律 | A^AA,A&AA |
零律 | A|1==0 |
互补律 | A|A=-1,A&A=0 |
位运算符的应用
常见应用
应用 | 原理 |
---|---|
获取第k个二进制位(从0开始):n>>k&1 | 例如:二进制100100>>21001,1001&11001&0001==1 |
消去数x上的最后面一位1:x&(x-1)。 | 分析x-1的两种情况,需要借位和不需要借位:不需要借位时,x-1的最后一位是0,x的最后一位是1,&运算后得到x-1,除了最后一位变为0其他完全相同;需要借位时,前一位如果是0,继续往前面借位,最终最后一位1被借位借走变为0,例如1000000变成0111112,减去1后变为0111111,&操作后全部为0。同样的,我们也可以消去最后一个0,只需使用x&(x+1) |
判断一个数是否是2的次幂:判断x&(x-1)是否为0 | 如果一个数是2的次幂,那么它的二进制位只有一个1,我们把这个1消去,判断是否为0 |
判断一个整数二进制位有多少个1:1.使用n>>k&1逐个判断。2.使用x&(x-1)逐个消去 | 二进制位上有多少个1,我们可以逐个判断它的最后一个二进制位到第一个二进制位是否为1,也可以通过不断消去最后一位1并记录进行多少次后该为0。同理,用来判断多少个0也是可以的 |
将整数x转换为y,需要改变多少个二进制位:判断x|y的二进制位中1的个数 | 将整数x转换为y,如果x和y在第i位上相等,则不需要改变这一位,如果在第i位上不相等,则需要改变这一位。所以问题转化为了x和y有多少个二进制位不相同。位运算中存在异或操作,相同为0,相异为1,所以问题转变成了计算x异或y之后这个数中1的个数。 |
返回x的最后一位1:x&-x或者x&(~x+1)例如:1000100返回成为100 | 例如一个二级制串:……10000,用~取反后变为……01111,再加一后变为……10000,此时再进行&操作,因为x的最后一位1前面是所有的二进制位都已经取反了,所以&操作得到的结果为00000……10000 |
二进制进行枚举 | 例如S = [1,2,3],我们需要得到[ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2] ],那么我们可以用二进制表示他们之间的枚举关系:此处有3个元素,我们可以选择000,001,010,011,100,101,111这些二进制,第n个二进制位上为1,我们就选择第n个元素,如101选择1和3,000不选择任何元素 |
一个序列中,只有一个数出现一次,剩下都出现两次,找出出现一次的。 | 因为只有一个数恰好出现一个,剩下的都出现过两次,所以只要将所有的数异或起来,就可以得到唯一的那个数。 |
一个序列中,只有一个数出现一次,剩下都出现三次,找出出现一次的。 | 因为数是出现三次的,也就是说,只要我们把所有数第n位上二进制取出来相加求和,再对三取余,最终得到的一定是只出现一次数第n位上的二进制位 |
最后一个应用给出两种样例代码
int singleNumber(vector<int>& nums) {
int ans = 0;
int help[32]={0};//存储i位上的2进制之和
for(int i=0;i<nums.size();i++)
for (int j = 0; j <= 31; j++)
{
help[j] += ((nums[i] >> j)&1);
}
for (int i = 0; i <= 31; i++)
ans |= ((help[i] % 3)<<i);
return ans;
}
int singleNumber(vector<int>& nums) {
int ans = 0;
for (int i = 0; i < 32; ++i) {
int total = 0;
for (int num: nums) {
total += ((num >> i) & 1);
}
if (total % 3) {
ans |= (1 << i);
}
}
return ans;
}
位运算表示一些常规运算
- 交换两数:
void swap(int &a,int &b){
a ^= b;
b ^= a;
a ^= b;
}
- 实现乘以2和除以2:a<<1a*2,a>>1a/2
- 位运算判断奇偶性:在二进制中,最低位决定了是奇数还是偶数,所以我们可以提取出最低位的值,也就是判断a&1是1还是0,是0则是偶数,是1则是奇数
- 改变正负:-x=~x+1,关于详细原因可以参考原码反码和补码的知识
- 实现加法A+B:我们可以使用异或操作来实现,因为异或操作其实是不进位加法。那么进位操作我们就可以通过A&B来实现,因为A&B得到的都是A和B上都有的1,我们左移后得到的就是进位之后的结果
int aplusb(int a, int b) {
while(b){
int c = a ^ b;
int d = (a & b) << 1;
a = c;
b = d;
}
return a;
}
这题我们可以先排序再判重,判重使用双指针时间复杂度为n,排序时间复杂度为nlgn,最终时间复杂度为nlgn,空间lgn,因为要使用栈空间排序。
当然我们也可以用哈希表存储,遍历一次存入哈希表,选出只出现一次的就行了,时间复杂度n,空间复杂度nlgn。
但是我们还可以使用位运算,它的时间复杂度也是n,我们甚至可以把空间复杂度降低到1.
时间复杂度n,空间复杂度n
假设出现一个的两个元素是x,y,那么最终所有的元素异或的结果就是res = x^y。并且res!=0,那么我们可以找出res二进制表示中的某一位是1,那么这一位1对于这两个数x,y只有一个数的该位置是1。对于原来的数组,我们可以根据这个位置是不是1就可以将数组分成两个部分。求出x,y其中一个,我们就能求出两个了。
代码如下:
vector<int> singleNumber(vector<int>& nums) {
vector<int>a,b,ans(2,0);
int n,res=nums[0];
for(int i=1;i<nums.size();i++)
res^=nums[i];
for(n=0;n<=31;n++)
{
if(res>>n&1)
break;
}
for(int i=0;i<nums.size();i++)
if(nums[i]>>n&1)
a.push_back(nums[i]);
else
b.push_back(nums[i]);
ans[0]=a[0];
ans[1]=b[0];
for(int i=1;i<a.size();i++)
ans[0]^=a[i];
for(int i=1;i<b.size();i++)
ans[1]^=b[i];
return ans;
}
时间复杂度n,空间复杂度1
整体思路差不多,但是利用了0^x=x的运算律,避免开辟空间存储
vector<int> singleNumber(vector<int>& nums) {
vector<int>ans(2,0);
int n,res=nums[0];
for(int i=1;i<nums.size();i++)
res^=nums[i];
for(n=0;n<=31;n++)
{
if(res>>n&1)
break;
}
for(int i=0;i<nums.size();i++)
if(nums[i]>>n&1)
ans[0]^=nums[i];
else
ans[1]^=nums[i];
return ans;
}