算法通关村第十一关——理解位运算的规则(青铜)

算法通关村第十一关——理解位运算的规则(青铜)

    • 1. 数字在计算机中的表示
    • 2. 位运算规则
      • 2.1 与、或、异或和取反
      • 2.2 位移运算
      • 2.3 移位运算与乘除法的关系
      • 2.4 位运算常用技巧

1. 数字在计算机中的表示

原码是一个数的二进制表示形式,最高位表示符号位,0表示正数,1表示负数。

例如,+3的原码为00000011,-3的原码为10000011。

反码是对原码取反的结果,除了符号位不变,其余位都取反。

例如,+3的反码为00000011,-3的反码为11111100。

补码是对反码加1的结果。补码可以解决原码相加减时的溢出问题,并且使得减法运算转化为加法运算。

例如,+3的补码为00000011,-3的补码为11111101。

补码的形成主要是为了解决两个问题:

  1. 零的表示:在原码表示中,正数的补码与原码相同,而负数的补码则需要通过对原码取反再加1来表示。这样,在计算机中对零的表示会有两种情况,即+0和-0,导致了不一致性。而使用补码表示时,正零和负零的补码都是00000000,解决了零表示的一致性问题。
  2. 溢出的处理:在原码表示中,加法运算可能出现溢出问题,即当两个正数相加或两个负数相加得到的结果超过了所能表示的范围,导致溢出。而使用补码表示时,可以通过简单的二进制加法运算处理溢出,并且不需要额外的处理逻辑。这样,加法运算变得更加简便和高效。

通过使用补码表示,可以统一处理正数和负数的加减法运算,并且避免了溢出问题,提高了计算机处理负数的效率和准确性。同时,补码还具有唯一的表示形式,不会出现多种表示一个数的情况,避免了歧义。

假设我们使用8位二进制表示形式来表示整数。我们来看一个例子,计算 +3 - 5 的结果。

首先,将+3和-5转换为补码表示:

+3的原码为00000011,反码为00000011,补码为00000011。 -5的原码为10000101,反码为11111010,补码为11111011。

然后,进行减法运算,即将+3的补码与-5的补码相加:

00000011 + 11111011 = 100001010

注意,我们得到了一个9位的结果,超出了原来的8位范围,这是溢出的结果。在这种情况下,我们需要舍弃最高位,只保留低8位作为结果:

00001010

将结果00001010转换为十进制,得到+10。因此,+3 - 5 的结果为+10。

通过使用补码表示,我们可以简单地执行二进制加法运算,并处理溢出的问题。这样,在计算机中对负数进行加减法运算变得更加方便和高效。

2. 位运算规则

2.1 与、或、异或和取反

与运算的符号是 &,运算规则是:对于每个二进制位,当两个数对应的位都为 1 时,结果才为 1,否则结果为 0。

0 & 0=0
0 & 1=0
1 & 0=0
1 & 1=1

或运算的符号是 |,运算规则是:对于每个二进制位,当两个数对应的位都为 0 时,结果才为 0,否则结果为 1。

00=0
01=1
10=1
11=1

异或运算的符号是 ⊕(在代码中用∧ 表示异或),运算规则是:对于每个二进制位,当两个数对应的位相同时,结果为 0,否则结果为 1。

00=0
01=1
10=1
11=0

取反运算的符号是 ∼,运算规则是:对一个数的每个二进制位进行取反操作,0 变成 1,1 变成 0。

0=11=0

以下例子显示上述四种位运算符的运算结果,参与运算的数字都采用有符号的 8 位二进制表示。

46 的二进制表示是 00101110,51 的二进制表示是 00110011。考虑以下位运算的结果。

  • 46 & 51的结果是34,对应的二进制表示是 00100010。

  • 46 | 51 的结果是63,对应的二进制表示是 00111111。

  • 46 ⊕ 51 的结果是29,对应的二进制表示是 00011101。

  • ∼ 46 的结果是−47,对应的二进制表示是 11010001。

  • ∼ 51 的结果是 −52,对应的二进制表示是 11001100。

2.2 位移运算

移位运算按照移位方向分类可以分成左移和右移,按照是否带符号分类可以分成算术移位

和逻辑移位。

原始:0000 0110 6

右移一次:0000 0011 3 相当于除以2

左移一次:0000 1100 12 相当于乘以2

6*3 =>6*(2+1)=> 6*2+6*1 
66*33=>66*(32+1) 66*32+66*1

左移运算的符号是 <<,左移运算时,将全部二进制位向左移动若干位,高位丢弃,低位补 0。对于左移运算,算术移位和逻辑移位是相同的。

右移运算的符号是 >>。右移运算时,将全部二进制位向右移动若干位,低位丢弃,高位的补位由算术移位或逻辑移位决定:

算术右移时,高位补最高位;

逻辑右移时,高位补 0。

以下例子显示移位运算的运算结果,参与运算的数字都采用有符号的 8 位二进制表示。

  • 示例1:29 的二进制表示是 00011101。29左移 2 位的结果是 116,对应的二进制表示是 01110100;29 左移 3 位的结果是 −24,对应的二进制表示是 11101000。

  • 示例2:50的二进制表示是 00110010。50 右移 1 位的结果是 25,对应的二进制表示是 00011001;50 右移 2 位的结果是 12,对应的二进制表示是 00001100。对于 0和正数,算术右移和逻辑右移的结果是相同的。

  • 示例3:-50的二进制表示是 11001110(补码)。

    -50 算术右移 2 位的结果是 −13,对应的二进制表示是 11110011;

    -50 逻辑右移 2位的结果是 51,对应的二进制表示是 00110011。

2.3 移位运算与乘除法的关系

观察上面的例子可以看到,移位运算可以实现乘除操作。由于计算机底层的一切运算都是基于位运算实现的,因此使用移位运算实现乘除法的效率显著高于直接乘除法。

左移运算对应乘法运算:

将一个数左移 k 位,等价于将这个数乘以 2^k。例如,29 左移 2 位的结果是 116,等价于 29×4。

当乘数不是 2 的整数次幂时,可以将乘数拆成若干项 2 的整数次幂之和,例如,a×6 等价于 (a<<2)+(a<<1)。

对于任意整数,乘法运算都可以用左移运算实现,但需要注意溢出的情况,例如在 8 位二进制表示下,29 左移 3 位会出现溢出。

算术右移运算对应除法运算:

将一个数右移 k 位,相当于将这个数除以 2^k。例如,50 右移 2 位的结果是 12,等价于 50/4,结果向下取整。

从程序实现的角度,考虑程序中的整数除法,是否可以说,将一个数(算术)右移 k 位,和将这个数除以 2^k等价?

对于 0 和正数,上述说法是成立的,整数除法是向 0 取整,右移运算是向下取整,也是向 0 取整。但是对于负数,上述说法就不成立了,整数除法是向 0 取整,右移运算是向下取整,两者就不相同了。

例如,(−50)>>2 的结果是 −13,而 (−50)/4 的结果是 −12,两者是不相等的。因此,将一个数(算术)右移 k 位,和将这个数除以 2^k是不等价的。

算法题通常限制测试数据在正数和 0 的情况下,可以放心地使用左移或右移操作。

2.4 位运算常用技巧

位运算的性质有很多,此处列举一些常见性质,假设以下出现的变量都是有符号整数。

  • 幂等律:a &a=a,a ∣ a=a(注意异或不满足幂等律);

  • 交换律:a & b=b & a,a ∣ b=b ∣ a,a⊕b=b⊕a;

  • 结合律:(a & b) & c=a & (b & c),(a ∣ b) ∣ c=a ∣ (b ∣ c),(a⊕b)⊕c=a⊕(b⊕c);

  • 分配律:(a & b) ∣ c=(a ∣ c) & (b ∣ c),(a ∣ b) & c=(a & c) ∣ (b & c),(a⊕b) & c=(a & c)⊕(b & c);

  • 德摩根律:∼(a & b)=(∼a) ∣ (∼b),∼(a ∣ b)=(∼a) & (∼b);

  • 取反运算性质:−1=∼0,−a=∼(a−1);

  • 与运算性质:a & 0=0,a & (−1)=a,a & (∼a)=0;

  • 或运算性质:a ∣ 0=a,a ∣ (∼a)=−1;

  • 异或运算性质:a⊕0=a,a⊕a=0;

根据上面的性质,可以得到很多处理技巧,这里列举几个:

  • a & (a−1) 的结果为将 a 的二进制表示的最后一个 1 变成 0;

  • (补码)a & (−a)的结果为只保留 a 的二进制表示的最后一个 1,其余的 1 都变成 0。

处理位操作时,还有很多技巧,不要死记硬背,理解其原理对解决相关问题有很大帮助。

下面的示例中,1s和0s分别表示与x等长的一串1和一串0:

算法通关村第十一关——理解位运算的规则(青铜)_第1张图片

而如何获取、设置和更新某个位的数据,也有固定的套路。例如:

1. 获取位

该方法是将数字 1 左移 i 位,得到形如 00010000 的值。然后将这个值与 num 执行 “位与” 操作,从而将 i 位之外的所有位清零。最后我们检查结果是否为零。如果不为零,则说明第 i 位为 1;否则第 i 位为 0。

boolean getBit(int num, int i) {
    return ((num & (1 << i)) != 0);
}

2. 设置位

setBit 方法先将数字 1 左移 i 位,得到形如 00010000 的值。接着将这个值和 num 执行 “位或” 操作,这样只会改变第 i 位的数据。因为除了第 i 位之外的其他位都是零,所以不会影响 num 的其余位。

int setBit(int num, int i) {
    return num | (1 << i);
}

3. 清零位

该方法与 setBit 方法相反。首先将数字 1 左移 i 位获得形如 00010000 的值,然后对这个值进行取反操作,得到类似 11101111 的值。接着将这个值和 num 执行 “位与” 操作,从而不会影响 num 的其余位,只会将第 i 位清零。

int clearBit(int num, int i) {
    int mask = ~(1 << i);
    return num & mask;
}

4. 更新位

这个方法将 setBit 和 clearBit 合二为一。首先使用类似 11101111 的值将 num 的第 i 位清零。然后将待写入值 v 左移 i 位,得到一个只有第 i 位为 v 而其他位都为 0 的数。最后将之前的结果执行 “位或” 操作,如果 v 为 1,则 num 的第 i 位更新为 1;否则更新为 0。

int updateBit(int num, int i, int v) {
    int mask = ~(1 << i);
    return (num & mask) | (v << i);
}

以上是针对位运算中获取、设置、清零和更新位的几种常见操作的示例代码。这些方法在处理位级别的操作上非常有用,可以方便地对数字的某一位进行操作和修改。

你可能感兴趣的:(数据结构,算法,算法,笔记,java,数据结构)