1.位运算基础
1.1 基本位运算符
public class BitwiseOperators {
public static void main(String[] args) {
int a = 5; // 二进制:0101
int b = 3; // 二进制:0011
// 1. 按位与 &
System.out.println("5 & 3 = " + (a & b)); // 输出1
// 2. 按位或 |
System.out.println("5 | 3 = " + (a | b)); // 输出7
// 3. 按位异或 ^
System.out.println("5 ^ 3 = " + (a ^ b)); // 输出6
// 4. 按位取反 ~
System.out.println("~5 = " + (~a)); // 输出-6
// 5. 左移 <<
System.out.println("5 << 1 = " + (a << 1)); // 输出10
// 6. 右移 >>
System.out.println("5 >> 1 = " + (a >> 1)); // 输出2
}
}
1.2 位运算详解
- 按位与(&)
0101 (5)
& 0011 (3)
-------
0001 (1)
规则:两个位都是1才得1,否则得0
- 按位或(|)
0101 (5)
| 0011 (3)
-------
0111 (7)
规则:两个位都是0才得0,否则得1
- 按位异或(^)
0101 (5)
^ 0011 (3)
-------
0110 (6)
规则:两个位相同得0,不同得1
- 左移(<<)
5 << 1 的过程:
0101 (5)
1010 (10)
规则:所有位向左移动,右边补0
每左移一位相当于乘2
- 右移(>>)
5 >> 1 的过程:
0101 (5)
0010 (2)
规则:所有位向右移动,左边补符号位
每右移一位相当于除2
2. 实用技巧
2.1 判断奇偶
public class NumberUtils {
public static boolean isOdd(int n) {
return (n & 1) == 1; // 如果最后一位是1,则为奇数
}
public static void main(String[] args) {
System.out.println("5是否为奇数:" + isOdd(5)); // true
System.out.println("6是否为奇数:" + isOdd(6)); // false
}
}
详细讲解
好的,让我详细解释这个判断奇偶数的位运算技巧。
1. 原理解释
public class OddEvenCheck {
public static boolean isOdd(int n) {
return (n & 1) == 1;
}
public static void main(String[] args) {
// 测试几个数字
System.out.println("5是否为奇数:" + isOdd(5)); // true
System.out.println("8是否为奇数:" + isOdd(8)); // false
System.out.println("15是否为奇数:" + isOdd(15)); // true
}
}
让我们看看为什么 (n & 1) == 1 可以判断奇偶:
-
任何数的二进制表示中:
- 奇数的最后一位必定是1
- 偶数的最后一位必定是0
-
具体示例:
5(奇数) = 0101
&
1 = 0001
--------------
结果 = 0001 (等于1)
8(偶数) = 1000
&
1 = 0001
--------------
结果 = 0000 (等于0)
15(奇数)= 1111
&
1 = 0001
--------------
结果 = 0001 (等于1)
2. 为什么这样做有效?
-
与运算(&)的特点:
- 只有两个位都是1时,结果才是1
- 其他情况结果都是0
-
数字1的特点:
- 二进制表示为:0001
- 只有最后一位是1
-
当任何数与1进行与运算时:
- 只看最后一位的运算结果
- 其他位都会变成0
3. 更多示例
public class DetailedOddEvenCheck {
public static void checkNumber(int n) {
System.out.println("数字: " + n);
System.out.println("二进制: " + Integer.toBinaryString(n));
System.out.println("与1进行与运算: " + (n & 1));
System.out.println("是否为奇数: " + ((n & 1) == 1));
System.out.println("-------------");
}
public static void main(String[] args) {
checkNumber(5); // 奇数
checkNumber(8); // 偶数
checkNumber(15); // 奇数
checkNumber(20); // 偶数
}
}
输出示例:
数字: 5
二进制: 101
与1进行与运算: 1
是否为奇数: true
-------------
数字: 8
二进制: 1000
与1进行与运算: 0
是否为奇数: false
-------------
4. 与传统方法比较
- 传统方法(取模):
boolean isOdd = n % 2 == 1;
- 位运算方法:
boolean isOdd = (n & 1) == 1;
位运算的优点:
- 执行速度更快(CPU层面的运算)
- 代码简洁
- 不需要除法运算
5. 实际应用
public class OddEvenApplication {
// 分离奇数和偶数
public static void separateOddEven(int[] arr) {
System.out.println("奇数:");
for (int n : arr) {
if ((n & 1) == 1) {
System.out.print(n + " ");
}
}
System.out.println("\n偶数:");
for (int n : arr) {
if ((n & 1) == 0) {
System.out.print(n + " ");
}
}
}
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
separateOddEven(numbers);
}
}
6. 记忆要点
- 记住奇数的二进制最后一位一定是1
- 记住偶数的二进制最后一位一定是0
- 与1进行与运算就是保留最后一位,其他位都变成0
这个技巧在实际编程中经常使用,尤其是在需要频繁判断奇偶性的场景下,使用位运算会更高效!
2.2 获取最右边的1
public class BitManipulation {
public static int getRightmostOne(int n) {
return n & (-n);
// 或者:n & (~n + 1)
}
public static void main(String[] args) {
int n = 12; // 二进制:1100
System.out.println(Integer.toBinaryString(getRightmostOne(n))); // 输出:100
}
}
详细讲解
让我用更直观的方式解释"获取最右边的1"。
1. 什么是"最右边的1"?
在二进制数字中,从右往左看,第一个遇到的1就是"最右边的1"。
让我们看几个例子:
数字12的二进制: 1100
最右边的1所在位置:0100(结果为4)
数字10的二进制: 1010
最右边的1所在位置:0010(结果为2)
数字24的二进制: 11000
最右边的1所在位置:01000(结果为8)
2. 直观示例
public class RightmostOneDemo {
public static void showRightmostOne(int n) {
String binary = Integer.toBinaryString(n);
System.out.println("原始数字: " + n);
System.out.println("二进制表示: " + binary);
int result = n & (-n);
String resultBinary = Integer.toBinaryString(result);
// 为了对齐显示,补齐0
while (resultBinary.length() < binary.length()) {
resultBinary = "0" + resultBinary;
}
System.out.println("最右边的1: " + resultBinary);
System.out.println("对应十进制: " + result);
System.out.println("-------------");
}
public static void main(String[] args) {
showRightmostOne(12); // 1100
showRightmostOne(10); // 1010
showRightmostOne(24); // 11000
}
}
运行结果:
原始数字: 12
二进制表示: 1100
最右边的1: 0100
对应十进制: 4
-------------
原始数字: 10
二进制表示: 1010
最右边的1: 0010
对应十进制: 2
-------------
原始数字: 24
二进制表示: 11000
最右边的1: 01000
对应十进制: 8
-------------
3. 图形化解释
以数字12为例:
12的二进制:
1 1 0 0
位置:3 2 1 0
↑ ↑ ↑ ↑
8 4 2 1
最右边的1在位置2,对应值是4
所以结果是4(二进制:0100)
4. 实际应用举例
假设我们要找出一个数组中两个出现奇数次的数字:
public class FindTwoNumbers {
public static void findTwoOddNumbers(int[] arr) {
int xor = 0;
// 得到两个奇数次数字的异或结果
for (int num : arr) {
xor ^= num;
}
// 获取最右边的1
int rightOne = xor & (-xor);
// 用这个1的位置将数组分成两组
// 一组是该位置为1的数字
// 一组是该位置为0的数字
System.out.println("用于分组的位置值: " + rightOne);
System.out.println("该位置为1的数字:");
for (int num : arr) {
if ((num & rightOne) != 0) {
System.out.print(num + " ");
}
}
System.out.println("\n该位置为0的数字:");
for (int num : arr) {
if ((num & rightOne) == 0) {
System.out.print(num + " ");
}
}
}
public static void main(String[] args) {
int[] arr = {2, 4, 3, 6, 3, 2, 5, 5};
findTwoOddNumbers(arr);
}
}
5. 为什么需要获取最右边的1?
-
在分组问题中:
- 最右边的1代表了两个不同数字的一个区分位
- 可以用这个位置把数字分成两组
-
在判断2的幂时:
- 2的幂的二进制中只有一个1
- 获取最右边的1后应该等于原数字
-
在位操作中:
- 最右边的1通常是最容易处理的位
- 可以用来逐位处理二进制数
6. 实用技巧
- 可以用
n & (-n)快速获取最右边的1 - 结果一定是2的某个次幂
- 可以用来判断一个位置是否为1
这样理解起来是不是清晰多了?最右边的1就是从右往左数,第一个遇到的值为1的二进制位,而获取最右边的1就是将这个位置的1保留,其他位置都变成0。
7. n & (-n)的详细讲解
让我详细讲解 n & (-n) 这个操作。
1. 基本原理
public class ExplainRightmostOne {
public static void explain(int n) {
System.out.println("原始数字: " + n);
// 1. 显示n的二进制
String nBinary = Integer.toBinaryString(n);
System.out.println("n的二进制: " + nBinary);
// 2. 显示-n的二进制
String negNBinary = Integer.toBinaryString(-n);
// 只取后32位
negNBinary = negNBinary.substring(Math.max(0, negNBinary.length() - 32));
System.out.println("-n的二进制: " + negNBinary);
// 3. 显示结果
int result = n & (-n);
String resultBinary = Integer.toBinaryString(result);
System.out.println("结果二进制: " + resultBinary);
System.out.println("结果十进制: " + result);
System.out.println("-------------");
}
public static void main(String[] args) {
explain(12); // 1100
explain(10); // 1010
}
}
2. 详细步骤说明
以数字12为例:
步骤1: 获取n的二进制
12 的二进制: 0000 1100
步骤2: 计算-n
1) 取反: 1111 0011
2) 加1: 1111 0100 (-12的二进制)
步骤3: 进行与运算
0000 1100 (n)
& 1111 0100 (-n)
-----------
0000 0100 (结果为4,即最右边的1)
3. 为什么这样做有效?
-
原理解释:
- 在二进制中,负数是通过"取反加一"得到的
- 这个过程会使得最右边的1保持不变,而其右边的0变成1
- 其左边的所有位会变成相反数
-
举例分析数字12:
原始数字12: 0000 1100
取反: 1111 0011
加1(-12): 1111 0100
与运算后:
0000 1100
& 1111 0100
-----------
0000 0100 (4)
4. 更多示例
public class MoreExamples {
public static void main(String[] args) {
// 测试不同类型的数字
int[] numbers = {12, 10, 24, 8, 6};
for (int n : numbers) {
System.out.println("数字: " + n);
System.out.println("二进制: " + Integer.toBinaryString(n));
System.out.println("最右边的1: " + (n & (-n)));
System.out.println("-------------");
}
}
}
输出示例:
数字: 12
二进制: 1100
最右边的1: 4 (0100)
-------------
数字: 10
二进制: 1010
最右边的1: 2 (0010)
-------------
5. 实际应用
public class Applications {
// 判断是否是2的幂
public static boolean isPowerOfTwo(int n) {
return n > 0 && (n & (-n)) == n;
}
// 清除最右边的1
public static int clearRightmostOne(int n) {
return n & (n - 1);
}
// 统计1的个数
public static int countOnes(int n) {
int count = 0;
while (n != 0) {
n = n & (n - 1); // 清除最右边的1
count++;
}
return count;
}
}
6. 注意事项
-
边界情况:
- 当n为0时,结果为0
- 当n为负数时,也能正常工作
-
性质:
- 结果总是2的幂
- 结果对应原数字中最右边的1的位置
-
常见用途:
- 分离最右边的1
- 判断2的幂
- 在位运算相关的算法中使用
这个操作是位运算中的一个重要技巧,理解它的工作原理对于解决位运算相关的问题很有帮助!
8.为什么要获取最右边的1呢
让我用一个生活中的例子来解释。
生活中的例子
想象你是一个老师,班上有一群学生:
小明 小明 小红 小红 小红 小张 小张 小李
你发现有两个学生调皮(小红和小李,他们来了奇数次),其他学生都很乖(来了偶数次)。你要找出这两个调皮的学生。
解决步骤
1. 第一步:先让所有学生排队
第一排:小明 小明 小红 小红 小红 小张 小张 小李
小明来了2次(偶数)
小红来了3次(奇数)
小张来了2次(偶数)
小李来了1次(奇数)
2. 第二步:找一个区分特征
就像找一个能区分学生的特征,比如:
- 小红和小李一个戴眼镜,一个不戴眼镜
- 这就相当于找到了最右边的1
// 代码表示
int xor = 0;
for (int num : arr) {
xor ^= num; // 相当于把所有学生的特征异或在一起
}
// 找到区分特征(最右边的1)
int rightOne = xor & (-xor);
3. 第三步:根据特征分组
戴眼镜的组:小红 小红 小红 小张 小张
不戴眼镜的组:小明 小明 小李
就像把学生分成两组:
- 一组是戴眼镜的
- 一组是不戴眼镜的
// 代码中的分组
int x = 0;
for (int num : arr) {
if ((num & rightOne) != 0) { // 相当于判断是否戴眼镜
x ^= num; // 戴眼镜的组
}
}
4. 最后的结果
戴眼镜的组:小红 小红 小红 小张 小张
- 小张来了偶数次,异或后消失
- 剩下小红(第一个调皮的学生找到了!)
不戴眼镜的组:小明 小明 小李
- 小明来了偶数次,异或后消失
- 剩下小李(第二个调皮的学生找到了!)
完整的示例代码
public class StudentExample {
public static void findTwoNaughtyStudents(int[] students) {
System.out.println("班级学生名单:");
for (int student : students) {
System.out.print(student + " ");
}
System.out.println("\n");
// 1. 所有学生的特征异或在一起
int xor = 0;
for (int student : students) {
xor ^= student;
}
System.out.println("两个调皮学生的特征异或结果:" + xor);
// 2. 找到区分特征(戴眼镜还是不戴眼镜)
int rightOne = xor & (-xor);
System.out.println("找到的区分特征是:" + rightOne);
// 3. 根据特征分组
System.out.println("\n开始分组:");
int x = 0;
for (int student : students) {
if ((student & rightOne) != 0) {
System.out.println(student + " 戴眼镜");
x ^= student;
} else {
System.out.println(student + " 不戴眼镜");
}
}
// 4. 得到两个调皮的学生
int y = xor ^ x;
System.out.println("\n找到两个调皮的学生:");
System.out.println("第一个学生:" + x);
System.out.println("第二个学生:" + y);
}
public static void main(String[] args) {
int[] students = {2, 2, 3, 3, 3, 4, 4, 5};
findTwoNaughtyStudents(students);
}
}
这样理解是不是更清晰了?
- 最右边的1就像是一个区分特征(比如戴眼镜)
- 通过这个特征可以把学生分成两组
- 在每组中,乖的学生(出现偶数次)会互相抵消
- 最后在每组中剩下的就是调皮的学生
这就是为什么我们需要找到最右边的1,它帮助我们找到一个可以区分两个目标的特征!
9.为什么 y = xor ^ x 可以得到另一个学生?
让我用更直观的方式解释为什么 y = xor ^ x 可以得到另一个学生。
1. 先复习异或的性质
- A ^ A = 0(相同的数异或等于0)
- A ^ 0 = A(任何数和0异或等于它自己)
- A ^ B ^ A = B(一个数异或另一个数两次,等于没异或)
2. 举个具体例子
假设数组是:[2, 2, 3, 3, 3, 4, 4, 5]
其中3和5是我们要找的两个数(出现奇数次的数)
步骤1: 计算xor(所有数的异或)
xor = 2^2^3^3^3^4^4^5
= 0^3^0^5 (相同的数异或为0)
= 3^5 (最终xor等于两个奇数次数的异或)
步骤2: 分组计算x
假设3在戴眼镜组,5在不戴眼镜组
x = 3 (戴眼镜组所有数的异或结果)
步骤3: 计算y
y = xor ^ x
= (3^5) ^ 3
= 5 (根据异或性质,3^3=0,所以剩下5)
3. 用图解释
初始状态:
xor = 3 ^ 5(两个我们要找的数的异或结果)
找到x后:
x = 3(第一个数)
求y:
y = xor ^ x
= (3 ^ 5) ^ 3
┌─────┘ └──┐
│ │
└──→ 3^3=0 5
最终得到:y = 5
4. 代码演示
public class XORExplanation {
public static void explain(int[] arr) {
// 1. 得到xor
int xor = 0;
for (int num : arr) {
xor ^= num;
}
System.out.println("xor(两个奇数次数的异或) = " + xor);
// 2. 分组得到x
int rightOne = xor & (-xor);
int x = 0;
for (int num : arr) {
if ((num & rightOne) != 0) {
x ^= num;
}
}
System.out.println("x(第一组异或结果) = " + x);
// 3. 通过xor^x得到y
int y = xor ^ x;
System.out.println("y(第二组结果) = " + y);
// 验证
System.out.println("\n验证:");
System.out.println("x^y = " + (x ^ y));
System.out.println("xor = " + xor);
}
public static void main(String[] args) {
int[] arr = {2, 2, 3, 3, 3, 4, 4, 5};
explain(arr);
}
}
5. 为什么这样做有效?
-
xor包含了两个奇数次数的异或结果(设为a和b)xor = a ^ b
-
通过分组,我们找到了其中一个数(设为a)
x = a
-
要得到另一个数(b),我们只需要:
y = xor ^ xy = (a ^ b) ^ ay = b(因为 a^a=0)
这就像是:
- 你知道两个数的异或结果(xor)
- 你找到了其中一个数(x)
- 用这个数去异或原来的异或结果,就能得到另一个数(y)
就像解方程:
- 知道 a ^ b = xor
- 已经找到 a = x
- 那么 b = xor ^ a 就是另一个数
这就是为什么 y = xor ^ x 能够得到另一个出现奇数次的数!
3. 常见面试题示例
public class BitOperationExamples {
// 1. 交换两个数
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
// 2. 判断是否是2的幂
public static boolean isPowerOfTwo(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
// 3. 计算二进制中1的个数
public static int countOnes(int n) {
int count = 0;
while (n != 0) {
n = n & (n - 1); // 消除最右边的1
count++;
}
return count;
}
}
为什么可以判是否是2的幂
让我详细解释为什么 n > 0 && (n & (n - 1)) == 0 可以判断一个数是否是2的幂。
1. 先看2的幂的二进制特点
2⁰ = 1 = 0001
2¹ = 2 = 0010
2² = 4 = 0100
2³ = 8 = 1000
2⁴ = 16 = 10000
观察:2的幂的二进制表示中有且仅有一个1。
2. 图解 n & (n-1) 的过程
public class PowerOfTwo {
public static void explain(int n) {
System.out.println("数字: " + n);
System.out.println("二进制: " + Integer.toBinaryString(n));
System.out.println("n-1的二进制: " + Integer.toBinaryString(n-1));
System.out.println("n & (n-1): " + Integer.toBinaryString(n & (n-1)));
System.out.println("是否是2的幂: " + (n > 0 && (n & (n-1)) == 0));
System.out.println("--------------");
}
public static void main(String[] args) {
// 测试2的幂
explain(8); // 2的3次方
// 测试非2的幂
explain(6);
}
}
例子1:n = 8(2的幂)
n = 8 = 1000
n-1 = 7 = 0111
n & (n-1) = 1000 & 0111 = 0000 = 0
例子2:n = 6(不是2的幂)
n = 6 = 0110
n-1 = 5 = 0101
n & (n-1) = 0110 & 0101 = 0100 ≠ 0
3. 为什么这样可以判断2的幂?
-
对于2的幂:
- 二进制表示中只有一个1
- n-1 会把这个1变成0,右边都变成1
- 所以 n & (n-1) 一定等于0
-
对于非2的幂:
- 二进制表示中至少有两个1
- n-1 只会影响最右边的1及其右边的位
- 所以 n & (n-1) 一定不等于0
4. 更多示例
public class PowerOfTwoExamples {
public static void checkNumber(int n) {
System.out.println("\n检查数字: " + n);
System.out.println("二进制表示: " + Integer.toBinaryString(n));
System.out.println("n-1的二进制: " + Integer.toBinaryString(n-1));
System.out.println("n & (n-1): " + (n & (n-1)));
System.out.println("是2的幂吗? " + (n > 0 && (n & (n-1)) == 0));
}
public static void main(String[] args) {
// 测试2的幂
checkNumber(1); // 2⁰
checkNumber(2); // 2¹
checkNumber(4); // 2²
checkNumber(8); // 2³
checkNumber(16); // 2⁴
// 测试非2的幂
checkNumber(6);
checkNumber(10);
checkNumber(15);
}
}
输出示例:
检查数字: 8
二进制表示: 1000
n-1的二进制: 0111
n & (n-1): 0
是2的幂吗? true
检查数字: 6
二进制表示: 110
n-1的二进制: 101
n & (n-1): 4
是2的幂吗? false
5. 生动的比喻
想象一个场景:
- 2的幂就像一个队伍中只有一个人站着,其他人都蹲着
- n-1 相当于让这个站着的人蹲下,他后面的人都站起来
- n & (n-1) 就是看原来站着的人和现在站着的人重叠的部分
- 如果是2的幂,就不会有重叠(结果为0)
- 如果不是2的幂,一定会有重叠(结果不为0)
6. 实际应用
public class PowerOfTwoUtils {
// 判断是否是2的幂
public static boolean isPowerOfTwo(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
// 获取大于等于n的最小2的幂
public static int nextPowerOfTwo(int n) {
if (n <= 0) return 1;
if (isPowerOfTwo(n)) return n;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
return n + 1;
}
}
这个技巧在很多场景都很有用:
- 判断数字是否是2的幂
- 内存分配(常用2的幂作为块大小)
- 散列表的大小设计
- 位运算优化
记住:n & (n-1) 的本质是消除二进制中最右边的1,所以对于只有一个1的数(2的幂),结果一定是0!
如何判断二进制中的1的个数
让我用直观的方式解释如何计算二进制中1的个数(也叫汉明重量)。
1. 最简单的方法(逐位检查)
public class CountOnes {
public static int countOnes1(int n) {
int count = 0;
// 检查每一位
while (n != 0) {
count += n & 1; // 判断最后一位是否为1
n >>>= 1; // 右移一位
}
return count;
}
public static void explain1(int n) {
System.out.println("数字: " + n);
System.out.println("二进制: " + Integer.toBinaryString(n));
System.out.println("逐位检查过程:");
int temp = n;
int pos = 0;
while (temp != 0) {
System.out.println("第" + pos + "位: " + (temp & 1));
temp >>>= 1;
pos++;
}
System.out.println("1的个数: " + countOnes1(n));
System.out.println("-------------");
}
}
例如,计算7的二进制中1的个数:
7的二进制:111
检查最后一位:111 & 1 = 1 count = 1
右移一位: 11 & 1 = 1 count = 2
右移一位: 1 & 1 = 1 count = 3
2. 巧妙的方法(消除最右边的1)
public class CountOnes {
public static int countOnes2(int n) {
int count = 0;
while (n != 0) {
n = n & (n - 1); // 消除最右边的1
count++;
}
return count;
}
public static void explain2(int n) {
System.out.println("数字: " + n);
System.out.println("二进制: " + Integer.toBinaryString(n));
System.out.println("消除最右边的1的过程:");
int temp = n;
while (temp != 0) {
System.out.println("当前值: " + Integer.toBinaryString(temp));
temp = temp & (temp - 1);
}
System.out.println("1的个数: " + countOnes2(n));
System.out.println("-------------");
}
}
让我们以数字7为例,看看这个过程:
7的二进制: 111
7-1 = 6的二进制:110
第一次:111 & 110 = 110 count = 1
110的二进制: 110
110-1 = 5的二进制:101
第二次:110 & 101 = 100 count = 2
100的二进制: 100
100-1 = 3的二进制:011
第三次:100 & 011 = 000 count = 3
结束:得到3个1
3. 完整的示例代码
public class BinaryOnesCounter {
// 方法1:逐位检查
public static int countBits1(int n) {
int count = 0;
while (n != 0) {
count += n & 1;
n >>>= 1;
}
return count;
}
// 方法2:消除最右边的1
public static int countBits2(int n) {
int count = 0;
while (n != 0) {
n &= (n - 1);
count++;
}
return count;
}
public static void demonstrateCounting(int n) {
System.out.println("数字: " + n);
System.out.println("二进制表示: " + Integer.toBinaryString(n));
// 演示方法1
System.out.println("\n方法1 - 逐位检查过程:");
int temp1 = n;
int pos = 0;
while (temp1 != 0) {
if ((temp1 & 1) == 1) {
System.out.println("在位置 " + pos + " 发现了1");
}
temp1 >>>= 1;
pos++;
}
// 演示方法2
System.out.println("\n方法2 - 消除最右边的1的过程:");
int temp2 = n;
while (temp2 != 0) {
int next = temp2 & (temp2 - 1);
System.out.println(Integer.toBinaryString(temp2) +
" -> " + Integer.toBinaryString(next));
temp2 = next;
}
System.out.println("\n结果比较:");
System.out.println("方法1结果: " + countBits1(n));
System.out.println("方法2结果: " + countBits2(n));
System.out.println("-------------");
}
public static void main(String[] args) {
demonstrateCounting(7); // 111
demonstrateCounting(15); // 1111
demonstrateCounting(21); // 10101
}
}
4. 两种方法的比较
-
逐位检查法(方法1)
- 优点:直观,容易理解
- 缺点:需要检查所有位
- 时间复杂度:O(32) 对于int类型
-
消除最右边的1(方法2)
- 优点:只需要检查实际的1的个数
- 缺点:不太直观
- 时间复杂度:O(m) 其中m是1的个数
5. 实际应用
public class Applications {
// 判断是否是2的幂
public static boolean isPowerOfTwo(int n) {
return n > 0 && countBits2(n) == 1;
}
// 判断两个数的二进制中有多少位不同
public static int hammingDistance(int x, int y) {
return countBits2(x ^ y);
}
}
方法2(n & (n-1))是一个非常巧妙的位运算技巧,它可以快速消除二进制中最右边的1。这个技巧在很多位运算相关的问题中都很有用!
记忆口诀
1. 基本运算口诀
& (与):都1才1,有0就0
| (或):有1就1,都0才0
^ (异或):相同得0,不同得1
~ (取反):0变1,1变0
<< (左移):左边丢弃,右边补0
>> (右移):右边丢弃,左边补符号位
>>> (无符号右移):右边丢弃,左边补0
2. 实用技巧口诀
// 1. 判断奇偶
n & 1 = 1 (奇数)
n & 1 = 0 (偶数)
// 2. 交换两数
a = a ^ b
b = a ^ b
a = a ^ b
// 3. 取绝对值(x >> 31 得到符号位)
(x ^ (x >> 31)) - (x >> 31)
// 4. 快速乘除2
n << 1 (乘2)
n >> 1 (除2)
3. 记忆方法
- 与运算(&)记忆为"遇0则0"
1 & 1 = 1 // 都是1才为1
1 & 0 = 0 // 遇到0就是0
0 & 1 = 0 // 遇到0就是0
0 & 0 = 0 // 遇到0就是0
- 或运算(|)记忆为"遇1则1"
1 | 1 = 1 // 遇到1就是1
1 | 0 = 1 // 遇到1就是1
0 | 1 = 1 // 遇到1就是1
0 | 0 = 0 // 都是0才为0
- 异或运算(^)记忆为"相同为0,不同为1"
1 ^ 1 = 0 // 相同为0
1 ^ 0 = 1 // 不同为1
0 ^ 1 = 1 // 不同为1
0 ^ 0 = 0 // 相同为0
- 移位运算记忆为"左乘右除"
n << 1 相当于 n * 2
n >> 1 相当于 n / 2
4. 常用场景口诀
// 1. 获取最低位的1
lowbit = n & (-n)
// 2. 消除最低位的1
n = n & (n-1)
// 3. 判断是否是2的幂
isPowerOfTwo = (n & (n-1)) == 0
// 4. 获取第k位
bit = (n >> k) & 1
// 5. 设置第k位为1
n |= (1 << k)
// 6. 设置第k位为0
n &= ~(1 << k)
// 7. 翻转第k位
n ^= (1 << k)
5. 实战速记表
场景 操作 记忆口诀
判断奇偶 n & 1 "末位1奇0偶"
乘除2 << 1 或 >> 1 "左乘右除"
交换两数 用异或 "异或三步换"
取绝对值 用符号位 "符号位反转"
获取最低位1 n & (-n) "自己与负"
消最低位1 n & (n-1) "自己与减"
判断2的幂 n & (n-1) == 0 "与减为零"
这样记忆:
- 先记住基本运算的特点
- 再记住常用场景的解决方案
- 最后通过实战来加深理解
比如要判断一个数是否是2的幂:
- 2的幂的二进制特点是:只有一个1
- 减1后,这个1变成0,后面都变成1
- 所以与原数相与必然为0
通过这种方式,把抽象的位运算变成具体的场景,更容易记住和理解。