LeetCode算法日记 - Day 17: 算法中的位运算技巧总结
目录
1. 常见位运算
1.1 基础运算
1.1.1 & 按位与
1.1.2 | 按位或
1.1.3 ^ 按位异或
1.1.4 ~ 按位取反
1.2 判断 n 的第 x 位是 0 还是 1
1.3 将第 x 位设为 1
1.4 将第 x 位设为 0
1.5 提取最右侧的 1(lowbit)
1.6 删除最右侧的 1
1.7 异或 ^ 的运算规律
2. 位 1 的个数
2.1 题目解析
2.2 解法
2.3 代码实现
3. 比特位计数
3.1 题目解析
3.2 解法
3.3 代码实现
4. 汉明距离
4.1 题目解析
4.2 解法
4.3 代码实现
1. 常见位运算
1.1 基础运算
1.1.1 & 按位与
有零就是零,常用于筛选/清零
位运算 | 二进制 | 结果 |
---|---|---|
1 & 1 | 1 | 1 |
1 & 0 | 0 | 0 |
0 & 1 | 0 | 0 |
0 & 0 | 0 | 0 |
1.1.2 | 按位或
只要有一个1就是1,常用于保留/合并
A | B | A | B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
1.1.3 ^ 按位异或
不同才为 1,常用于去重
A | B | A ^ B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
1.1.4 ~ 按位取反
0 变 1,1 变 0
A | ~A |
---|---|
0 | 1 |
1 | 0 |
1.2 判断 n 的第 x 位是 0 还是 1
本质是从二进制里读出某一位(位查询),可以使用位移 + 掩码:把目标搬到最后右边,再 &1 留末位判断,bit = (n >> x) & 1。
1.3 将第 x 位设为 1
本质就是位写入,可以通过 | 完成,n | (1 << x)。
1.4 将第 x 位设为 0
本质就是位清零,可以通过使 x 位与上0完成,n & ~(1 << x)。
1.5 提取最右侧的 1(lowbit)
本质是定位最低有效位 1 的全值,可以利用补码性质一次得到,lowbit = n & -n。
1.6 删除最右侧的 1
n-1 会把“最低 1 及其右侧”翻转;与回去即可清掉,n & (n-1)。
1.7 异或 ^
的运算规律
举三个常见例子:
(1) a ^ 0 = a
(2) a ^ a = 0
(3) a^b^c =
2. 位 1 的个数
191. 位1的个数 - 力扣(LeetCode)
给定一个正整数 n
,编写一个函数,获取一个正整数的二进制形式并返回其二进制表达式中 设置位 的个数(也被称为汉明重量)。
示例 1:
输入:n = 11
输出:3
解释:输入的二进制串 1011 中,共有 3 个设置位。
示例 2:
输入:n = 128 输出:1 解释:输入的二进制串 10000000 中,共有 1 个设置位。
示例 3:
输入:n = 2147483645 输出:30 解释:输入的二进制串 1111111111111111111111111111101 中,共有 30 个设置位。
2.1 题目解析
我们需要计算一个正整数 n 的二进制表示中有多少个“1”位。这被称为 汉明重量(Hamming Weight)。在实际中,二进制数的每一位如果是 1,就可以被称为“设置位”。
常规解法:
最直观的做法是将数字 n 转换为二进制字符串,然后遍历字符串,统计其中 1 的数量。比如:
-
将 n 转为二进制字符串;
-
遍历字符串,统计其中 1 的个数。
这种做法直观易懂,但性能较差,因为每次转换为字符串时会有一定的开销。
这种解法的时间复杂度是 O(log n),因为将一个数字转为二进制需要 O(log n) 的时间。每次转换为二进制并统计 1 的个数需要额外的空间和操作,可能会引入不必要的计算。
思路转折:
为了提高效率,我们可以通过 位运算 来实现这一功能。特别是通过 n & (n - 1) 操作可以高效地减少 n 中的 1 的个数。每次执行这个操作都会清除最右边的一个 1,这就是我们要用的技巧。通过这种方式,我们可以在 O(log n) 时间内解决问题。
2.2 解法
通过利用 n & (n - 1) 操作来减少 n 中的 1 位数。每次这个操作将 n 中最右边的一个 1 清零,并且我们只需对这个操作重复进行直到 n 变为 0。通过计数这些操作次数,我们可以得到 n 中 1 的数量。
-
每次 n & (n - 1) 操作会清除 n 中最右边的 1。
-
所以,只需要执行这个操作若干次,每次将 n 中的一个 1 清除,直到 n 为 0,统计这些操作的次数即为汉明重量。
i)初始化一个计数器 ret,用于存储设置位的个数。
ii)使用 while 循环,直到 n 为 0:
-
每次执行 n = n & (n - 1),去除最右边的 1。
-
每执行一次操作,ret 增加 1。
iii)返回 ret,即二进制表示中 1 的个数。
2.3 代码实现
class Solution {public int hammingWeight(int n) {int ret = 0;// 当 n 不为 0 时,继续执行while (n > 0) {// 清除最右边的 1n = n & (n - 1);// 计数器加 1ret++;}return ret;}
}
3. 比特位计数
338. 比特位计数 - 力扣(LeetCode)
给你一个整数 n
,对于 0 <= i <= n
中的每个 i
,计算其二进制表示中 1
的个数 ,返回一个长度为 n + 1
的数组 ans
作为答案。
示例 1:
输入:n = 2 输出:[0,1,1] 解释: 0 --> 0 1 --> 1 2 --> 10
示例 2:
输入:n = 5 输出:[0,1,1,2,1,2] 解释: 0 --> 0 1 --> 1 2 --> 10 3 --> 11 4 --> 100 5 --> 101
3.1 题目解析
本题要求对于每个数字 i (0 <= i <= n),计算其二进制表示中 1 的个数,并将这些结果返回一个长度为 n + 1 的数组 ans。换句话说,给定一个整数 n,我们需要求出从 0 到 n 中每个数的二进制中 1 的数量。
常规解法:
一种直观的做法是逐个计算每个数字 i 的二进制形式,统计其中 1 的个数。例如:
-
遍历每个数字 i 从 0 到 n;
-
对于每个数字 i,转换为二进制表示,并统计 1 的个数。 这种方法比较直观,但是每次都需要进行二进制转换,效率较低,尤其是当 n 较大时,转换和计数会花费大量时间。
这种方法的时间复杂度是 O(n * log n),因为每个数字的二进制表示最多有 O(log n) 位,需要对每个数字执行一次二进制转换和计数。而当 n 非常大时,这种方法的效率可能不足以满足性能要求。
思路转折:
为了提高效率,我们可以使用 动态规划 或 优化的位运算方法。关键是利用 i >> 1 这种操作,因为每个数字 i 的二进制 1 的个数可以通过它的“高位部分”来推导。
-
ret[i] = ret[i >> 1] + (i & 1):这个公式的含义是:i >> 1 表示去掉 i 的最低位后剩余的数字,我们已经知道其二进制 1 的个数;(i & 1) 用来检查 i 的最低位是否为 1,如果是 1 就加 1,否则加 0。
通过这种方式,我们可以利用之前计算过的结果高效地推算出下一个结果,从而避免重复计算。
3.2 解法
通过动态规划的方式,使用递推公式 ret[i] = ret[i >> 1] + (i & 1) 来计算每个数字的二进制 1 的个数,其中:
-
i >> 1 表示数字 i 右移一位,去掉最低位
-
(i & 1) 检查 i 的最低位是否为 1。
这种方法通过复用之前计算的结果,避免了重复的二进制转换,极大提高了效率。
i)初始化一个长度为 n + 1 的数组 ret,用来存储从 0 到 n 每个数字的二进制中 1 的个数。
ii)从 i = 1 开始遍历到 n,使用公式 ret[i] = ret[i >> 1] + (i & 1) 来计算每个数字的二进制中 1 的个数。
iii)返回最终的 ret 数组。
3.3 代码实现
class Solution {public int[] countBits(int n) {int[] ret = new int[n + 1]; // 用于存储结果// 从 1 开始,因为 0 的汉明重量已知为 0for (int i = 1; i <= n; i++) {ret[i] = ret[i >> 1] + (i & 1); // 右移一位并加上最低位的 1}return ret;}
}
4. 汉明距离
461. 汉明距离 - 力扣(LeetCode)
两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。
给你两个整数 x
和 y
,计算并返回它们之间的汉明距离。
示例 1:
输入:x = 1, y = 4 输出:2 解释: 1 (0 0 0 1) 4 (0 1 0 0)↑ ↑ 上面的箭头指出了对应二进制位不同的位置。
示例 2:
输入:x = 3, y = 1 输出:1
提示:
0 <= x, y <= 231 - 1
4.1 题目解析
本题要求计算两个整数 x 和 y 之间的 汉明距离。汉明距离是指这两个数字对应二进制位不同的位置的个数。例如,如果两个数字的二进制表示不同,那么它们之间的汉明距离就是它们在二进制表示上不相同的位数。
常规解法:
最直观的做法是将两个数字 x 和 y 转换为二进制表示,逐位比较它们的差异。具体步骤如下:
-
将 x 和 y 转换为二进制字符串。
-
遍历这两个字符串,计算它们在相同位置上的字符是否不同,如果不同,则增加汉明距离。
这种做法较为直接,但对于数字比较大的情况下,转化为二进制字符串并逐位比较会浪费时间和空间。
上述方法的时间复杂度是 O(log n),因为需要将数字转为二进制并逐位比较。然而,对于较大的数字来说,转换为字符串后再进行比较可能会涉及额外的内存分配,降低效率。
思路转折:
为了提高效率,可以通过位运算来解决这个问题。通过 异或(XOR) 操作,我们可以迅速找出 x 和 y 在二进制表示上不同的位置。
-
异或操作:如果两个数字在某一位不同,则该位的结果为 1;如果相同,则该位的结果为 0。
-
通过 x ^ y 我们可以得到一个新的数字,其中 1 表示两个数字在该位上不同,0 表示相同。
-
之后,我们只需要统计这个异或结果中 1 的个数,即为它们之间的汉明距离。
4.2 解法
异或运算:通过 x ^ y,得到一个新数字,其中每个 1 表示 x 和 y 在该位不同
统计 1 的个数:通过位操作统计异或结果中 1 的个数,即为汉明距离。
i)计算 x ^ y,得到一个新的数 tmp,其中每个 1 表示 x 和 y 在该位不同。
ii)使用 tmp & 1 判断 tmp 的最低位是否为 1,如果是 1,则说明该位 x 和 y 不同,汉明距离增加 1。
iii)右移 tmp,继续检查剩余的位,直到 tmp 为 0。
iiii)返回最终的汉明距离。
4.3 代码实现
class Solution {public int hammingDistance(int x, int y) {int distance = 0; // 汉明距离计数器int tmp = x ^ y; // 计算 x 和 y 的异或结果while (tmp > 0) { // 当 tmp 不为 0 时,继续统计不同的位distance += tmp & 1; // 检查最低位是否为 1,如果是,增加汉明距离tmp >>= 1; // 右移一位,继续检查}return distance; // 返回最终汉明距离}
}