位运算其实是十分简单的算法,但碍于操作符功能较混杂,劝退了一部分的小伙伴,那么这一篇从 实战的角度吃透位运算,
一、计算机中存储数值的方式
下面以int
类型为例来讲解计算机中存储数值的方式
1、原码
正数的原码是其二进制
负数的原码是先得到其正数的二进制,然后在最高位补1(也就是符号位,位于最左侧)
- 20的原码是 00000000 00000000 00000000 00010100
- -20的原码是 10000000 00000000 00000000 00010100
- 2147483647的原码是01111111 11111111 11111111 11111111
- -2147483647的原码是11111111 11111111 11111111 11111111
但此时有个问题,存在+0
和-0
两种情况,所以原码不能解决问题
2、反码
正数的反码是其原码
负数的反码是其原码的最高位不变,其余位取反
- 20的反码是 00000000 00000000 00000000 00010100
- -20的反码是 11111111 11111111 11111111 11101011
- 2147483647的反码是01111111 11111111 11111111 11111111
- -2147483647的反码是10000000 00000000 00000000 00000000
仍存在+0
和-0
两种情况,所以反码仍不能解决问题
3、补码
正数的补码是其反码
负数的补码是其反码的最低位+1
- 20的补码是 00000000 00000000 00000000 00010100
- -20的补码是 11111111 11111111 11111111 11101100
- 2147483647的补码是01111111 11111111 11111111 11111111
- -2147483647的补码是10000000 00000000 00000000 00000001
- -2147483648的补码是10000000 00000000 00000000 00000000,是一个特殊的存在
此时+0
和-0
就一致成了0
,所以在计算机中,数值是以补码的方式来存储的,若要求出其十进制下的数字,得层层反推:补码,反码,原码
4、整理一下
正数的原码,反码,补码均为其二进制
负数的原码为其正数的二进制最高位补1,反码为其原码除最高位取反,补码为其反码最低位+1
然后我们得以解释-2147483648
的由来以及为什么int
类型是个环
- 2147483647的补码是 01111111 11111111 11111111 11111111
- 1的补码是 00000000 00000000 00000000 00000001
- -1的补码是 11111111 11111111 11111111 11111111
- 2147483647 + 1 = 10000000 00000000 00000000 00000000 = -2147483648
- -2147483648 + (-1) = 01111111 11111111 11111111 11111111 = 2147483647
这里-2147483648
最高位的1
不仅代表的是负号,也代表的是2的32次方(会在>>
运算符中讲到)
二、位运算相关运算符
实战归实战,相关运算符还得先看一遍
1、非运算符~
将补码的所有位取反即可
- 20的补码是 00000000 00000000 00000000 00010100
- ~20的补码是 11111111 11111111 11111111 11101011 = -21
- 2147483647的补码是 01111111 11111111 11111111 11111111
- ~2147483647的补码是 10000000 00000000 00000000 00000000 = -2147483648
- 0的补码是 00000000 00000000 00000000 00000000
- ~0的补码是 11111111 11111111 11111111 11111111 = -1
我们可以推出结论,对于一个十进制的数x:~x = -x - 1
2、与运算符&
比较两个数的补码,按位比较,同为1取1,否则取0
- 20的补码是 00000000 00000000 00000000 00010100
- -20的补码是 11111111 11111111 11111111 11101100
- -20 & 20 = 00000000 00000000 00000000 00000100 = 4
3、或运算符|
比较两个数的补码,按位比较,同为0取0,否则取1
- 20的补码是 00000000 00000000 00000000 00010100
- -20的补码是 11111111 11111111 11111111 11101100
- -20 | 20 = 11111111 11111111 11111111 11111100 = -4
4、或运算符^
比较两个数的补码,按位比较,相异取1,否则取0
- 20的补码是 00000000 00000000 00000000 00010100
- -20的补码是 11111111 11111111 11111111 11101100
- -20 ^ 20 = 11111111 11111111 11111111 11111000 = -8
特别的,0 ^ x = x
,x ^ x = 0
5、左移运算<<
将补码每一位左移n
位,低位补0,高位舍弃
- 20的补码是 00000000 00000000 00000000 00010100
- 20 << 1 是 00000000 00000000 00000000 00101000 = 40
- 20 << 2 是 00000000 00000000 00000000 01010000 = 80
- 2147483647的补码是 01111111 11111111 11111111 11111111
- 2147483647 << 1 是 11111111 11111111 11111111 11111110 = -2
- 1073741824的补码是 01000000 00000000 00000000 00000000
- 1073741824 << 1 是 10000000 00000000 00000000 00000000 = -2147483648
6、右移运算>>
将补码每一位左移n
位,高位补0,低位舍弃,相当于除以二
- 20的补码是 00000000 00000000 00000000 00010100
- 20 >> 1 是 00000000 00000000 00000000 00001010 = 10
- 20 >> 2 是 00000000 00000000 00000000 00000101 = 5
- -2147483648的补码是10000000 00000000 00000000 00000000
- -2147483648 >> 1 是11000000 00000000 00000000 00000000 = -1073741824
也就是说-2147483648
最高位的1
不仅代表的是负号,也代表的是2的32次方
三、其他进制
通过位运算,可以得到一个数在2的指数进制下的值,比如4进制,8进制,16进制
1、分组
每个int
在计算机中是32个bit,所以4进制,6进制,16进制可以如下表示
4进制:分成十六组,每组最大值为3,每个组都代表一个4进制数
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
8进制:分成十一组,除最高位的组外,每组最大值为7,每个组都代表一个8进制数
00 000 000 000 000 000 000 000 000 000 000
16进制:分成八组,每组最大值为15,每组代表一个16进制数
0000 0000 0000 0000 0000 0000 0000 0000
运用整体的思想,把32位拆成组,对每组分别求值
2、求值
求值的过程就是通过位运算将某个组独立出来,再求值
通过
(num >> n * k) & p
将组独立出来并求值
>>
来去除低位的bit,&
来去除高位的bit
其中k
为每一组的bit数,4进制为2,8进制为3,16进制为4,
n为倍数,从32 / k - 1
开始,到0
结束,目的是从高位到低位取得num的各组
p
为进制数 - 1,4进制是3(二进制:11),8进制是7(二进制:111),16进制为15(二进制:1111)
举个例子,求41的4进制
- 41的补码:00000000 00000000 00000000 00101001
- 41以4进制分组(高位重复的0暂时忽略):00 10 10 01
(41 >> 3 * 2) & 3 = (二进制)00
即0
,(41 >> 2 * 2) & 3 = (二进制)10
即2
(41 >> 1 * 2) & 3 = (二进制)10
即2
,(41 >> 0 * 2) & 3 = (二进制)01
即1
- 所以41的4进制为(高位重复的0暂时忽略):
0221
3、实战题目:LeetCode 405 数字装换成十六进制数
通过实战来讲解
首先分组,取-1举例,16进制将分成8个组
- -1的补码:11111111 11111111 11111111 11111111
- -1以16进制分组:1111 1111 1111 1111 1111 1111 1111 1111
(-1 >> 7 * 4) & 15 = (二进制)1111
即15
,十六进制下为f
,得到每一组的结果均为f
- 则-1的16进制为:
ffffffff
具体实现细节不多讲了,LeetCode 405 官方题解写的非常清楚
public String toHex(int num) {
if (num == 0) return "0";
StringBuilder builder = new StringBuilder();
// 分为8组,从最左端开始算
for (int i = 7; i >= 0; i--) {
int val = (num >> (4 * i)) & 15; // 0xf
if (builder.length() > 0 || val > 0)
builder.append(val <= 9 ? (char)('0' + val) : (char)('a' + val - 10));
}
return builder.toString();
}
四、位运算实战操练
尝试取理解位运算的各种应用,以理解代替死记硬背
1、清除x最低位的1
清除x最低位的1:x & (x - 1)
我们假设x的补码,并找到最低位的1(其中a
可以是1或0)
- x的补码是 aaaaaaaa aaaaaaaa aaaaaaaa aaaa1000
- x - 1的补码是 aaaaaaaa aaaaaaaa aaaaaaaa aaaa0111
- x & (x - 1) 是 aaaaaaaa aaaaaaaa aaaaaaaa aaaa0000
也就是利用x-1
将x
低位的1和0恰好错开,使得&
只能得到0
1)例题一:LeetCode 191 位1的个数
只要n != 0,则它的补码中还有1。我们不断删除最低位的1,每删一次就记录一次,直到n = 0
public int hammingWeight(int n) {
int ans = 0;
while (n != 0) { // 只要n比0大,就还有1
// 清除最低位的1
n &= (n - 1);
ans++;
}
return ans;
}
2)例题二:LeetCode 231 2的幂
如果一个数是2的幂次方,它得是一个正数,并且它的补码中只有1个1,也就是说,清除最低位的1后应该为0
public boolean isPowerOfTwo(int n) {
// 清除最低位的1
return n > 0 && (n & (n - 1)) == 0;
}
3)实战题目:
LeetCode 338 比特位计数
2、获取 / 修改第n位
获取第n位:(x >> n) & 1
将第n位置1:x | (1 << n),利用|
“有1则为1”的性质
将第n位置0:x & (~(1 << n)),利用&
的性质,1 & 1或0 = 1或0(不变)
,0 & 1或0 = 0
将第n位取反:x ^ (1 <<n),利用^
的性质0 ^ x = x
我们假设x = 20
- 20的补码是 00000000 00000000 00000000 00010100
- 设n = 7,1 << n 的补码是 00000000 00000000 00000000 10000000
- 设n = 2,1 << n 的补码是 00000000 00000000 00000000 00000100
- 设n = 2,~(1 << n)的补码是 11111111 11111111 11111111 11111011
1)例题一:LeetCode 190 颠倒二进制位
ans不断左移并加上获取的n的每一位即可,类似于LeetCode 7 整数反转
public int reverseBits(int n) {
int ans = 0;
for (int i = 0; i < 32; i++)
// n >> i & 1 得到尾数是否为1
ans = (ans << 1) + (n >> i & 1);
return ans;
}
2)实战题目:
LeetCode 52 N皇后Ⅱ
LeetCode 36 有效的数独
LeetCode 37 解数独
实战会偏向于硬核,将第n位置0置1通常用在代替boolean
3、异或
0 ^ x = x,x ^ x = 0
1)例题一:LeetCode 136 只出现一次的数字
由于其余数字均出现两次,利用x ^ x = 0
可以很快得到答案
public int singleNumber(int[] nums) {
int ans = 0;
for (int x : nums)
ans ^= x;
return ans;
}
2)实战题目
LeetCode 260 只出现一次的数字Ⅲ
4、高级用法
由于用的次数不多,作为了解理解一下即可
lowbit:x & -x
将n位低位置0:x & (~0 << n)
将n位高位置0:x & (~0 >> n)
lowbit在树状数组中出现,可以参看我的上一篇博客:数据结构:树状数组:姐来展示下什么叫高端前缀和