关卡来自于我写的一个位运算平台,一共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 = 0
和 a ^ 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个关卡的知识点,就代表你对位运算的概念已经有了一定的理解,最后,感谢阅读,觉得有用望不吝啬点赞收藏。