JS中的【位操作符】浅识

前言


为什么会突然想写关于这个方面的知识点呢?一切源于在刷 leetcode 时候有道题需要用的相关知识点,最近在重温高程的时候刚好也看到这一节,就想着记录下。因为位操作这块知识点确实有很人去注意到。下面给上题目直达链接

直达链接:Sum of Two Integers 可以在看文章之前试着能不能解答这道题目

位操作符


1. 基础概念

二进制数(binaries)是逢2进位的进位制,0、1是基本算符;计算机运算基础采用二进制。电脑的基础是二进制。在早期设计的常用的进制主要是十进制(因为我们有十个手指,所以十进制是比较合理的选择,用手指可以表示十个数字,0的概念直到很久以后才出现,所以是1-10而不是0-9)。电子计算机出现以后,使用电子管来表示十种状态过于复杂,所以所有的电子计算机中只有两种基本的状态,开和关。也就是说,电子管的两种状态决定了以电子管为基础的电子计算机采用二进制来表示数字和数据。

chrome 中,当我们在控制台输入 8+7 的时候会很轻易的得到 15,可能在现实生活中我们可以通过掰手指头或者其他方式计算出来。但对于计算机来说,它只有 01 ,那么计算机是如何计算出来这个数的呢?

根据《高程》第三章所述,ECMAScript中的所有数值都以 IEEE-754 64位格式存储,但位操作符并不是直接操作64位,而是先将64位转成32位的整数,再执行操作,最后将结果转回64位。

对于有符号的整数,32位中的前21位用于表示整数的值。第32位表示该数值的符号:0表示正数 1表示负数。即 符号位 。正数以纯二进制格式存储。例如数值 18 的二进制表示为:

0000 0000 0000 0000 0000 0000 0001 0010 或者更为简洁的 10010

nEyhrR.png

那么对于负数,同样是以二进制码存储,但使用的格式是 二进制补码,需经过下面3个步骤,以-18的二进制补码为例子:

  1. 求这个数值绝对值的二进制码,即 18 的二进制码
    0000 0000 0000 0000 0000 0000 0001 0010

  2. 然后求其二进制反码,即0和1互换
    1111 1111 1111 1111 1111 1111 1110 1101

  3. 最后二进制反码加1

    1111 1111 1111 1111 1111 1111 1110 1101

    ......................................1

    -——————————————

    1111 1111 1111 1111 1111 1111 1110 1110

这样可以得到 -18 的二进制表示。

但是,ECMAScript不会这样展现一个负数的二进制码,在以一个二进制字符串形式输出一个负数的时候,我们看到的只是这个负数绝对值的二进制码前面加上了一个负号,这样似乎更符合我们平常逻辑

nE6eZq.png

2. 操作符号 - 按位非(NOT)

~ 表示,按位非的结果就是返回数值的反码

1
2
3
var num1 = 25 // 00000000000000000000000000011001
var num2 = -num1 // 11111111111111111111111111100110
console.log(num2) // -26

按位非操作的本质:操作数的负值减1

3. 操作符号 - 按位与(AND)

& 表示,同时为1时候才返回1,任何一位是0,结果都是0,真值表如下:

第一个数 第二个数 结果
1 1 1
1 0 0
0 1 0
0 0 0

4. 操作符号 - 按位或(OR)

| 表示,只要有一个为1,就返回1,只有在两个为0的情况才返回0,真值表如下:

第一个数 第二个数 结果
1 1 1
1 0 1
0 1 1
0 0 0

5. 操作符号 - 按异或(XOR)

^ 表示,两个数值对应位只有一个1时才返回1,如果两位都是1或者都是0,则返回0,真值表如下:

第一个数 第二个数 结果
1 1 0
1 0 1
0 1 1
0 0 0

6. 操作符号 - 左移

<< 表示,该操作会将数值的所有位向左移动指定的位数。

1
2
var oldValue = 2; // 二进制 10
var newValue = oldValue << 5; // 左移5位 10 00000 十进制位64

向左移动,就是原来的数值后面多出5个空位并以0来填充。

7. 操作符号 - 有符号的右移

>> 表示,会将数值向右移动,保留符号位

1
2
var oldValue = 64; // 二进制 1000000
var newValue = oldValue >> 5; // 右移5位 10 十进制位2

8. 操作符号 - 无符号的右移

>>> 表示,和有符号右移不同的是,在负数的情况下,会把负数的二进制码当成正数的二进制码。由于负数以其绝对值的二进制补码形式表示,也就是数值中 1 会非常多,所以当无符号右移动时候变成正数,就会导致结果非常大

回到题目上


回到 leetcode 题目上,题意是要不使用 +- 实现两个整数之和,既然不能使用加减符号,那就只能用底层位操作符来计算,我们先用 二进制 来算两个数的相加,这里以 7+6 为例子:

nE6WY8.png

1101 转换成十进制就是 13 了。在这个过程我们看到主要处理了两个地方,一种是 1+0 ,这种比较简单,结果就是 1 , 另一种就是 1+1,由于产生了进位,在 不考虑进位 的情况下结果是 0 ,看到这里是不是觉得很熟悉。没错,这就是 异或 ^ 的真值表结果。

目前我们完成了在不考虑进位的情况下两数的相加,现在我们再来观察下进位应该怎么解决。观察上面图片,不难发现产生进位的情况就是在 1+1 的式子下,其他情况都不会产生进位。那我们要保存进位,加回原来异或结果的数值上。回过头想,这不就是 按位与 & 的真值表结果。

理解这两步,就不难编写代码来实现两个数的相加,下面还是用 7+6 为例子来加深理解

nE6vpF.png

nEcmXd.png

上述结果为 1101 即十进制位 13。其实过程总结就是以下3个步骤:

  1. a^b (异或,两数相加不进位)
  2. (a&b)<<1 (相与保存进位并向左移一位)
  3. 判断进位是否为0,不是的话将两个新的数从第一步开始继续相加操作,知道进位为0

附上题目解答答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @param {number} a
* @param {number} b
* @return {number}
*/
var getSum = function(a, b) {
let sum = a^b
let carry = (a&b)<<1
if (carry !== 0) {
return getSum(sum, carry)
}
return sum
};

小结


其实,位操作在实际过程中使用的较少,我刚好也是看到这道题目比较感兴趣,才简单去了解下,对位操作也有了基本认识。文章有什么问题还请各位看官指出。