java数据结构与算法基础-----位运算-----持续补充

本文详细介绍了在Java数据结构与算法刷题过程中常遇到的位运算知识点,包括异或、奇偶性判断、加法实现、补码的理解、与和或操作的应用。通过实例解析了这些概念在实际问题中的应用,如LeetCode题目中的位运算解法。
摘要由CSDN通过智能技术生成
java数据结构与算法刷题目录(剑指Offer、LeetCode、ACM)-----主目录-----持续更新(进不去说明我没写完):https://blog.csdn.net/grd_java/article/details/123063846

刷题过程中,用到什么关于位运算的知识点,就补充什么知识点到这里

一、基本位运算

1. 异或相关

异或
  1. 两数相同异或为0,两数不相同异或为1
  2. 任何数a异或0,都等于a本身。0 ⊕ a = a
  3. 两个相同的数异或必然为0。a ⊕ a = 0;
  4. 异或具有结合律和交换律。
  1. 结合律0⊕1⊕2⊕2 = (0⊕1)⊕(2⊕2) = 1 ⊕ 0 = 1;
  2. 交换律0⊕1⊕2⊕2 = 2⊕1⊕2⊕0
2. 奇偶异或1 = 加一或减一
  1. 如果mid是偶数,mid +1 操作相当于 mid ^ 1

例如2是偶数,二进制为0010,异或1(二进制0001)结果为0011.

  1. 如果mid是奇数,mid - 1 操作相当于 mid ^ 1

例如1是奇数,二进制为0001,异或1(二进制0001)结果为0000.

实现加法
  1. 我们不能使用加减运算符。那么就只能学习硬件底层的加法器的逻辑来处理了(计算机组成原理中加法器的知识)
  2. 首先想要实现一位加法器,必须知道如何处理进位信息,例如1+1 = 进位1和本位0
  3. 也就是说,整个加法器都是由两个操作来处理,无进位加法结果,和进位后结果
  1. 无进位加法结果实现方法:a异或b。因为我们要实现本位1+0 = 0+1 = 1和0+0 = 1+1 = 0.

注意,这里加法是抛去进位信息的。例如1+1 = 10,其中1是进位,而0是本位,我们这里只要本位信息

  1. 进位结果实现方法:(a & b) << 1. 因为我们要实现进位1+0 = 0+1 = 0和0+0 = 0,和1+1 = 1

1+1 = 1的原因是 1+1 = 10.也就是逢二进一,所以进位为1.

不容易理解的是进位结果为什么是(a & b) << 1

  1. 假设a = 0111,b = 0101.此时a&b = 0101,左移一位为1010
  2. 我们发现右移后的结果,里面的1正好是进位后的1需要去的地方
  3. 因为a+b = 0111 + 0101,其中标红的位置两个1相加必然会产生进位
  4. 往哪里进位?如果允许2的出现的话,结果会是这样a+b = 0212
  5. 但是很遗憾,不允许2的出现,所以他俩得向前进位1,也就是0212 = 0+101+10.也就是2需要逢二进一,进位到它们的高位
  6. 因此a & b只能获取哪些地方是加出2的,这些2需要向高位进1。例如a&b = 0101 正好对应a+b = 0212中需要进位的2的位置
  7. 因此我们需要左移一位,获取0212 = 0+101+10中0+11+1的进1的位置
  1. 例如a = 1,b = 2. 二进制分别为a = 0001,b = 0010
  2. 此时不考虑进位,相加结果为a^b = 0011,我们发现压根也不会产生进位信息
  1. 例如a = 2,b = 3,二进制分别为a = 0010,b = 0011
  2. 此时a ^ b = 0001,也就是不进位加法结果
  3. 此时获取需要进位的位置(相加为2的位置),a & b = 0010,其中1的位置,正好是a+b后二进制为按位相加 = 2的位置,这些位置需要进位1
  4. 因此通过左移操作,获取需要进位的1进位到哪里,(a&b)<<1 = 0100
  5. 此时将本位结果x = a^b = 0001和进位相加结果y = (a&b)<<1 = 0100,不进位相加x ^ y得到x = 0101 = 5.
  6. 继续获取进位信息(x&y)<<1 = 0000.发现没有需要进位的了,则加法完成。

2. 与操作

1.
  1. 两个数都是1,相与为1. 1&1=1
  2. 两个数有一个是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
  1. 对于100000000001.虽然我们一眼看到只有两个1,但是笨办法依然需要将所有的位数都处理一遍
  2. 我们如何跳过中间的0,直接统计1的个数呢?让上面这一串,只循环右移2次
  3. 针对这个问题,BK算法出现了,他利用了二进制的特性,实现一次性删除最右侧的1的效果
    在这里插入图片描述
  4. 对于十进制来说我们做减法时,低位不够减需要向高位借1,拿过来就是10
  5. 二进制也一样,不够减就得借1.拿过来就是2
  6. 也就是说,如果对2进制串进行-1操作的话,最低位是1还好,可以直接减去,如果不够减,就必须一直向高位借,直到遇到一个够借1的。这样就会将2进制串中,最后一个1借掉
  7. 例如上图中,x = 10001000,-1后最后一个1被借了,变成10000111
  8. 此时将这两个二进制串进行与运算,就会实现将最后一个1去掉的效果
4. 只保留末尾的1
  1. 正数的补码和源码是一样的,例如1 = 0,000 0000 0000 0000 0000 0000 0000 0001 (以32位进行保存)
  2. 负数的补码和源码的区别是,符号位和最右边的1不变,这两个不变的二进制位中间的其余数值位全部取反
  1. 原码:例如-1= 1,000 0000 0000 0000 0000 0000 0000 0001
  2. 补码:例如-1= 1,111 1111 1111 1111 1111 1111 1111 1111
  1. 我们现在有了1和-1的补码。
  1. 1 = 0,000 0000 0000 0000 0000 0000 0000 0001
  2. -1= 1,111 1111 1111 1111 1111 1111 1111 1111
  3. 我们发现,除了最右边的1以外,这个1左边所有的数,都是不同的。
  1. 如果此时我执行1与-1 也就是 1 & (-1)

我会得到0,000 0000 0000 0000 0000 0000 0000 0001,也就是除了最右边的1以外,其余全是0. 这样我就得到了这个数的最低位的那个1.也就是我得到了这个数,最右边的一个二进制1的位置。并且其余二进制位全是0

5.只提取奇数位,或偶数位
  1. 我们将奇数位置标识出来:(10101010101010101010101010101010)二进制中,只对奇数位置操作
  2. 当然如果转换为16进制,它会比较好看一点:其16进制形式为0xaaaaaaaa
  3. 如果n&0xaaaaaaaa ==0,就说明n的二进制中的1,只出现在偶数位置上。因为如果出现在奇数位置上,两个二进制为都是1,与操作过后的结果一定也是1.
  4. 当然我们可以标识偶数位置(01010101010101010101010101010101) = 0x55555555
  5. 如果n& 0x55555555 = n的话,就说明n中1的位置正好在偶数位置上

3. 补码

补码

  1. C,C++,java等编程语言中,为了更好的和硬件交互,数字以补码形式存储。
  2. 各种码的转换关系如下,了解即可,我们只需要统一用补码进行计算即可。(看不懂没关系,继续看下面)
    在这里插入图片描述
    在这里插入图片描述
补码(了解即可)
  1. 真值:我们通过除基取余法得到的二进制代码,统一称为真值。例如十进制数8的二进制位1000,这个1000就是一个真值。
  2. 原码:那么如何区分真值是正数还是负数呢?我们只需要用掉开头的一个二进制位,0表示正数,1表示负数。例如8的原码就是0000 … 1000 标红的那位就是符号为,剩下的都是数值位. -8的原码就是1000 … 1000负数的符号位为1.
  3. 补码,方便计算机运算的一种码,它不方便人类理解,但是方便计算机。它可以通过原码来推导
  1. 正数的补码 = 原码
  2. 负数的补码 = 符号位不变,其余位取反,然后末位+1。当然我们有一个口诀,就是从右向左找到第一个1,然后将符号位和这个1之间所有元素按位取反即可(图解如下)
    在这里插入图片描述
不妨做道题:🏆LeetCode645. 错误的集合(位运算解法需要重点掌握)https://blog.csdn.net/grd_java/article/details/135757934
这道题,需要你知道关于补码的什么呢?
  1. 集合中保存的都是1~n的正数,计算机保存也都是补码,也就是符号位为0表示正数
  2. 正数的补码和源码是一样的,例如1 = 0,000 0000 0000 0000 0000 0000 0000 0001 (以32位进行保存)
  3. 负数的补码和源码的区别是,符号位和最右边的1不变,这两个不变的二进制位中间的其余数值位全部取反
  1. 原码:例如-1= 1,000 0000 0000 0000 0000 0000 0000 0001
  2. 补码:例如-1= 1,111 1111 1111 1111 1111 1111 1111 1111
  1. 我们现在有了1和-1的补码。
  1. 1 = 0,000 0000 0000 0000 0000 0000 0000 0001
  2. -1= 1,111 1111 1111 1111 1111 1111 1111 1111
  3. 我们发现,除了最右边的1以外,这个1左边所有的数,都是不同的。
  1. 如果此时我执行1与-1 也就是 1 & (-1)

我会得到0,000 0000 0000 0000 0000 0000 0000 0001,也就是除了最右边的1以外,其余全是0. 这样我就得到了这个数的最低位的那个1.也就是我得到了这个数,最右边的一个二进制1的位置。并且其余二进制位全是0

  1. 得到它有什么用呢?作用就是简化判断条件,让我们只需要用if考虑两种情况,而不是无数种。
  1. 0 & 任何数都是0,只有1 & 1 才能唯一的 = 1. 这就是它的作用。对于最终得到的只有最右1,其余全为0的二进制串lowbit = 0,000 0000 0000 0000 0000 0000 0000 0001来说,只有遇到一个同样1在最右边的数才会不为0,否则它必然为0.
  2. 它让任何数与其相与只有两种结果,要么为1,要么为0.而不是各种值。
  1. 例如 8 = 0,000 0000 0000 0000 0000 0000 0000 1000
  2. 和lowbit相与0,000 0000 0000 0000 0000 0000 0000 0001
  3. 结果为==0,000 0000 0000 0000 0000 0000 0000 0000
  1. 如果不进行只取最右边1的操作,直接随便两个数呢?

8的二进制补码为:0000 … 1000
9的二进制补码为:0000 … 1001
异或结果为:==== 0000 … 1000 这个值=8,不同的数,还有无穷多种结果
请你告诉我,我该如何写if语句,描述这大量的结果呢?我们当然希望只有0或者1两种状态,以方便我们写if语句。所以这就是只保留最右边的1,其余全部为0的作用。

4. 或操作

1. 找到二进制最左侧的1,然后将这个1右边全部填充为1。

例如00100000000这个二进制,找到最左侧的1,然后右边全部填充为1.也就是00111111111,我们通过找num补数的案例来讲解

  1. 对整数num的二进制(不操作前导0),全部取反,就是num的补数
  2. 例如5的二进制0000 0000 0000 0000 0000 0000 0000 0101,红色的0都是前导0,求补数时,是不需要取反的。
  3. 5的补数0000 0000 0000 0000 0000 0000 0000 0010,只有黄色的非前导0部分才进行取反
  4. 如果只对010操作的话,我们只需要让其每一位和1异或就可以取反,例如101 ^ 111 = 010
  5. 但是计算机中5的二进制是0000 0000 0000 0000 0000 0000 0000 0101。如果异或1111 1111 1111 1111 1111 1111 1111 1111,会得到1111 1111 1111 1111 1111 1111 1111 1010,这样的话,答案就错了
  6. 如何解决这个问题呢?如果我们能让5的二进制只异或0000 0000 0000 0000 0000 0000 0000 0111的话,就可以得到5的补数0000 0000 0000 0000 0000 0000 0000 0010

所以对于一个数num = 0000 0000 0000 0000 0000 0000 0000 0101,如何能找出只有黄色部分全1,红色部分全0的二进制串t = 0000 0000 0000 0000 0000 0000 0000 0111,就是破题的关键

而针对这个问题,有个很简单的操作方式,就是通过位移操作和或操作配合,对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;
  • 17
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殷丿grd_志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值