java数据结构与算法刷题目录(剑指Offer、LeetCode、ACM)-----主目录-----持续更新(进不去说明我没写完):https://blog.csdn.net/grd_java/article/details/123063846 |
---|
刷题过程中,用到什么关于位运算的知识点,就补充什么知识点到这里
一、基本位运算
1. 异或相关
异或 |
---|
- 两数相同异或为0,两数不相同异或为1
- 任何数a异或0,都等于a本身。0 ⊕ a = a
- 两个相同的数异或必然为0。a ⊕ a = 0;
- 异或具有结合律和交换律。
- 结合律0⊕1⊕2⊕2 = (0⊕1)⊕(2⊕2) = 1 ⊕ 0 = 1;
- 交换律0⊕1⊕2⊕2 = 2⊕1⊕2⊕0
2. 奇偶异或1 = 加一或减一 |
---|
- 如果mid是偶数,mid +1 操作相当于 mid ^ 1
例如2是偶数,二进制为0010,异或1(二进制0001)结果为0011.
- 如果mid是奇数,mid - 1 操作相当于 mid ^ 1
例如1是奇数,二进制为0001,异或1(二进制0001)结果为0000.
实现加法 |
---|
- 我们不能使用加减运算符。那么就只能学习硬件底层的加法器的逻辑来处理了(计算机组成原理中加法器的知识)
- 首先想要实现一位加法器,必须知道如何处理进位信息,例如1+1 = 进位1和本位0
- 也就是说,整个加法器都是由两个操作来处理,无进位加法结果,和进位后结果
- 无进位加法结果实现方法:a异或b。因为我们要实现本位1+0 = 0+1 = 1和0+0 = 1+1 = 0.
注意,这里加法是抛去进位信息的。例如1+1 =
1
0,其中1
是进位,而0是本位,我们这里只要本位信息
- 进位结果实现方法:(a & b) << 1. 因为我们要实现进位1+0 = 0+1 = 0和0+0 = 0,和1+1 = 1
1+1 = 1的原因是 1+1 = 10.也就是逢二进一,所以进位为1.
不容易理解的是进位结果为什么是(a & b) << 1
- 假设a = 0111,b = 0101.此时a&b = 0101,左移一位为1010
- 我们发现右移后的结果,里面的1正好是进位后的1需要去的地方
- 因为a+b = 0
1
11
+ 01
01
,其中标红的位置两个1相加必然会产生进位- 往哪里进位?如果允许2的出现的话,结果会是这样a+b = 0
2
12
- 但是很遗憾,不允许2的出现,所以他俩得向前进位1,也就是0
2
12
=0+1
01+1
0.也就是2需要逢二进一,进位到它们的高位- 因此a & b只能获取哪些地方是加出
2
的,这些2需要向高位进1。例如a&b = 01
01
正好对应a+b = 02
12
中需要进位的2
的位置- 因此我们需要左移一位,获取0
2
12
=0+1
01+1
0中0+1
和1+1
的进1的位置
- 例如a = 1,b = 2. 二进制分别为a = 0001,b = 0010
- 此时不考虑进位,相加结果为a^b = 0011,我们发现压根也不会产生进位信息
- 例如a = 2,b = 3,二进制分别为a = 0010,b = 0011
- 此时a ^ b = 0001,也就是不进位加法结果
- 此时获取需要进位的位置(相加为2的位置),a & b = 0010,其中1的位置,正好是a+b后二进制为按位相加 = 2的位置,这些位置需要进位1
- 因此通过左移操作,获取需要进位的1进位到哪里,(a&b)<<1 = 0100
- 此时将本位结果x = a^b = 0001和进位相加结果y = (a&b)<<1 = 0100,不进位相加x ^ y得到x = 0101 = 5.
- 继续获取进位信息(x&y)<<1 = 0000.发现没有需要进位的了,则加法完成。
2. 与操作
1. 与 |
---|
- 两个数都是1,相与为1. 1&1=1
- 两个数有一个是0,相与为0,1&0=0
2. 奇偶数与1操作 |
---|
mid -= mid&1. 如果mid是奇数,则等价于mid-1变为偶数。如果mid是偶数,则等价于mid-0不进行改变。
假设mid是偶数2,二进制为0010,2&1 = 0010 & 0001 = 0000.也就是mid&1 = 0.mid - 0 = mid。
假设mid是奇数3,二进制为0011,3&1 = 0011&0001 = 0001,也就是mid&1 = 1.mid -=mid&1 ==> mid -1
3. 去末尾1 |
---|
- 对于100000000001.虽然我们一眼看到只有两个1,但是笨办法依然需要将所有的位数都处理一遍
- 我们如何跳过中间的0,直接统计1的个数呢?让上面这一串,只循环右移2次
- 针对这个问题,BK算法出现了,他利用了二进制的特性,实现一次性删除最右侧的1的效果
- 对于十进制来说我们做减法时,低位不够减需要向高位借1,拿过来就是10
- 二进制也一样,不够减就得借1.拿过来就是2
- 也就是说,如果对2进制串进行-1操作的话,最低位是1还好,可以直接减去,如果不够减,就必须一直向高位借,直到遇到一个够借1的。这样就会将2进制串中,最后一个1借掉
- 例如上图中,x = 10001000,-1后最后一个1被借了,变成10000111
- 此时将这两个二进制串进行与运算,就会实现将最后一个1去掉的效果
4. 只保留末尾的1 |
---|
- 正数的补码和源码是一样的,例如1 =
0
,000 0000 0000 0000 0000 0000 0000 0001 (以32位进行保存)- 负数的补码和源码的区别是,
符号位和最右边的1不变
,这两个不变的二进制位中间的其余数值位
全部取反
- 原码:例如-1=
1
,000 0000 0000 0000 0000 0000 0000 0001- 补码:例如-1=
1
,111 1111 1111 1111 1111 1111 1111 1111
- 我们现在有了1和-1的补码。
- 1 =
0
,000 0000 0000 0000 0000 0000 0000 0001- -1=
1
,111 1111 1111 1111 1111 1111 1111 1111- 我们发现,除了最右边的1以外,这个1左边所有的数,都是不同的。
- 如果此时我执行1与-1 也就是 1 & (-1)
我会得到
0
,000 0000 0000 0000 0000 0000 0000 0001,也就是除了最右边的1以外,其余全是0. 这样我就得到了这个数的最低位的那个1.也就是我得到了这个数,最右边的一个二进制1的位置。并且其余二进制位全是0
5.只提取奇数位,或偶数位 |
---|
- 我们将奇数位置标识出来:(10101010101010101010101010101010)二进制中,只对奇数位置操作
- 当然如果转换为16进制,它会比较好看一点:其16进制形式为0xaaaaaaaa
- 如果n&0xaaaaaaaa ==0,就说明n的二进制中的1,只出现在偶数位置上。因为如果出现在奇数位置上,两个二进制为都是1,与操作过后的结果一定也是1.
- 当然我们可以标识偶数位置(01010101010101010101010101010101) = 0x55555555
- 如果n& 0x55555555 = n的话,就说明n中1的位置正好在偶数位置上
3. 补码
补码
- C,C++,java等编程语言中,为了更好的和硬件交互,数字以补码形式存储。
- 各种码的转换关系如下,了解即可,我们只需要统一用补码进行计算即可。(看不懂没关系,继续看下面)
补码(了解即可) |
---|
- 真值:我们通过除基取余法得到的二进制代码,统一称为真值。例如十进制数8的二进制位1000,这个1000就是一个真值。
- 原码:那么如何区分真值是正数还是负数呢?我们只需要用掉开头的一个二进制位,0表示正数,1表示负数。例如8的原码就是
0
000 … 1000 标红的那位就是符号为,剩下的都是数值位. -8的原码就是1
000 … 1000负数的符号位为1.- 补码,方便计算机运算的一种码,它不方便人类理解,但是方便计算机。它可以通过原码来推导
- 正数的补码 = 原码
- 负数的补码 = 符号位不变,其余位取反,然后末位+1。当然我们有一个口诀,就是从右向左找到第一个1,然后将符号位和这个1之间所有元素按位取反即可(图解如下)
不妨做道题:🏆LeetCode645. 错误的集合(位运算解法需要重点掌握)https://blog.csdn.net/grd_java/article/details/135757934 |
---|
这道题,需要你知道关于补码的什么呢? |
---|
- 集合中保存的都是1~n的正数,计算机保存也都是补码,也就是符号位为0表示正数
- 正数的补码和源码是一样的,例如1 =
0
,000 0000 0000 0000 0000 0000 0000 0001 (以32位进行保存)- 负数的补码和源码的区别是,
符号位和最右边的1不变
,这两个不变的二进制位中间的其余数值位
全部取反
- 原码:例如-1=
1
,000 0000 0000 0000 0000 0000 0000 0001- 补码:例如-1=
1
,111 1111 1111 1111 1111 1111 1111 1111
- 我们现在有了1和-1的补码。
- 1 =
0
,000 0000 0000 0000 0000 0000 0000 0001- -1=
1
,111 1111 1111 1111 1111 1111 1111 1111- 我们发现,除了最右边的1以外,这个1左边所有的数,都是不同的。
- 如果此时我执行1与-1 也就是 1 & (-1)
我会得到
0
,000 0000 0000 0000 0000 0000 0000 0001,也就是除了最右边的1以外,其余全是0. 这样我就得到了这个数的最低位的那个1.也就是我得到了这个数,最右边的一个二进制1的位置。并且其余二进制位全是0
- 得到它有什么用呢?作用就是简化判断条件,让我们只需要用if考虑两种情况,而不是无数种。
- 0 & 任何数都是0,只有1 & 1 才能唯一的 = 1. 这就是它的作用。对于最终得到的只有最右1,其余全为0的二进制串lowbit =
0
,000 0000 0000 0000 0000 0000 0000 0001来说,只有遇到一个同样1在最右边的数才会不为0,否则它必然为0.- 它让任何数与其相与只有两种结果,要么为1,要么为0.而不是各种值。
- 例如 8 =
0
,000 0000 0000 0000 0000 0000 0000 1000- 和lowbit相与
0
,000 0000 0000 0000 0000 0000 0000 0001- 结果为==
0
,000 0000 0000 0000 0000 0000 0000 0000
- 如果不进行只取最右边1的操作,直接随便两个数呢?
8的二进制补码为:0000 … 1000
9的二进制补码为:0000 … 1001
异或结果为:==== 0000 … 1000 这个值=8,不同的数,还有无穷多种结果
请你告诉我,我该如何写if语句,描述这大量的结果呢?我们当然希望只有0或者1两种状态,以方便我们写if语句。所以这就是只保留最右边的1,其余全部为0的作用。
4. 或操作
1. 找到二进制最左侧的1,然后将这个1右边全部填充为1。 |
---|
例如00
1
00000000这个二进制,找到最左侧的1
,然后右边全部填充为1.也就是00111111111
,我们通过找num补数的案例来讲解
- 对整数num的二进制(不操作前导0),全部取反,就是num的补数
- 例如5的二进制
0000 0000 0000 0000 0000 0000 0000 0
101,红色的0都是前导0,求补数时,是不需要取反的。- 5的补数
0000 0000 0000 0000 0000 0000 0000 0
010,只有黄色的非前导0部分才进行取反- 如果只对010操作的话,我们只需要让其每一位和1异或就可以取反,例如
101 ^ 111 = 010
- 但是计算机中5的二进制是
0000 0000 0000 0000 0000 0000 0000 0
101。如果异或1111 1111 1111 1111 1111 1111 1111 1
111,会得到1111 1111 1111 1111 1111 1111 1111 1
010,这样的话,答案就错了- 如何解决这个问题呢?如果我们能让5的二进制只异或
0000 0000 0000 0000 0000 0000 0000 0
111的话,就可以得到5的补数0000 0000 0000 0000 0000 0000 0000 0
010
所以对于一个数num =
0000 0000 0000 0000 0000 0000 0000 0
101,如何能找出只有黄色部分全1,红色部分全0的二进制串t =0000 0000 0000 0000 0000 0000 0000 0
111,就是破题的关键
而针对这个问题,有个很简单的操作方式,就是通过位移操作和或操作配合,对1,2,4,8,16,…的右移结果相或,就可以抛弃前导0,对其余位全部填充1.案例如下:下面的案例是针对32位的int型,所以只需要右移到16.如果是64位的long型,需要右移到32,依此类推。
/** 将num中非前导0的地方都填充为1 **/
//t=num: 0100 0000 0000 0000 0000 0000 0000 0101
//t>>1 0010 0000 0000 0000 0000 0000 0000 0010
//t=t|t>>1 0110 0000 0000 0000 0000 0000 0000 0111
//t>>2 0001 1000 0000 0000 0000 0000 0000 0001
//t=t|t>>2 0111 1000 0000 0000 0000 0000 0000 0111
//t>>4 0000 0111 1000 0000 0000 0000 0000 0000
//t=t|t>>4 0111 1111 1000 0000 0000 0000 0000 0111
//t>>8 0000 0000 0111 1111 1000 0000 0000 0000
//t=t|t>>8 0111 1111 1111 1111 1000 0000 0000 0111
//t>>16 0000 0000 0000 0000 0111 1111 1111 1111
//t=t|t>>16 0111 1111 1111 1111 1111 1111 1111 1111
int t = num;
t = t | (t >> 1);
t |= t >> 2;
t |= t >> 4;
t |= t >> 8;
t |= t >> 16;