文章目录
位运算的各种姿势
LeetCode官方解释:
位操作(Bit Manipulation)是程序设计中对位模式或二进制数的一元和二元操作。在许多古老的微处理器上,位运算比加减运算略快,通常位运算比乘除法运算要快很多。在现代架构中,情况并非如此:位运算的运算速度通常与加法运算相同(仍然快于乘法运算)。
位操作包括:
- ¬ 取反(NOT)
- ∩ 按位或(OR)
- ⊕ 按位异或(XOR)
- ∪ 按位与(AND)
- 移位,
移位是一个二元运算符,用来将一个二进制数中的每一位全部都向一个方向移动指定位,溢出的部分将被舍弃,而空缺的部分填入一定的值。
移位又分为- 算术移位
- 逻辑移位
位运算是计算机中非常重要的运算方式,位运算的符号有(&、|、^、~、>>、<<)
注:以下True也可以是1,False也可以是0
&
:这是一个与运算符号,效果是两者都为True才是True。1 & 1 = 1, 1 & 0 = 0
|
:或运算符号,效果是两者其一为True,就是True。1 | 1 = 1, 1 | 0 = 1
^
:异或运算符号,效果是相同为False,相反为True。1 ^ 1 = 0, 1 ^ 0 = 1
~
:取反运算符号,效果是True为False,False为True。>>
:右移运算符号,效果是将一个数的二进制形式向右移动。7 = 111, 7 >> 1 = 11 = 3
(移动一位等于除以2向下取整,两位就是除以4)<<
:左移运算符号,效果是将一个数的二进制形式向左移动。7 = 111, 7 << 1 = 1110 = 14
(移动一位等于乘以2,两位就是乘以4)
姿势1(x & 1):
x & 1 == 0
表示判断是否是偶数
x & 1 == 1
表示判断是否是奇数
因为偶数的二进制形式最后一位是0,奇数最后一位是1
那么7 = 111 & 001 = 1
,而6 = 110 & 001 = 0
姿势2(x & -x):
如果x是偶数,那么x & -x
表示截取这个偶数二进制形式下从最低位的1开始到最后的值,也就是100…0。
比如100 = 1100100
,运算后4 = 100
。
用法,如果这个数是偶数,我们可以x - (x & -x)
,这样这个数的最低位的1就变成了0
如果x是奇数,那么x & -x
是多少呢?答案是:任意奇数算出的答案都是1。
原理-x
在二进制下等于所有位取反(~
)后再加1。比如-100 = 0011011 + 1 = 0011100
。首先,这个数字的最低位的1的右边所有的值全部都取反了,所以做&运算一定都是0,而最低位的1虽然也被取反,但是由于它前面的0全部变成1,+1之后进位,导致前面的1又变为0,自己又变成了1。比如01111 + 1 = 10000
,所以在偶数情况下只为保留最低位的1。
那如果是奇数呢?这样的话最低位就是最右边的那一位,所以只有1。
姿势3(0 ^ x):
0 ^ x = x
(x为任意整数)
姿势4(1 ^ x):
两种情况:
当x为偶数时 1 ^ x = x + 1
当x为偶数时 1 ^ x = x - 1
x同样为任意整数。
姿势5((var ^ (var >> bit)) - (var >> bit)):
如果不用if-else和比较运算符,那么可以使用这个来求解绝对值
其中bit是位,比如int是32位的,long是64位的,但注意要-1,32位最多只能移31位
摘自别人的解释:
- var >= 0: var >> 7 => 0x00,即:(var ^ 0x00) - 0x00,异或结果为var
- var < 0: var >> 7 => 0xFF,即:(var ^ 0xFF) - 0xFF,var ^ 0xFF是在对var的全部位取反,-0xFF <=> +1, 对signed int取反加一就是取其相反数。
举个栗子🌰:var = -3 <=> 0xFD,(var ^ 0xFF) - 0xFF= 0x02 - 0xff= 0x03
作者:dexin
链接:https://leetcode-cn.com/problems/maximum-lcci/solution/ji-yu-wei-yun-suan-shi-xian-da-xiao-bi-jiao-by-dex/
姿势6(两个数的最大值):
求两个数的最大值,这不是很简单嘛。蛋式,你不可以使用if-else和比较运算符。
这是LeetCode上的一道题:https://leetcode-cn.com/problems/maximum-lcci/solution/gen-ju-ti-mu-de-ti-shi-wan-cheng-wei-yun-suan-qiu-/
下面给出题解:
我们要得到一个数k,当这个数为1时,b大,当这个数为0时,a大。得到后,我们使用b * k + a * (k ^ 1)
得出答案,是不是很巧妙!!!那么怎么得到k呢?
我们只要得到a - b
的符号位就可以了,符号位在第一位上,所以如果是int类型,我们要右移31位,如果是long类型我们要右移63位。在这里我们使用long类型,因为a - b
是有可能会溢出的。
class Solution {
public:
int maximum(int a, int b) {
long k = (((long)a - (long)b) >> 63) & 1;
return b * k + a * (k ^ 1);
}
};
最后给出一个写的很不错的文章:https://www.csdn.net/gather_2b/MtzaIg4sOTctYmxvZwO0O0OO0O0O.html
姿势7:两数相加
在不使用算术运算符的情况下完成两数相加。
同样也是LeetCode上的一道题:https://leetcode-cn.com/problems/add-without-plus-lcci/comments/
这题很有意思,我们要想做出这道题,就要先了解二进制的加法。二进制中逢二进一,所以二进制中的每一位1 + 1 = 0(进位)
1 + 0 = 1
0 + 0 = 0
都有这么三种情况。我们发现,除了需要进位,和异或操作(^)一模一样。
那么剩下我们就是要考虑进位情况了,怎么拿出所有要进位的位置呢?我们发现只有两个都为1的情况才需要进位,那么我们只要用与运算(&)就可以取出所有进位的情况。然后把它们向左移动一位,再进行异或操作,就可以完成进位了。
可是现在还有一个问题,进位的过程中也有可能需要进位,所以你需要递归完成这个步骤,直到进位值都是0,也就是不需要进位为止。
class Solution {
public:
int add(int a, int b) {
int res = a ^ b;
if (b == 0) return res;
return add(res, (unsigned int)(a & b) << 1);
}
};
我们还可以精简到一句话
class Solution {
public:
int add(int a, int b) {
return b == 0 ? a : add(a ^ b, (unsigned int)(a & b) << 1);
}
};
姿势8:(num & x)
x可以是一个全为1的二进制数。
比如1111 = 15
,它可以将一个数的前4位取出。取出来的数字就是num % 16
的结果,而剩下的值就是num / 16
的结果。
同理,我们有111 = 7
,11 = 3
, 1 = 1
它们分别可以对8,4,2取模。
姿势9:(求区域异或)
求一个数组中其中一段的异或值。这个很简单,一个一个算即可。但如果操作数量很多显然一个一个算不是好办法。
求区域和可以用前缀和,求区域异或一样可以用区域异或。
class Solution {
public:
vector<int> xorQueries(vector<int>& arr, vector<vector<int>>& queries) {
int size = arr.size();
vector<int> ans, pre_xor; pre_xor.push_back(0);
for (int i = 0; i < size; i++) pre_xor.push_back(pre_xor.back() ^ arr[i]);
for (auto query : queries) {
ans.push_back(pre_xor[query[1] + 1] ^ pre_xor[query[0]]);
}
return ans;
}
};
姿势10:(提取最高位的1)
先来看代码
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
返回的答案就是i
只保留最高位的1其余都为0的值。
怎么做到的呢?其实很简单。我们设最高位的1
是目标位,整个数是001xxx..xx
我们假设x
可以是1也可以是0。
- 第一个或运算必定会把目标位右边一位变成1,不管是0是1。即
0011xxxx..xx
- 第二个或运算必定会把目标位右边3位变成1,即
001111xxx..x
- 第三个或运算必定会把目标位右边7位变成1,即
0011111111xx...x
- 第四个或运算必定会把目标位右边15位变成1,即
001111...1111xx...x
- 第五个或运算必定会把目标位右边31位变成1,即
00111111...11111
,因为目标位右边没有31位,所以目标位右边被全部填成了1
。
那么最后这句又是什么意思呢?return i - (i >>> 1);
>>>
是java
的无符号位右移,这样如果是负数那么符号位依然补0不会补1。
最后返回值的答案就是0011111...111 - 00011111..111
。后者少了一个1。所以相减后就是001000..000
。这样就只保留了目标位。
姿势11:(1的所有子集)
假设有一个数101010
,我想按照它1的排列将它输出(比如111 110 101 100 011 010 001 000
),这怎么办呢?
for (int i = num; i != 0; i = i - 1 & num) {
cout << i;
}
这样,输出的i
就可以满足我们的要求了。
姿势12:(快速乘)
如果在不使用乘号的情况下将两数相乘?
我们可以将两个数排成二进制,然后使用二进制下的竖式计算来让两数相乘。
为什么要用二进制呢?
很简单,第一,计算机底层就是用二进制存储的,所以我们可以直接使用位运算求解。第二,二进制只有0和1,所以二进制下的竖式计算是不需要使用到乘法的,因为要么乘以1,要么乘以0,而乘1就相当于没有乘。
int quickMulti(int A, int B) {
int ans = 0;
for ( ; B; B >>= 1, A <<= 1) if (B & 1) ans += A;
return ans;
}
要么乘以0,而乘1就相当于没有乘。
int quickMulti(int A, int B) {
int ans = 0;
for ( ; B; B >>= 1, A <<= 1) if (B & 1) ans += A;
return ans;
}
这道题就可以使用上面说的方法求解。