前端算法 - 位运算

210 阅读4分钟

什么是位

介绍位之前,要先介绍一下什么是 二进制

我们一般数学中用到的都是十进制 ,当碰见9 的时候,再往前大1,那么就要进位,变为10

那么 二进制 也是同样的道理,只有0 / 1两种状态,遇到1 的时候,再往前大1,就要进位

📝 为什么 计算机 采用 二进制

  1. 计算机是由逻辑电路组成,逻辑电路通常只有两个状态,开关的接通与断开,这两种状态正好可以用“1”和“0”表示

  2. 抗干扰能力强,可靠性高:二进制中只使用0和1两个数字,传输和处理时不易出错,因而可以保障计算机具有很高的可靠性

  3. 与十进制数相比,二进制数的运算规则要简单得多,这不仅可以使运算器的结构得到简化,而且有利于提高运算速度。

计算机对二进行的运算( |、&、^、<< 、 >> ) 都是叫位运算

优势就是处理数据速度很快, 缺点是对于程序员来说有点不那么直观

🚀 位运算符

位运算符之前,先介绍一下 js自带的 二进制 / 十进制 的转换方法

十进制转二进制

var num = 100;
num.toString(2) // '1100100'

二进制转十进制

var num = 1100100;
parseInt(num,2);

image.png

1. << (按位左移)

左移第一个操作数 向 左移动指定位数,左边超出的位数将会被清除,右边将会补零。

9 (十进制): 001001 (二进制)
 --------------------------------
9 << 2 (十进制): 100100 (二进制) = 36 (十进制)

// 左移 2位
1 (十进制): 000001 (二进制)
 --------------------------------
1 << 2 (十进制): 000100 (二进制) = 4 (十进制)

// 左移 3位
1 (十进制): 000001 (二进制)
 --------------------------------
1 << 3 (十进制): 001000 (二进制) = 8 (十进制)

可以看出9换算成 二进制001001
根据定义 ==== 第一个1 移动 2位, 001001 ==> 100100

同样的右移(>>)就是向右移动指定位数

2. | (按位或)

只要有一个为 1,则为 1

举例

5 | 1 = 5

5表示为 二进制0101 1表示为 二进制0001

5:  0 1 0 1
1:  0 0 0 1
———————————
5:  0 1 0 1

3. & (按位与)

只要两个同时为 1,则为 1

举例

5 & 1 = 1
5 & 2 = 0
5:  0 1 0 1
1:  0 0 0 1
———————————
1:  0 0 0 1


5:  0 1 0 1
2:  0 0 1 0
———————————
0:  0 0 0 0

4. ^ (按位异或)

按位异或 两个操作数有且仅有一个对应的二进制位为 1 时,该位的结果值为 1

举例

5 & 1 = 4
5 & 2 = 7
5:  0 1 0 1
1:  0 0 0 1
———————————
4:  0 1 0 0


5:  0 1 0 1
2:  0 0 1 0
———————————
7: 0 1 1 1

说了这么多,到底应该怎么在实际场景中使用呢?

🥁 实际使用

vue

 enum ShapeFlags {
  ELEMENT = 1, // 虚拟节点是一个元素
  FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件
  STATEFUL_COMPONENT = 1 << 2, // 普通组件
  TEXT_CHILDREN = 1 << 3, // 儿子是文本的
  ARRAY_CHILDREN = 1 << 4, // 儿子是数组
  SLOTS_CHILDREN = 1 << 5, // 插槽
  TELEPORT = 1 << 6,
  SUSPENSE = 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9,
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT,
 }

vue 中使用 左移来标注 element 的类型,可以发现有个特点 就都是 1的左移操作

为什么这样设计呢 ?

其实这样有个显著的优点是: 换成2进制只有一位有 1

1 = 000001
1 << 2 = 000100 (二进制) = 4 (十进制)
1 << 3 = 001000 (二进制) = 8 (十进制)
1 << 4 = 010000 (二进制) = 16 (十进制)

那么只有只有 1 个 1 有什么好处呢?🧐

好处 是便于 判断 / 扩大

怎么判断呢?🧐
COMPONENT 举例说明

ShapeFlags.STATEFUL_COMPONENT = 1 << 2 = 00100
ShapeFlags.FUNCTIONAL_COMPONENT = 1 << 1 = 00010

COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT,

执行按位或 (在 | 按位或中,只要其中 一个位 是 1 ,则为1)

1 << 2 ==> 4:  0 1 0 0
1 << 1 ==> 2:  0 0 1 0
——————————————————————
           6:  0 1 1 0

最终COMPONENT二进制结果是0 1 1 0

怎么使用COMPONENT进行判断呢?🧐

使用 &(按位与) 进行判断是否包含 (按位与 必须是 两位都为1,才能为 1)

COMPONENT 是否包含FUNCTIONAL_COMPONENTTEXT_CHILDREN举例说明

console.log(ShapeFlags.COMPONENT & ShapeFlags.FUNCTIONAL_COMPONENT); // 2
console.log(ShapeFlags.COMPONENT & ShapeFlags.TEXT_CHILDREN); // 0
  1. ShapeFlags.COMPONENT & ShapeFlags.FUNCTIONAL_COMPONENT
 6:  0 1 1 0
 2:  0 0 1 0
——————————————————————
 2:  0 0 1 0
  1. ShapeFlags.COMPONENT & ShapeFlags.TEXT_CHILDREN
 6:  0 1 1 0
 8:  1 0 0 0
——————————————————————
 0:  0 0 0 0

只要判断 按位与之后 的结果 是否> 0 ,就可以判断 是否包含对应的elemnetType

✈️ 总结: 按位 或 扩大查找范围, 按位与 进行判断是否包含

leetCode

只出现一次的数字

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间

示例:

示例 1:
输入:nums = [2,2,1]
输出:1

示例 2 :
输入:nums = [4,1,2,1,2]
输出:4

示例 3 :
输入:nums = [1]
输出:1

这道题有很多种解法,第一时间想到的应该是 遍历,形成一个[{value:对应 value出现的次数}]的这种结构
但是题目中有一句很重要某个元素只出现一次以外,其余每个元素均出现两次, 所以有更简单的算法,那就是使用异或操作符

异或是 只要有操作位中 有且只能有1位 是 1,才为 1

举例 [6,8,6]

6:  0 1 1 0
8:  1 0 0 0
———————————————
14: 1 1 1 0
6 : 0 1 1 0
———————————————
8:  1 0 0 0 

这样把 数字8 给筛选出来了

var singleNumber = function(nums) { 
    let num = nums[0];
    for(let i = 1;i<nums.length;i++){ 
        num = num ^ nums[i] } return num 
};

❤️ 总结

位运算其实在前端用的不算太多,但是由于是操作的二进制数据,所以有一些独特的优势,在权限管理方面,可以参考Vue的这种做法,不使用字符串 admin或者是 normal来进行判断,使用二进制

不过还是那句话,只要项目和人能跑一个就行, 如果你对自己的代码有更高的要求的话,还是要多学习一些最佳实践🎉