位运算相关解题技巧
位运算相关概念:
1. 二进制表示
计算机内部所有的数据都是以二进制的形式存储。二进制数使用0
和1
两个符号来表示数据。每个0
或1
称为一位(Bit)。
2. 位运算符
位运算符直接对整数的二进制位进行操作。常见的位运算符包括:
- 与(AND)
&
: 对于每一位,如果两个操作数中相应的位都是1,则结果为1,否则为0。 - 或(OR)
|
: 对于每一位,如果两个操作数中相应的位至少有一个1,则结果为1,否则为0。 - 异或(XOR)
^
: 对于每一位,如果两个操作数中相应的位不同,则结果为1,否则为0。 - 非(NOT)
~
: 对操作数的每一位,将1变为0,0变为1。 - 左移(Left Shift)
<<
: 将操作数的二进制表示向左移动指定的位数,右边空出的位用0填充。 - 右移(Right Shift)
>>
: 将操作数的二进制表示向右移动指定的位数。对于无符号数,左边空出的位用0填充;对于有符号数,填充方式取决于具体实现,通常是用符号位填充。
位运算的应用
检查、设置和清除位
- 检查第
n
位是否为1:(num & (1 << n)) != 0
- 设置第
n
位为1:num |= (1 << n)
- 清除第
n
位(设置为0):num &= ~(1 << n)
- 切换第
n
位:num ^= (1 << n)
权限控制
位运算常用于实现简单的权限控制系统。例如,使用不同的位来代表不同的权限,然后通过位运算来添加、检查或移除特定的权限。
快速乘除以2的幂
- 乘以2的
n
次幂:num << n
- 除以2的
n
次幂:num >> n
交换两个变量
不使用临时变量交换两个变量的值:
a ^= b;
b ^= a;
a ^= b;
奇偶校验
通过异或运算,可以快速判断一个数的奇偶性:
- 奇数:
num & 1 == 1
- 偶数:
num & 1 == 0
技巧和注意事项
- 位运算只适用于整数。在对浮点数进行位运算之前,必须将其转换为整数类型。
- 理解补码表示法。计算机中的负数通常使用补码形式表示,这对于非运算和右移运算尤其重要。
- 优化性能。位运算通常比传统的算术运算更快,可以在性能要求严格的应用中使用位运算进行优化。
- 代码可读性。虽然位运算很强大,但过度使用可能会降低代码的可读性。
另:异或运算相关性质
异或运算(XOR,符号为 ^
)是位运算中非常有用且有趣的一个,它遵循一些独特的性质,这些性质在算法设计、数据加密、错误检测和纠正等领域中被广泛应用。下面是异或运算的一些基本性质及其应用实例。
异或运算的性质
- 交换律:a ^ b ^ a = b ^ a ^ a,即异或运算满足交换律,意味着异或运算的顺序不影响结果。
- 结合律:(a ^ b) ^ c = a ^ (b ^ c),即异或运算满足结合律,可以按照不同的组合方式进行运算,得到的结果是一致的。
- 自反性:a ^ b ^ b = a ^ 0 = a,即对一个给定的数A,用同样的运算因(B)作两次异或运算后仍得到A本身。
- 特定值的异或:任何数与0异或都等于其本身,即x ^ 0 = x;任何数与其自身异或都等于0,即x^x=0。
应用实例
- 值交换:不使用临时变量交换两个变量的值。
a = a ^ b;
b = a ^ b; // Now, b is a.
a = a ^ b; // Now, a is b.
- 寻找唯一不同的元素:在一个数组中,除了一个元素之外,其他所有元素都出现两次。可以使用异或运算找到这个唯一的元素,因为相同的数异或结果为0,0与任何数异或结果为那个数本身。
int singleNumber = 0;
for (int num : nums) {
singleNumber ^= num;
}
// singleNumber 就是那个唯一的不同元素。
- 构建简单的加密解密机制:由于异或运算的自反性,同一个键值进行两次异或操作可以得回原文,这使得异或运算可以用于简单的加密和解密过程。
char encrypted = data ^ key;
char decrypted = encrypted ^ key; // decrypted == data
- 二进制下的位翻转:利用异或运算,可以实现二进制位的翻转。例如,将一个数与一个全为1的数进行异或操作,即可实现位翻转。
这些性质和应用示例展示了异或运算的强大与灵活性,使其成为解决特定问题的有力工具。
位运算相关公式:
位运算公式 | 解释 |
---|---|
x & 1 | 判断奇偶性 真为奇,假为偶 |
x >> 1 | 将一个整数右移一位等价于将这个整数除以2(忽略小数部分) |
n >>k & 1 | 求n的二进制位的第k位数字 |
x | (1 << k) | 将x第k位置为1 |
x ^ (1 << k) | 将x第k位取反 |
x & (x - 1) | 将x最右边的1置为0(去掉最右边的1) |
x | (x + 1) | 将x最右边的0置为1 |
Leetcode例题:
136. 只出现一次的数字
给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
输入:nums = [2,2,1]
输出:1
示例 2 :
输入:nums = [4,1,2,1,2]
输出:4
示例 3 :
输入:nums = [1]
输出:1
提示:
1 <= nums.length <= 3 * 104
-3 * 104 <= nums[i] <= 3 * 104
- 除了某个元素只出现一次以外,其余每个元素均出现两次。
class Solution {
public int singleNumber(int[] nums) {
//异或运算,支持交换律
int res = 0;
for (int i = 0; i < nums.length; i++) {
res = res ^ nums[i];
}
return res;
}
}
LCR 133. 位 1 的个数
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为 汉明重量).)。
示例 1:
输入:n = 11 (控制台输入 00000000000000000000000000001011)
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
示例 2:
输入:n = 128 (控制台输入 00000000000000000000000010000000)
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
示例 3:
输入:n = 4294967293 (控制台输入 11111111111111111111111111111101,部分语言中 n = -3)
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。
提示:
- 输入必须是长度为
32
的 二进制串 。
提示:
- 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
- 在 Java 中,编译器使用 二进制补码 记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数
-3
。
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int sum = 0;
while (n != 0) {
n = n & (n - 1);//把最右边的1变为0
sum++;
}
return sum;
}
}
//时间复杂度:O(logn)。循环次数等于 n 的二进制位中 1 的个数,最坏情况下 n 的二进制位全部为 1。我们需要循环 logn 次。
//空间复杂度:O(1),我们只需要常数的空间保存若干变量。
求m的n次方优化:
普通:
// n是正整数,时间复杂度为O(n)
int pow(int m, int n) {
int res = 1;
for (int i = 0; i < n; i++) {
res = res * m;
}
return res;
}
优化(有点难理解):
// 不需要遍历n次,时间复杂度为O(logn)
int pow(int m, int n) {
int sum = 1;
int tmp = m;
while(n != 0) {
if(n & 1 == 1) { // 该位是1,则乘上该位对应的幂次
sum *= tmp;
}
tmp *= tmp;
n >> 1;
}
return sum;
}
260. 只出现一次的数字 III
给你一个整数数组 nums
,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。
你必须设计并实现线性时间复杂度的算法且仅使用常量额外空间来解决此问题。
示例 1:
输入:nums = [1,2,1,3,2,5]
输出:[3,5]
解释:[5, 3] 也是有效的答案。
示例 2:
输入:nums = [-1,0]
输出:[-1,0]
示例 3:
输入:nums = [0,1]
输出:[1,0]
提示:
2 <= nums.length <= 3 * 104
-231 <= nums[i] <= 231 - 1
- 除两个只出现一次的整数外,
nums
中的其他数字都出现两次
class Solution {//哈希法,空间复杂度为O(n)
public int[] singleNumber(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
int[] res = new int[2];
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
int index = 0;
for (Integer key : map.keySet()) {
if (map.get(key) == 1) res[index++] = key;
}
return res;
}
}
class Solution {//位运算技巧
public int[] singleNumber(int[] nums) {
int z = 0;
for (int i = 0; i < nums.length; i++) {
z ^= nums[i];
}
//m表示第几个二进制位不同
int m = 1;
while ((m & z) == 0) {
m = m << 1;//找到不同的那个二进制位
}
int x = 0, y = 0;
for (int i = 0; i < nums.length; i++) {//通过m与运算将数组分为两部分,而两个只出现一次的数字必会分配到两个数组中,进而进行异或运算
if ((nums[i] & m) == 0) {
//不需要额外new一个数组,直接计算就行
x = x ^ nums[i];
} else {
y = y ^ nums[i];
}
}
return new int[]{x, y};
}
}
137. 只出现一次的数字 II
给你一个整数数组 nums
,除某个元素仅出现 一次 外,其余每个元素都恰出现 **三次 。**请你找出并返回那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法且使用常数级空间来解决此问题。
示例 1:
输入:nums = [2,2,3,2]
输出:3
示例 2:
输入:nums = [0,1,0,1,0,1,99]
输出:99
提示:
1 <= nums.length <= 3 * 104
-231 <= nums[i] <= 231 - 1
nums
中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次
class Solution {//hash
public int singleNumber(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (entry.getValue() == 1) return entry.getKey();
}
return 0;
}
}
class Solution {//位运算,确定每一个二进制位
public int singleNumber(int[] nums) {
int[] res = new int[32];
int m = 1;
int sum = 0;
for (int i = 0; i < 32; i++) {
for (int j = 0; j < nums.length; j++) {
if ((nums[j] & m) != 0) {
res[i]++;//收集所有数字与1与为1的个数
}
}
res[i] = res[i] % 3;//模3得到的值即为唯一不同元素该位的值
sum = sum + res[i] * m;//运算结果相加
m = m << 1;//m再左移一位继续判断
}
return sum;
}
}