10个互动关卡带你轻松掌握js的位运算

关卡来自于我写的一个位运算平台,一共10个关卡,提供中英文切换。

第一关:基础

知识点

在计算机中,所有数据都以二进制形式存储。二进制只有0和1两个数字,分别代表关(off)和开(on)。

十进制数字转换为二进制的方法是不断除以2,记录余数,然后从下往上读取余数。例如:

ini 复制代码
十进制10转二进制:
10 ÷ 2 = 5 余 0
5 ÷ 2 = 2 余 1
2 ÷ 2 = 1 余 0
1 ÷ 2 = 0 余 1
从下往上读:1010

在JavaScript中,整数以32位二进制形式存储,最高位是符号位(0表示正数,1表示负数)。

可以使用toString(2)方法将十进制数转换为二进制字符串:

js 复制代码
const num = 10;
console.log(num.toString(2)); // 输出: "1010"

也可以使用parseInt(str, 2)将二进制字符串转换为十进制数:

js 复制代码
const binary = "1010";
console.log(parseInt(binary, 2)); // 输出: 10

挑战

编写一个函数,接收一个十进制整数作为参数,返回它的二进制表示形式(字符串)。不要使用内置的toString方法。

js 复制代码
function decimalToBinary(num) {
    // 在这里实现代码
}

关卡解答

根据前面的知识点,我们知道想要将十进制转换成二进制,那就需要使用求余数法,即可以使用除以2取余数的方法,将余数存入数组,最后反转数组并连接成字符串。

js 复制代码
function decimalToBinary(num) {
  // 注意边界值0
  if (+num === 0) return "0";
  
  // 使用数组存储余数
  let binary = [];
  // 可能存在负数,转换成正整数
  let temp = Math.abs(num);
  
  // 需要取余数直到该数为0
  while (temp > 0) {
    // 添加余数
    binary.push(temp % 2);
    // 取完就要向下取整除以2
    temp = Math.floor(temp / 2);
  }
  // 反转数组结果,并转换成字符串
  return binary.reverse().join("");
}

第二关:按位与运算符 (&)

知识点

按位与运算符对两个操作数的每一位执行与操作。只有当两个相应的位都为1时,结果才为1,否则为0

按位与可以用来检查一个数的特定位是否为1,或者将某些位清零。

scss 复制代码
示例:5 & 3
5 的二进制:101
3 的二进制:011
结果:       001 (十进制为1)

常见用途:

  • 检查一个数是奇数还是偶数:num & 1如果结果为1,则为奇数;如果为0,则为偶数。
  • 清除特定位:将想要保留的位设为1,其他位设为0,然后与原数进行按位与操作。
  • 判断两个数的符号是否相同:(a ^ b) >= 0

挑战

编写一个函数,判断一个数是否是2的幂(即2的n次方)。提示:2的幂在二进制中只有一个1。

js 复制代码
function isPowerOfTwo(n) {
    // 在这里实现代码
}

解答

如果一个数是2的幂,那么它的二进制表示中只有一个1。因此可以利用 n & (n-1) 的结果来判断。

js 复制代码
function isPowerOfTwo(n) {
    // 结果为0,即为2的幂
    return (n & (n - 1)) === 0;
}

第三关: 按位或运算符 (|)

知识点

按位或运算符对两个操作数的每一位执行或操作。如果两个相应的位中至少有一个为1,则结果为1,否则为0

按位或可以用来将某些位设置为1,同时保留其他位不变。

scss 复制代码
示例:5 | 3
5 的二进制:101
3 的二进制:011
结果:       111 (十进制为7)

常见用途:

  • 设置特定位:将想要设置的位设为1,其他位设为0,然后与原数进行按位或操作。
  • 合并标志位:在使用位掩码表示多个布尔值时,可以使用按位或来添加标志。

挑战

编写一个函数,接收两个整数作为参数,返回将第一个整数的第n位(从右往左,从0开始计数)设置为1后的结果。

js 复制代码
function setBit(num, n) {
    // 在这里实现代码
}

解答

我们可以使用按位或运算符和左移运算符。创建一个只有第n位为1的掩码,然后与原数进行按位或操作。

js 复制代码
function setBit(num, n) {
  // 使用左移运算符创建一个第n位为1的掩码,即1左移n位,然后与num执行按位或操作,左移运算符后面会提到
  const mask = 1 << n;
  return num | mask;
}

第四关:按位异或运算符 (^)

知识点

按位异或运算符对两个操作数的每一位执行异或操作。如果两个相应的位不同,则结果为1,如果相同则为0。

按位异或有一些有趣的性质,例如:

scss 复制代码
示例:5 ^ 3
5 的二进制:101
3 的二进制:011
结果:       110 (十进制为6)

常见用途:

  • 切换位的值:对一个数的特定位进行异或操作,可以切换该位的值(0变1,1变0)。
  • 不使用临时变量交换两个变量的值:a = a ^ b; b = a ^ b; a = a ^ b;
  • 查找数组中只出现一次的数:如果一个数组中除了一个数字只出现一次外,其他数字都出现两次,那么对所有数字进行异或操作,结果就是那个只出现一次的数字。

挑战

编写一个函数,接收一个整数数组,其中除了一个数字只出现一次外,其他数字都出现了两次。找出并返回那个只出现一次的数字。

js 复制代码
function findSingleNumber(nums) {
    // 在这里实现代码
}

解答

利用异或运算的性质:a ^ a = 0a ^ 0 = a。对数组中所有元素进行异或操作,最终结果就是只出现一次的数字。

js 复制代码
function findSingleNumber(nums) {
    // 结果值
    let r = 0;
    for(const n of nums){
        // 执行按位异或
        r ^= n;
    }
    // 最终结果就是那个只出现1次的数字
    return r;
}

本题还可以衍生出,不过这不在本文的范畴,感兴趣的可以查看相关资料。

第五关:按位非运算符 (~)

知识点

按位非运算符对操作数的每一位执行非操作,即将0变为1,将1变为0。这相当于对数字取反再减1。

在JavaScript中,数字以32位二进制形式存储,按位非运算会影响所有这32位。

scss 复制代码
示例:~5
5 的二进制(32位):00000000000000000000000000000101
~5 的结果:          11111111111111111111111111111010 (十进制为-6)

为什么5等于-6?这是因为JavaScript使用二进制补码表示负数。对一个数取反再加1,就得到它的相反数。所以n = -(n+1)。

常见用途:

  • 快速取整:~~3.14等于3(相当于Math.floor对正数的效果)
  • 检查数组中是否存在某个元素:if(~arr.indexOf(item))if(arr.indexOf(item) !== -1)更简洁

挑战

编写一个函数,接收一个整数作为参数,返回将该整数所有位取反后的结果。

js 复制代码
function bitwiseNOT(num) {
    // 在这里实现代码
}

解答

本题其实就是考验按位非运算符的作用,因此直接使用按位非运算符~,但要注意JavaScript中的数字表示方式。

js 复制代码
function bitwiseNOT(num) {
  return ~num;
}

第六关:左移运算符 (<<)

知识点

左移运算符将操作数的所有位向左移动指定的位数,右侧补0。这相当于将数字乘以2的n次方。

bash 复制代码
示例:5 << 1
5 的二进制:101
左移1位后:  1010 (十进制为10)

示例:5 << 2
5 的二进制:101
左移2位后:  10100 (十进制为20)

左移n位相当于乘以2的n次方:x << n等价于x * Math.pow(2, n),但位运算通常更高效。

常见用途:

  • 快速乘法:当需要将一个数乘以2的幂时,使用左移运算更高效。
  • 创建掩码:生成特定位模式的掩码。

挑战

编写一个函数,计算2的n次方(n为非负整数)。使用左移运算符实现,不要使用Math.pow()。

js 复制代码
function powerOfTwo(n) {
    // 在这里实现代码
}

解答

我们可以使用左移运算符,1 << n 相当于 2^n。

js 复制代码
function powerOfTwo(n) {
    return 1 << n;
}

第七关:右移运算符 (>>)

知识点

右移运算符将操作数的所有位向右移动指定的位数,丢弃移除的位。对于正数,左侧补0;对于负数,左侧补1(保持符号位)。

这相当于将数字除以2的n次方并向下取整。

scss 复制代码
示例:10 >> 1
10 的二进制:1010
右移1位后:  0101 (十进制为5)

示例:-10 >> 1
-10 的二进制(简化表示):...1111111111110110
右移1位后:              ...1111111111111011 (十进制为-5)

右移n位相当于除以2的n次方并向下取整:x >> n近似等价于Math.floor(x / Math.pow(2, n))

常见用途:

  • 快速除法:当需要将一个数除以2的幂时,使用右移运算更高效。
  • 保持符号的整数除法。

挑战

编写一个函数,接收一个整数作为参数,返回将该整数除以2并向下取整的结果。使用右移运算符实现。

js 复制代码
function divideByTwo(num) {
    // 在这里实现代码
}

解答

根据知识点可以得知右移1位运算符 >> 可以实现除以2并向下取整的效果。

js 复制代码
function divideByTwo(num) {
    return num >> 1;
}

第八关:无符号右移运算符 (>>>)

知识点

无符号右移运算符将操作数的所有位向右移动指定的位数,丢弃移除的位,左侧始终补0,不考虑符号位。

这与有符号右移(>>)的区别在于,无论操作数是正数还是负数,左侧都补0。

scss 复制代码
示例:10 >>> 1
10 的二进制:00000000000000000000000000001010
右移1位后:  00000000000000000000000000000101 (十进制为5)

示例:-10 >>> 1
-10 的二进制:11111111111111111111111111110110
右移1位后:   01111111111111111111111111111011 (十进制为2147483643)

对于正数,>>> 和 >> 的结果相同。但对于负数,结果会非常不同,因为符号位也参与了移位。

常见用途:

  • 处理无符号整数:在需要将负数视为大的无符号数时使用。
  • 实现无符号除法。

挑战

编写一个函数,接收一个整数作为参数,返回将该整数的所有位向右移动1位,左侧补0的结果。

js 复制代码
function unsignedRightShift(num) {
    // 在这里实现代码
}

解答

使用无符号右移运算符 >>> 可以实现向右移动并左侧补0的效果。

js 复制代码
function unsignedRightShift(num) {
    return num >>> 1;
}

第九关:位掩码

知识点

位掩码是使用二进制位来存储多个布尔值(标志)的技术。每一位代表一个标志,可以通过位运算来设置、清除、切换或检查这些标志。

位掩码在需要存储多个开关状态但又想节省内存时非常有用。例如,在权限系统、图形处理和游戏开发中经常使用。

常用的位掩码操作:

  • 设置标志:flags |= mask
  • 清除标志:flags &= ~mask
  • 切换标志:flags ^= mask
  • 检查标志:(flags & mask) !== 0
js 复制代码
例如,我们可以用一个整数的不同位来表示用户的权限:

const READ = 1;          // 0001
const WRITE = 2;         // 0010
const EXECUTE = 4;       // 0100
const ADMIN = 8;         // 1000

// 授予用户读和写权限
let userPermissions = READ | WRITE;  // 0011 (3)

// 检查用户是否有读权限
if (userPermissions & READ) {
  console.log("用户有读权限");
}

// 添加执行权限
userPermissions |= EXECUTE;  // 0111 (7)

// 移除写权限
userPermissions &= ~WRITE;   // 0101 (5)

挑战

实现一个简单的权限系统。编写一个函数,接收用户当前权限和要检查的权限作为参数,返回用户是否拥有该权限。

js 复制代码
function hasPermission(userPermissions, permissionToCheck) {
    // 在这里实现代码
}

解答

使用按位与运算符检查特定权限位是否设置。如果 (userPermissions & permissionToCheck) !== 0,则用户拥有该权限。

js 复制代码
function hasPermission(userPermissions, permissionToCheck) {
    return (userPermissions & permissionToCheck) !== 0;
}

第十关:位运算技巧

知识点

位运算可以用来实现许多常见操作,通常比传统方法更高效。以下是一些常用的位运算技巧:

常用技巧:

  • 判断奇偶性:n & 1 === 0(偶数),n & 1 === 1(奇数)
  • 交换两个变量的值(不使用临时变量):a ^= b; b ^= a; a ^= b;
  • 取绝对值(仅适用于32位整数):const abs = (n ^ (n >> 31)) - (n >> 31);
  • 取整(向下取整,仅适用于正数):const floor = n >> 0;
  • 计算2的幂:const pow2 = 1 << n(计算2的n次方)
  • 乘以/除以2的幂:n << m(n乘以2的m次方),n >> m(n除以2的m次方,向下取整)
  • 检查一个数是否是2的幂:n & (n - 1) === 0 && n > 0
  • 计算平均值(避免溢出):const avg = (a & b) + ((a ^ b) >> 1);

挑战

编写一个函数,接收两个整数作为参数,返回它们的平均值(向下取整)。使用位运算实现,避免可能的溢出问题。

js 复制代码
function average(a, b) {
    // 在这里实现代码
}

解答

可以使用 (a & b) + ((a ^ b) >> 1) 计算平均值,这种方法可以避免 (a + b) / 2 可能导致的溢出问题。

js 复制代码
function average(a, b) {
    return (a & b) + ((a ^ b) >> 1);
}

理解了这10个关卡的知识点,就代表你对位运算的概念已经有了一定的理解,最后,感谢阅读,觉得有用望不吝啬点赞收藏。

相关推荐
复苏季风7 小时前
为什么Vite动态加载图片报错? 动态资源引入的那些事儿
前端·vue.js·架构
sp428 小时前
静态网站生成利器 Eleventy
前端
阿聪_8 小时前
React.ComponentType 类型使用
前端
aiwery8 小时前
理解 JavaScript 中的 Iterator、Generator、Promise 与 async/await
前端·面试
K仔8 小时前
什么是DOM事件模型
前端
熊猫片沃子8 小时前
新手必避的 Vue 基础坑:从数据绑定到事件处理的常见错误与解决方案
前端·vue.js
lichenyang4538 小时前
UniApp 实现搜索页逻辑详解
前端
怪可爱的地球人8 小时前
处理“文本搜索和替换” 的工具-RegExp
前端
公众号:重生之成为赛博女保安8 小时前
一款基于selenium的前端验证码绕过爆破工具
前端·selenium·测试工具