i & (i-1) 位运算

105 阅读10分钟

i & (i-1) 位运算技巧完全指南

学习日期:2025年11月7日
主题:位运算中最经典的技巧之一


📌 核心原理

基本效果

i & (i-1) 会清除 i 二进制表示中最右边的 1

原理解析

当一个数字减1时,从二进制角度会发生:

情况1:末位是1

7 = 0111
6 = 0110  ← 只有最后一位从10

情况2:末位是0(有连续的0)

12 = 1100  ← 最右边的1
11 = 1011  ← 这个1变成0,后面所有0变成1

规律: i-1 会把最右边的1变成0,并把这个1右边的所有0变成1

位运算效果

i     = xxxx...x1000  (最右边的1,后面跟着若干个0)
i-1   = xxxx...x0111  (这个10,后面的0都变1)
i&(i-1)= xxxx...x0000  (AND运算,只保留左边相同的部分)

结果:去掉了最右边的1!


💡 具体示例

示例1:数字 6

i     = 6  = 0110 (二进制)
i-1   = 5  = 0101 (二进制)
i&(i-1)= 4  = 0100 (二进制)
           ↑ 最右边的1被清除了

示例2:数字 12

i     = 12 = 1100 (二进制)
i-1   = 11 = 1011 (二进制)
i&(i-1)= 8  = 1000 (二进制)
            ↑ 最右边的1被清除了

示例3:数字 7

i     = 7  = 0111 (二进制)
i-1   = 6  = 0110 (二进制)
i&(i-1)= 6  = 0110 (二进制)
            ↑ 最右边的1被清除了

🎯 LeetCode算法题应用

1. 判断2的幂 ⭐⭐⭐

LeetCode 231 - Power of Two

public boolean isPowerOfTwo(int n) {
    // 2的幂只有1个1:1, 10, 100, 1000...
    // n & (n-1) 会去掉唯一的1,结果为0
    return n > 0 && (n & (n - 1)) == 0;
}

// 示例:
// 4 = 0100, 4&3 = 0100&0011 = 0000 ✓
// 8 = 1000, 8&7 = 1000&0111 = 0000 ✓
// 6 = 0110, 6&5 = 0110&0101 = 0100 ✗ (不是0)

原理: 2的幂在二进制中只有1个1,去掉后就是0


2. 统计二进制中1的个数 ⭐⭐⭐

LeetCode 191 - Number of 1 Bits (汉明重量)

public int hammingWeight(int n) {
    int count = 0;
    while (n != 0) {
        n &= (n - 1);  // 每次去掉最右边的1
        count++;
    }
    return count;
}

// 示例:
// n = 11 = 1011
// 1011 & 1010 = 1010 (去掉1个1, count=1)
// 1010 & 1001 = 1000 (去掉1个1, count=2)
// 1000 & 0111 = 0000 (去掉1个1, count=3)

3. 前n个数字二进制中1的个数 ⭐⭐⭐

LeetCode 338 - Counting Bits

public int[] countBits(int n) {
    int[] result = new int[n + 1];
    for (int i = 1; i <= n; i++) {
        result[i] = result[i & (i - 1)] + 1;
    }
    return result;
}

核心思想:

  • i & (i-1) 得到的数字,比 i 少一个1
  • 所以 i 的1的个数 = i & (i-1) 的1的个数 + 1
  • 这是动态规划,利用之前计算好的结果

推导过程示例(n=7):

i=0: 0000, result[0] = 0 (基础情况)

i=1: 0001, 1&0=0, result[1] = result[0] + 1 = 1
i=2: 0010, 2&1=0, result[2] = result[0] + 1 = 1
i=3: 0011, 3&2=2, result[3] = result[2] + 1 = 2
i=4: 0100, 4&3=0, result[4] = result[0] + 1 = 1
i=5: 0101, 5&4=4, result[5] = result[4] + 1 = 2
i=6: 0110, 6&5=4, result[6] = result[4] + 1 = 2
i=7: 0111, 7&6=6, result[7] = result[6] + 1 = 3

关键理解:

  • i=6 时,6 & 5 = 4
  • 6 (0110) 有2个1,4 (0100) 有1个1
  • 关系:6的1个数 = 4的1个数 + 1 → 2 = 1 + 1 ✓

4. 判断4的幂 ⭐⭐

LeetCode 342 - Power of Four

public boolean isPowerOfFour(int n) {
    // 首先必须是2的幂(只有1个1)
    // 其次,这个1必须在奇数位(1, 4, 16, 64...)
    // 0x55555555 = 0101 0101 0101 0101 0101 0101 0101 0101
    return n > 0 && (n & (n - 1)) == 0 && (n & 0x55555555) != 0;
}

// 4 = 0100 ✓ (第3位,奇数位)
// 16 = 0001 0000 ✓ (第5位,奇数位)
// 8 = 1000 ✗ (第4位,偶数位)

5. 汉明距离 ⭐⭐

LeetCode 461 - Hamming Distance

public int hammingDistance(int x, int y) {
    int xor = x ^ y;  // 不同的位是1
    int count = 0;
    while (xor != 0) {
        xor &= (xor - 1);  // 去掉一个1
        count++;
    }
    return count;
}

6. 二进制手表 ⭐

LeetCode 401 - Binary Watch

public List<String> readBinaryWatch(int turnedOn) {
    List<String> result = new ArrayList<>();
    for (int h = 0; h < 12; h++) {
        for (int m = 0; m < 60; m++) {
            // 统计h和m的二进制中1的个数
            if (countOnes(h) + countOnes(m) == turnedOn) {
                result.add(String.format("%d:%02d", h, m));
            }
        }
    }
    return result;
}

private int countOnes(int n) {
    int count = 0;
    while (n != 0) {
        n &= (n - 1);
        count++;
    }
    return count;
}

📱 Android开发实战应用

1. Bitmap处理(极其重要!)⭐⭐⭐⭐⭐

BitmapFactory要求inSampleSize必须是2的幂

public class BitmapHelper {
    /**
     * 计算合适的inSampleSize
     * BitmapFactory要求inSampleSize必须是2的幂
     */
    public static int calculateInSampleSize(
            BitmapFactory.Options options, 
            int reqWidth, 
            int reqHeight) {
        
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }
        
        // 验证是否是2的幂
        if (!isPowerOfTwo(inSampleSize)) {
            inSampleSize = nextPowerOfTwo(inSampleSize);
        }
        
        return inSampleSize;
    }
    
    // 判断是否是2的幂
    public static boolean isPowerOfTwo(int n) {
        return n > 0 && (n & (n - 1)) == 0;
    }
    
    // 找到下一个2的幂
    public static int nextPowerOfTwo(int n) {
        n--;
        n |= n >> 1;
        n |= n >> 2;
        n |= n >> 4;
        n |= n >> 8;
        n |= n >> 16;
        return n + 1;
    }
}

// 使用示例
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.large_image, options);
options.inSampleSize = BitmapHelper.calculateInSampleSize(options, 100, 100);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.large_image, options);

2. LruCache缓存优化 ⭐⭐⭐⭐⭐

缓存大小设为2的幂,性能更好

public class CacheHelper {
    /**
     * LruCache的大小通常设置为2的幂次,性能更好
     */
    public static int calculateCacheSize(int maxMemory) {
        int cacheSize = maxMemory / 8;  // 使用1/8的内存
        
        // 向下舍入到最近的2的幂
        return roundDownToPowerOfTwo(cacheSize);
    }
    
    // 向下舍入到2的幂
    private static int roundDownToPowerOfTwo(int n) {
        if (n <= 0) return 1;
        if ((n & (n - 1)) == 0) return n;  // 已经是2的幂
        
        // 找到最高位的1
        int highestBit = 0;
        while (n > 0) {
            highestBit = n;
            n &= (n - 1);  // 去掉最低的1
        }
        return highestBit;
    }
}

// 实际使用
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = CacheHelper.calculateCacheSize(maxMemory);
LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
        return bitmap.getByteCount() / 1024;
    }
};

3. 权限管理 ⭐⭐⭐⭐

public class PermissionChecker {
    public static final int READ = 1;      // 0001
    public static final int WRITE = 2;     // 0010
    public static final int EXECUTE = 4;   // 0100
    public static final int DELETE = 8;    // 1000
    
    // 检查是否只有一个权限(在某些场景需要单一权限)
    public static boolean hasSinglePermission(int permissions) {
        // 如果只有一个1,去掉后就是0
        return permissions > 0 && (permissions & (permissions - 1)) == 0;
    }
    
    // 统计权限数量
    public static int countPermissions(int permissions) {
        int count = 0;
        while (permissions != 0) {
            permissions &= (permissions - 1);
            count++;
        }
        return count;
    }
}

// 使用示例
int userPerm = READ | WRITE;  // 0011, 有2个权限
boolean single = PermissionChecker.hasSinglePermission(userPerm);  // false
int count = PermissionChecker.countPermissions(userPerm);  // 2

4. SharedPreferences功能标志 ⭐⭐⭐⭐

public class FeatureFlags {
    private static final String PREF_NAME = "feature_flags";
    
    public static final int FEATURE_DARK_MODE = 1;        // 0001
    public static final int FEATURE_NOTIFICATION = 2;     // 0010
    public static final int FEATURE_AUTO_SYNC = 4;        // 0100
    public static final int FEATURE_ANALYTICS = 8;        // 1000
    
    private SharedPreferences prefs;
    
    public FeatureFlags(Context context) {
        prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
    }
    
    // 获取启用的功能数量
    public int getEnabledFeatureCount() {
        int flags = prefs.getInt("enabled_features", 0);
        int count = 0;
        while (flags != 0) {
            flags &= (flags - 1);
            count++;
        }
        return count;
    }
    
    // 检查是否只启用了一个功能
    public boolean hasSingleFeatureEnabled() {
        int flags = prefs.getInt("enabled_features", 0);
        return flags > 0 && (flags & (flags - 1)) == 0;
    }
}

5. 自定义View状态管理 ⭐⭐⭐⭐

public class CustomButton extends View {
    // 状态标志
    private static final int STATE_NORMAL = 0;
    private static final int STATE_PRESSED = 1 << 0;  // 1
    private static final int STATE_FOCUSED = 1 << 1;  // 2
    private static final int STATE_DISABLED = 1 << 2; // 4
    private static final int STATE_SELECTED = 1 << 3; // 8
    
    private int currentState = STATE_NORMAL;
    
    // 检查是否只有一个状态激活
    public boolean isSingleStateActive() {
        return currentState > 0 && (currentState & (currentState - 1)) == 0;
    }
    
    // 统计激活的状态数
    public int getActiveStateCount() {
        int count = 0;
        int temp = currentState;
        while (temp != 0) {
            temp &= (temp - 1);
            count++;
        }
        return count;
    }
    
    // 根据状态数量决定绘制逻辑
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        if (isSingleStateActive()) {
            // 单一状态,简单绘制
            drawSimpleState(canvas);
        } else {
            // 复合状态,复杂绘制
            drawComplexState(canvas);
        }
    }
}

6. 网络状态检查 ⭐⭐⭐

public class NetworkHelper {
    public static final int NETWORK_WIFI = 1;      // 0001
    public static final int NETWORK_MOBILE = 2;    // 0010
    public static final int NETWORK_ETHERNET = 4;  // 0100
    public static final int NETWORK_VPN = 8;       // 1000
    
    // 检查是否只连接了一种网络类型
    public static boolean isSingleNetworkType(int networkTypes) {
        return networkTypes > 0 && (networkTypes & (networkTypes - 1)) == 0;
    }
    
    // 统计当前连接的网络类型数量
    public static int countNetworkTypes(int networkTypes) {
        int count = 0;
        while (networkTypes != 0) {
            networkTypes &= (networkTypes - 1);
            count++;
        }
        return count;
    }
}

🔍 如何发现这类规律?

通用方法论

1. 从暴力解法开始观察
// 先写最简单的代码
for (int i = 0; i <= n; i++) {
    int count = 0;
    int num = i;
    while (num > 0) {
        count += num & 1;
        num >>= 1;
    }
    result[i] = count;
}

// 列出具体的值,找规律
i    二进制   1的个数
0    0000      0
1    0001      1
2    0010      1
3    0011      2
4    0100      1
5    0101      2
2. 观察数字之间的关系
  • "相邻的数字有什么关系?"
  • "偶数和奇数有什么规律?"
  • "能否用前面的结果推导后面的?"
3. 尝试常见位运算

常见的位运算就那么几种:

  • i & (i-1) ← 去掉最右边的1
  • i & (-i) ← 只保留最右边的1
  • i >> 1 ← 除以2
  • i & 1 ← 判断奇偶
4. 从已知技巧类推
// 已知技巧:判断一个数有多少个1
while (num > 0) {
    num &= (num - 1);  // 每次去掉一个1
    count++;
}

// 类推:
// 既然 i & (i-1) 可以去掉一个1,那么:
// i 的1个数 = [i & (i-1)] 的1个数 + 1
5. 问自己问题
  • ✅ 相邻数字有什么关系?
  • ✅ 能否用前面的结果算后面的?
  • ✅ 是否有周期性/对称性?
  • ✅ 除以2、乘以2有什么规律?
  • ✅ 奇数偶数有什么不同?

📊 应用场景总结

LeetCode算法题

题目难度应用
LC 231 - Power of Two简单判断2的幂
LC 191 - Number of 1 Bits简单统计1的个数
LC 338 - Counting Bits简单动态规划
LC 342 - Power of Four简单判断4的幂
LC 461 - Hamming Distance简单汉明距离
LC 401 - Binary Watch简单二进制手表

Android开发场景

场景频率重要性应用
Bitmap处理⭐⭐⭐⭐⭐极高inSampleSize必须是2的幂
LruCache⭐⭐⭐⭐⭐极高缓存大小优化
权限管理⭐⭐⭐⭐检查单一权限
标志位管理⭐⭐⭐⭐功能开关、状态管理
自定义View⭐⭐⭐⭐状态管理
网络检查⭐⭐⭐网络类型统计

💡 核心记忆点

1. 基本效果

i & (i-1) → 去掉最右边的1

2. 判断2的幂

(n & (n - 1)) == 0  // 且 n > 0

3. 统计1的个数

while (n != 0) {
    n &= (n - 1);
    count++;
}

4. 动态规划递推

result[i] = result[i & (i-1)] + 1;

5. 向下舍入到2的幂

while (n > 0) {
    highestBit = n;
    n &= (n - 1);
}
return highestBit;

🎯 最佳实践

Android开发建议

  1. 缓存大小都用2的幂 → 性能更好,内存对齐
  2. 标志位用位运算 → 节省内存,操作快速
  3. 图片缩放必须2的幂 → Android系统要求
  4. 能用位运算就用 → 比算术运算快

学习建议

  1. 大量练习 → 见多识广
  2. 系统观察 → 有方法地看
  3. 勇于尝试 → 试错不可怕
  4. 积累经验 → 记住常见技巧

📝 记忆口诀

i & (i-1) 去右边的1
判断2的幂,统计1个数
动态规划也常用
位运算题见它就不慌

Android开发很实用
Bitmap缓存都要它
标志管理更方便
性能优化少不了

✅ 推荐练习顺序

算法题

  1. LC 231 - Power of Two (最简单,入门)
  2. LC 191 - Number of 1 Bits (经典应用)
  3. LC 338 - Counting Bits (DP结合)
  4. LC 342 - Power of Four (进阶)
  5. LC 461 - Hamming Distance (综合应用)

实战项目

  1. 写一个图片加载工具类,用2的幂优化
  2. 实现一个权限管理系统
  3. 自定义View的状态管理
  4. 功能开关的标志位管理

🔗 相关知识点

  • 位运算基础:&, |, ^, ~, <<, >>
  • 动态规划
  • 二进制数学
  • Android性能优化
  • 内存管理

最后提醒: i & (i-1) 是位运算中最经典的技巧之一,务必掌握!在面试和实际开发中都会经常用到。


整理完成 - 2025年11月7日