位运算
今天刷了几道【位运算】的入门习题,挺有趣的,同时也发现自己的基础知识还不够牢固,所以记录一下,方便复习。
1486. 数组异或操作
题目描述
- 链接: 1486. 数组异或操作
输入输出
参考代码一
遍历区间 中的每一个整数 i,令 ans 与每一个 start + 2 * i 做异或运算(^),最终返回结果即可。
class Solution {
public:
int xorOperation(int n, int start) {
int ans = 0;
for(int i = 0; i<n; i++) {
ans ^= start + 2 * i;
}
return ans;
}
};
参考代码二
下面是官方的代码,时间复杂度为 。
class Solution {
public:
// 返回 0⊕1⊕2⊕⋯⊕x 的结果
int sumXor(int x) {
if (x % 4 == 0) return x;
if (x % 4 == 1) return 1;
if (x % 4 == 2) return x + 1;
return 0;
}
int xorOperation(int n, int start) {
// 也就是 s = start / 2,然后向下取整
int s = start >> 1;
// 根据 start 和 n 的奇偶性就可以求出最后一位的值 e,值为0或1。
int e = n & start & 1;
int ret = sumXor(s - 1) ^ sumXor(s + n - 1);
return ret << 1 | e;
}
};
记录
首先,记录异或的性质,我们设 是异或运算符,那么异或运算满足以下性质:
- ;
- ;
- ;
- ;
- 有 ;
那么现在来看本题要计算的式子:
观察公式可以知道,这些数的奇偶性质相同,因此它们的二进制表示中的最低位或者均为 ,或者均为
所以这道题的核心思想就是:
将参与运算的数的二进制位的最低位提取出来单独处理。
考虑将所有参与异或的数都删除最后一位再进行运算(也就是除以 ),然后用得到的结果乘于 (即向左移位),再加上最后一位值:
那么要计算的式子就转化为:
令 s = start/2,得到最后的公式:
这里记录一下,为什么最后一步要 然后再乘以 加
- 设 是
s到s+n-1的异或结果,是我们需要的结果 - 设 是
0到s-1的异或结果,即sumXor(s-1) - 因为
- 由于 异或运算 的相反性
- 所以 就是我们要的答案
- 所以令
res = sumXor(s+n-1) ^ sumXor(s-1) - 最后再乘以 2 并加上 e 即可:
return ret << 1 | e;
备注:
为什么当且仅当
start和n都为奇数时,结果的二进制位的最低位才为1。因为当
n为偶数时:
- 如果
start为偶数,那么n个偶数做异或,最后一位一定是0- 如果
start为奇数,那么n个奇数做异或,最后一位一定是0
1720. 解码异或后的数组
题目描述
输入输出
参考代码
解题思路都在代码里:
/*
题意:
- 原数组:arr
- 编码后的数组:encoded
- 编码规则:encoded[i] = arr[i] ^ arr[i+1]
- 已知:arr[0] = first 以及 encoded 数组
- 要求:返回原数组 arr
思路:
- 已知编码规则:encoded[i] = arr[i] ^ arr[i+1]
- 那么当 i 的范围在 [1, n) 之间时:
* 有 encoded[i-1] = arr[i-1] ^ arr[i]
* 此时在等式两边同时异或 arr[i-1],结果为:
* arr[i-1] ^ encoded[i-1] = arr[i-1] ^ arr[i-1] ^ arr[i]
* 由于 任意整数 和 自身做异或运算 的结果都等于 0
* 那么上面的式子就变为:
* arr[i-1] ^ encoded[i-1] = 0 ^ arr[i]
* 又因为 任意整数 和 0 做 异或运算 的结果都等于其自身
* 那么上面的式子就变为:
* arr[i-1] ^ encoded[i-1] = arr[i]
- 好了,解题思路出来了:
* 对 i 从 1 到 n-1,依次遍历,计算出 arr[i] 的值
* 即 arr[i] = arr[i-1] ^ encoded[i-1]
* 这样就可以解码得到原数组arr
*/
class Solution {
public:
vector<int> decode(vector<int>& encoded, int first) {
int n = encoded.size() + 1;
vector<int> arr(n, 0);
arr[0] = first;
for(int i = 1; i<n; i++) {
arr[i] = arr[i-1] ^ encoded[i-1];
}
return arr;
}
};
注意:
- 异或(
^)的两条基本性质:- 任意整数 和 自身 做异或运算 的结果都等于
- 任意整数 和 做 异或运算 的结果都等于其自身
693. 交替位二进制数
题目描述
- 链接:693. 交替位二进制数
输入输出
参考代码
/*
解题思路:
- 从低位到高位,我们对数值 n 进行取模最后再除以 2,可以得到的当前位上的数
- 如果 当前位上的数 和 前一位上的数字 相等,那么不符合条件,直接返回 false
- 如果不同,那么符合条件,此时更新前一位上的数字(将当前位上的数字赋值到前一位上)
- 然后指向更高一位的数字,接着循环,直到 n = 0,如果一直符合条件,则返回true
*/
class Solution {
public:
bool hasAlternatingBits(int n) {
int prior = 2;
while(n != 0) {
int now = n % 2;
if(now == prior) return false;
prior = now;
n /= 2;
}
return true;
}
};
191. 位 1 的个数
题目描述
- 链接:191. 位1的个数
输入输出
参考代码一
/*
思路:
- 依次遍历到每一位上的值,检查位上是否为 1
- 这里使用到了一个技巧:
* 如果想检查数字 n 的第 i 位是否为 1
* 那么可以让 n 与 2^i 进行 与运算(&)
* 如果 n & (1<<i) 的值不等于 0,那么表明 n 的第 i 位为 1
*/
class Solution {
public:
int hammingWeight(uint32_t n) {
int ans = 0;
for(int i = 0; i<32; i++) {
if(n & (1 << i)) {
ans++;
}
}
return ans;
}
};
参考代码二
/*
思路:
- 我们可以不断让当前的 n 与 (n-1) 做 与运算(&),直到 n 变为 0
- 原理:
* 看这行代码: n & (n - 1)
* 这行代码的作用是将数值 n 的二进制中末尾的 1 改为 0
* 为什么呢?来看下面例子:
* n : 1 0 1 1 0 0
* n - 1: 1 0 1 0 1 1
* n & (n-1): 1 0 1 0 0 0
* 所以说,n & (n - 1) 可以让 n 的二进制数中最后一个出现的 1 改写成 0
*/
class Solution {
public:
int hammingWeight(uint32_t n) {
int ans = 0;
while(n != 0) {
n = n & n-1;
ans++;
}
return ans;
}
};
参考代码三
/*
思路:
- 使用 n & 1 检测二进制末尾是否为 1
- 接着,让 n 右移 1 位
- 反复进行,直到 n 等于 0
*/
class Solution {
public:
int hammingWeight(uint32_t n) {
int ans = 0;
while(n != 0) {
ans += n&1;
n >>= 1;
}
return ans;
}
};
338. 比特位计
题目描述
- 链接:338. 比特位计
输入输出
参考代码
思路同上一题:
class Solution {
public:
int count(int val) {
int ans = 0;
while(val != 0) {
val &= (val-1);
ans++;
}
return ans;
}
vector<int> countBits(int n) {
vector<int> res(n+1, 0);
for(int i = 0; i<=n; i++) {
res[i] = count(i);
}
return res;
}
};
461. 汉明距离
题目描述
- 链接:461. 汉明距离
输入输出
参考代码
/*
思路:
- 首先计算 x 和 y 的异或结果,即 res = x^y
- 接着,统计 res 中 1 的个数即可
*/
class Solution {
public:
int hammingDistance(int x, int y) {
int ans = 0; // 存放结果
int res = x^y; // x⊕y 的结果
while(res != 0) {
res &= (res-1);
ans++;
}
return ans;
}
};
190. 颠倒二进制位
题目描述
- 链接:190. 颠倒二进制位
输入输出
参考代码一
/*
题意:
- 给定数字 n,将其二进制数翻转,求得另一个二进制数
解题:
- 每次取到 n 的二进制末尾的数字
- 拼接到结果集 ans 中
- 然后更新 n,(右移一位)
*/
class Solution {
public:
uint32_t reverseBits(uint32_t n) {
uint32_t ans = 0;
for(int i = 0; i<32; i++) {
ans = (ans << 1) | (n & 1);
n >>= 1;
}
return ans;
}
};
参考代码二
/*
进制转换
- 十六进制 二进制
- f 1111
- c 1100
- a 1010
- 5 0101
- 3 0011
分治法:
- 32 位无符号整数,如: 1111 1111 1111 1111 1111 1111 1111 1111
- 表示成 16 进制,则是: f f f f f f f f
- 接着:
* ffff ffff 右移 16 位,变成 0000 ffff
* ffff ffff 左移 16 位,变成 ffff 0000
* 它们俩相或,就可以完成低16位 与 高16位的交换
* 接着再依次分治下去,一共需要五次
- 注意,每次分治都需要先和上一个掩码进行交换
*/
class Solution {
public:
uint32_t reverseBits(uint32_t n) {
// 1. 在 32 位中,低 16 位与高 16 位交换
n = (n >> 16) | (n << 16);
// 2. 在 16 位中,低 8 位与高 8 位交换
n = ((n & 0xff00ff00) >> 8) | ((n & 0x00ff00ff) << 8);
// 3. 在 8 位中,低 4 位与高 4 位交换
n = ((n & 0xf0f0f0f0) >> 4) | ((n & 0x0f0f0f0f) << 4);
// 4. 在 4 位中,低 2 位与高 2 位交换
n = ((n & 0xcccccccc) >> 2) | ((n & 0x33333333) << 2);
// 5. 在 2 位中,低 1 位与高 1 位交换
n = ((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1);
return n;
}
};
/*
备注:
- 十六进制 二进制
- 0x00ff00ff 00000000111111110000000011111111
- 0x0f0f0f0f 00001111000011110000111100001111
- 0x33333333 00110011001100110011001100110011
- 0x55555555 01010101010101010101010101010101
*/
记录
总结一些位运算的入门小技巧:
首先,是求解 的函数。
定义如下:
int sumXor(int x) {
if (x % 4 == 0) return x;
if (x % 4 == 1) return 1;
if (x % 4 == 2) return x + 1;
return 0;
}
接着,是 异或 运算的性质:
- ;
- ;
- ;
- ;
- ;
- 有 ;
如果想要将一个数,比如 val 的最后一位删除掉,那么只需 右移 1 位即可,比如 val >> 1
另外,如果我们想知道在数字 的二进制表达中,第 位是否为 ,那么可以让 与 进行 与运算(&)。
如果 & 不等于,那么表明 n 的第 i 位为 1,用代码表示则是:n & (1<<i) != 0
代码 n & (n - 1) 可以让 n 的二进制数中最后一个出现的 1 改写成 0
如果你想知道两个数值的对应二进制位有几个不同,那么可以先计算二者的异或结果,再来统计结果中二进制位上 1 的个数即可。
当然,位运算的技巧不止这一点点,感兴趣的朋友可以自行百度。
本文正在参加「金石计划」