左哥算法 - 位运算

156 阅读21分钟

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 位运算详解
  1. 按位与(&)
  0101 (5)
& 0011 (3)
-------
  0001 (1)

规则:两个位都是1才得1,否则得0
  1. 按位或(|)
  0101 (5)
| 0011 (3)
-------
  0111 (7)

规则:两个位都是0才得0,否则得1
  1. 按位异或(^)
  0101 (5)
^ 0011 (3)
-------
  0110 (6)

规则:两个位相同得0,不同得1
  1. 左移(<<)
5 << 1 的过程:
0101 (5)
1010 (10)

规则:所有位向左移动,右边补0
每左移一位相当于乘2
  1. 右移(>>)
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. 任何数的二进制表示中:

    • 奇数的最后一位必定是1
    • 偶数的最后一位必定是0
  2. 具体示例:

5(奇数) = 0101
    &
1      = 0001
--------------
结果    = 0001 (等于1)

8(偶数) = 1000
    &
1      = 0001
--------------
结果    = 0000 (等于0)

15(奇数)= 1111
    &
1      = 0001
--------------
结果    = 0001 (等于1)
2. 为什么这样做有效?
  1. 与运算(&)的特点:

    • 只有两个位都是1时,结果才是1
    • 其他情况结果都是0
  2. 数字1的特点:

    • 二进制表示为:0001
    • 只有最后一位是1
  3. 当任何数与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. 与传统方法比较
  1. 传统方法(取模):
boolean isOdd = n % 2 == 1;
  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. 记住奇数的二进制最后一位一定是1
  2. 记住偶数的二进制最后一位一定是0
  3. 与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. 在分组问题中

    • 最右边的1代表了两个不同数字的一个区分位
    • 可以用这个位置把数字分成两组
  2. 在判断2的幂时

    • 2的幂的二进制中只有一个1
    • 获取最右边的1后应该等于原数字
  3. 在位操作中

    • 最右边的1通常是最容易处理的位
    • 可以用来逐位处理二进制数
6. 实用技巧
  1. 可以用 n & (-n) 快速获取最右边的1
  2. 结果一定是2的某个次幂
  3. 可以用来判断一个位置是否为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. 原理解释

    • 在二进制中,负数是通过"取反加一"得到的
    • 这个过程会使得最右边的1保持不变,而其右边的0变成1
    • 其左边的所有位会变成相反数
  2. 举例分析数字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. 注意事项
  1. 边界情况

    • 当n为0时,结果为0
    • 当n为负数时,也能正常工作
  2. 性质

    • 结果总是2的幂
    • 结果对应原数字中最右边的1的位置
  3. 常见用途

    • 分离最右边的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. 为什么这样做有效?
  1. xor 包含了两个奇数次数的异或结果(设为a和b)

    • xor = a ^ b
  2. 通过分组,我们找到了其中一个数(设为a)

    • x = a
  3. 要得到另一个数(b),我们只需要:

    • y = xor ^ x
    • y = (a ^ b) ^ a
    • y = 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 = 01000
3. 为什么这样可以判断2的幂?
  1. 对于2的幂

    • 二进制表示中只有一个1
    • n-1 会把这个1变成0,右边都变成1
    • 所以 n & (n-1) 一定等于0
  2. 对于非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;
    }
}

这个技巧在很多场景都很有用:

  1. 判断数字是否是2的幂
  2. 内存分配(常用2的幂作为块大小)
  3. 散列表的大小设计
  4. 位运算优化

记住: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. 逐位检查法(方法1)

    • 优点:直观,容易理解
    • 缺点:需要检查所有位
    • 时间复杂度:O(32) 对于int类型
  2. 消除最右边的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. 记忆方法

  1. 与运算(&)记忆为"遇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
1 | 0 = 1    // 遇到1就是1
0 | 1 = 1    // 遇到1就是1
0 | 0 = 0    // 都是0才为0
  1. 异或运算(^)记忆为"相同为0,不同为1"
1 ^ 1 = 0    // 相同为0
1 ^ 0 = 1    // 不同为1
0 ^ 1 = 1    // 不同为1
0 ^ 0 = 0    // 相同为0
  1. 移位运算记忆为"左乘右除"
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        "与减为零"

这样记忆:

  1. 先记住基本运算的特点
  2. 再记住常用场景的解决方案
  3. 最后通过实战来加深理解

比如要判断一个数是否是2的幂:

  • 2的幂的二进制特点是:只有一个1
  • 减1后,这个1变成0,后面都变成1
  • 所以与原数相与必然为0

通过这种方式,把抽象的位运算变成具体的场景,更容易记住和理解。