汉明重量算法深度解析

64 阅读10分钟

汉明重量算法深度解析

会话时间: 2025-11-12 02:48:56 UTC
主题: Hamming Weight (popcount) 算法原理与实现对比


📚 目录

  1. 算法概述
  2. 基础知识
  3. 算法原理详解
  4. 核心疑问解答
  5. 算法对比分析
  6. 实际应用建议
  7. 总结与复盘

算法概述

问题定义

计算一个整数的二进制表示中 1 的个数(Hamming Weight / Population Count)

算法代码

public int hammingWeight(int n) {
    n = n - ((n >>> 1) & 0x55555555);
    n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
    n = (n + (n >>> 4)) & 0x0f0f0f0f;
    n = n + (n >>> 8);
    n = n + (n >>> 16);
    return n & 0x3f;
}

基础知识

位移操作符区别

>> 算术右移
  • 右移时左边补符号位(正数补0,负数补1)
  • 保持数的正负性
int a = -8;  // 11111111 11111111 11111111 11111000
a >> 2;      // 11111111 11111111 11111111 11111110 (补1)
>>> 逻辑右移
  • 右移时左边永远补0
  • 不管正负数都补0
int a = -8;  // 11111111 11111111 11111111 11111000
a >>> 2;     // 00111111 11111111 11111111 11111110 (补0)

为什么使用 >>>
因为只关心位的模式,不关心正负号。


算法原理详解

核心思想:分治法(Divide and Conquer)

将32位分组,逐层合并统计:

32位 → 16组(2位) → 8组(4位) → 4组(8位) → 2组(16位) → 1组(32位)

第1步:每2位一组,统计1的个数

n = n - ((n >>> 1) & 0x55555555);
魔法数字
0x55555555 = 0101 0101 0101 0101 0101 0101 0101 0101
数学原理

对于任意2位 ab(a是高位,b是低位):

原始值 = a×2 + b  (二进制位值公式)
n >>> 1 = a        (右移1位得到高位)
n - (n>>>1) = (a×2 + b) - a = a + b
示例
11 → 原值=3, 右移=1, 相减=2 ✅ (21)
10 → 原值=2, 右移=1, 相减=1 ✅ (11)
01 → 原值=1, 右移=0, 相减=1 ✅ (11)
00 → 原值=0, 右移=0, 相减=0 ✅ (01)
完整过程(8位示例)
原始:    11  01  10  01
步骤1:   01  10  01  10  (n >>> 1)
掩码后:  01  00  01  00  ((n>>>1) & 0x55)
相减:    10  01  01  01  ✅ 每2位的1的个数

第2步:每4位一组,累加

n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
魔法数字
0x33333333 = 0011 0011 0011 0011 0011 0011 0011 0011
作用

将相邻的2位组分离后相加,避免进位干扰。

示例
当前:  10  01  01  01

分离低2位组:
  10 01 01 01
& 00 11 00 11
= 00 01 00 01

分离高2位组 (右移2位后):
  10 01 01 01 >>> 2 = 00 10 00 10
& 00 11 00 11
= 00 10 00 10

相加:
  00 01 00 01
+ 00 10 00 10
= 00 11 00 11  (前4位有3个1,后4位有3个1)

第3步:每8位一组

n = (n + (n >>> 4)) & 0x0f0f0f0f;
魔法数字
0x0f0f0f0f = 0000 1111 0000 1111 0000 1111 0000 1111
示例
当前: 0011 0011

右移4位: 0000 0011
相加:     0011 0110
掩码:     0000 0110  (6个1) 

第4-5步:继续合并至32位

n = n + (n >>> 8);   // 16位一组
n = n + (n >>> 16);  // 32位一组
为什么不需要掩码?

关键洞察

  • 32位整数最多有32个1
  • 32 = 0b100000,只需6位表示
  • 最终结果不会溢出6位范围
  • 高位的"垃圾数据"不影响低位正确性
完整过程
第3步后(4组8位计数):
[0000 0011] [0000 0010] [0000 0011] [0000 0010]

第4步 (n + n>>>8):
低16位 = 组0+组1的和
高16位 = 组2+组3的和

第5步 (n + n>>>16):
低位 = 所有组的总和 

第6步:提取结果

return n & 0x3f;  // 0x3f = 0011 1111 (63)
  • 32位最多32个1,需要6位存储(2^6 = 64)
  • 只取低6位,丢弃所有高位数据

核心疑问解答

Q1: 为什么不能直接累加?

问题:第1步得到每2位的计数后,为什么不直接全部相加?

回答

❌ 错误方式1:当作普通数字
10 01 01 01 (二进制) = 149 (十进制) ❌ 完全错误
❌ 错误方式2:循环提取
int sum = 0;
sum += (n & 0b11);        // 提取每2位
sum += ((n >> 2) & 0b11);
// ... 重复16次

问题:回到了循环,失去了O(1)的优势!

✅ 正确方式:层层合并

原因

  1. 避免进位冲突:直接加会导致进位串到隔壁组
  2. 并行处理:每步同时处理所有组,不需要循环
  3. 保证独立性:每组有独立空间,不会互相干扰

Q2: 为什么要合并到32位?

回答:确实合并到了32位,步骤如下:

步骤1: 2位  → 16组独立计数
步骤2: 4位  → 8组独立计数
步骤3: 8位  → 4组独立计数
步骤4: 16位 → 2组独立计数
步骤5: 32位 → 1组总计数 ✅
步骤6: 提取低6位

为什么后面看起来简单了?

步骤需要掩码?原因
1-3步✅ 需要防止组间进位干扰
4-5步❌ 不需要结果≤32,只需最后提取低6位

Q3: 为什么后面不需要掩码?

核心原因:最终结果有界(≤32),只需6位。

每步的最大值分析
步骤组数每组最大值需要位数
第1步16组22位
第2步8组43位
第3步4组84位
第4步2组165位
第5步1组326位

结论

  • 即使高位有"垃圾数据"
  • 即使有重复累加
  • 最后 & 0x3f 提取低6位,保证结果正确

算法对比分析

算法1: Brian Kernighan 算法

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

原理n & (n-1) 消除最右边的1

n     = 1011000
n-1   = 1010111
n&(n-1) = 1010000  ✅ 最右边的1被消除

算法2: 分治算法(本文讨论的)

public int hammingWeight(int n) {
    n = n - ((n >>> 1) & 0x55555555);
    n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
    n = (n + (n >>> 4)) & 0x0f0f0f0f;
    n = n + (n >>> 8);
    n = n + (n >>> 16);
    return n & 0x3f;
}

复杂度对比

算法时间复杂度空间复杂度实际执行
n & (n-1)O(k)O(1)k次循环(k=1的个数)
分治算法O(1)O(1)固定5次运算

性能对比

输入数据n & (n-1)分治算法
0b00000001 (1个1)1次循环5次运算
0b10101010 (4个1)4次循环5次运算
0b11111111 (8个1)8次循环5次运算
0xFFFFFFFF (32个1)32次循环5次运算

结论

  • 1很少n & (n-1) 更快
  • 1很多:分治算法更稳定
  • 最坏情况:分治算法快6倍+

实际应用建议

🎯 日常开发(推荐95%场景)

// ✅ 直接使用标准库
int count = Integer.bitCount(n);

理由

  • JVM会优化成硬件指令(POPCNT)
  • 性能最优
  • 代码简洁

📝 学习/面试场景

优先掌握:n & (n-1)
int count = 0;
while (n != 0) {
    n &= n - 1;
    count++;
}

优势

  • ✅ 代码简洁(5行)
  • ✅ 容易理解和解释
  • ✅ 面试常考
  • ✅ 平均性能足够
进阶理解:分治算法

优势

  • ✅ 体现算法深度
  • ✅ 理解位操作精髓
  • ✅ 了解JDK实现原理

🚀 性能关键场景

选择分治算法的情况

  • 高性能计算(图像处理、密码学)
  • 需要固定时间(实时系统、防时序攻击)
  • 大量调用(百万级循环)
  • 底层库开发

算法选择流程图

需要统计1的个数?
    ↓
生产代码?
    ↓ 是
Integer.bitCount(n) ✅

    ↓ 否(学习/面试)
需要展示深度理解?
    ↓ 是
分治算法 ✅

    ↓ 否
n & (n-1) ✅(优先推荐)

总结与复盘

核心知识点

  1. 位操作基础

    • >>> 逻辑右移(无符号右移)
    • >> 算术右移(保留符号位)
  2. 算法思想

    • 分治法:将问题分解为子问题
    • 并行处理:每步同时处理所有组
    • 空间换时间:用固定运算代替循环
  3. 数学原理

    • 第1步:利用 (a×2+b) - a = a+b
    • 后续步骤:层层合并,保证不溢出
  4. 工程权衡

    • 代码可读性 vs 性能
    • 平均情况 vs 最坏情况
    • 标准库 vs 自实现

关键理解

✅ 为什么层层合并?
  • 避免进位冲突
  • 实现并行处理
  • 保证O(1)复杂度
✅ 为什么后面不需要掩码?
  • 结果有界(≤32)
  • 只需6位存储
  • 最后提取低位即可
✅ 实际开发选择?
// 99%的情况
Integer.bitCount(n);

// 学习推荐
n & (n-1);

// 性能极致
分治算法

延伸思考

  1. 硬件支持

    • x86: POPCNT 指令
    • ARM: CNT 指令
    • JVM会自动优化
  2. 应用场景

    • 位图索引(数据库)
    • 布隆过滤器
    • 棋盘游戏(位棋盘表示)
    • 网络协议(校验和计算)
  3. 相关算法

    • 找第一个1的位置:n & -n
    • 判断2的幂:n & (n-1) == 0
    • 翻转所有位:~n

学习收获

  • ✅ 理解了分治算法的位操作应用
  • ✅ 掌握了两种主流算法的优劣
  • ✅ 学会了根据场景选择合适算法
  • ✅ 理解了工程实践中的权衡

参考资料

  • JDK源码: java.lang.Integer.bitCount()
  • 算法名称:
    • Hamming Weight
    • Population Count (popcount)
    • Sideways Addition
  • 相关技术: SWAR (SIMD Within A Register)

文档生成时间: 2025-11-12
学习者: MaryDQ
版本: v1.0


附录:完整代码对比

方法1: Brian Kernighan

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

方法2: 分治算法

public int hammingWeight(int n) {
    n = n - ((n >>> 1) & 0x55555555);
    n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
    n = (n + (n >>> 4)) & 0x0f0f0f0f;
    n = n + (n >>> 8);
    n = n + (n >>> 16);
    return n & 0x3f;
}

方法3: 标准库(推荐)

public int hammingWeight(int n) {
    return Integer.bitCount(n);
}

END