统计二进制中1个数 – bitCount优化详解

int类型由32位二进制数组成,Java中Integer.bitCount(int i)算法统计二进制数中1的个数,本文依次介绍和分析其线性实现、对数优化和运算优化,欢迎评论交流和指出错误,为支持作者高质量产出请尊重作者的著作权利,转载请注明出处😊

线性实现

各位上的1和0恰好表示1的出现次数,统计各位和即可,即依次无符号右移取最低位计和,时间 \small O(n),空间 \small O(1)

public int bitCount(int i) {
    int cnt = 0;
    while(i != 0) {
        cnt += (i & 0x01);
        i >>>= 1;
    }
    return cnt;
}

对数优化

每轮两两合并高i位和低i位的统计次数到2i位中,时间 \small O(log_2n),空间 \small O(1)

bitcount

public int bitCount(int i) {
    i = i & 0x55555555 + ((i >>> 1) & 0x55555555);
    i = i & 0x33333333 + ((i >>> 2) & 0x33333333);
    i = i & 0x0f0f0f0f + ((i >>> 4) & 0x0f0f0f0f);
    i = i & 0x00ff00ff + ((i >>> 8) & 0x00ff00ff);
    i = i & 0x0000ffff + ((i >>>16) & 0x0000ffff);
    return i;
}

运算优化

JDK实现如下,基于对数优化过程分析图,详析运算优化点

public static int bitCount(int i) {
    // HD, Figure 5-2
    i = i - ((i >>> 1) & 0x55555555);
    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
    i = (i + (i >>> 4)) & 0x0f0f0f0f;
    i = i + (i >>> 8);
    i = i + (i >>> 16);
    return i & 0x3f;
}

1位组合并优化

两两合并相邻1位组,每个一位组范围为0(0)-1(1),表示1位组中1出现个数,合并结果为 0(00)-2(10),需要 两位二进制数储存累计结果。朴素的想法是取高1低+取1低位:取低1位的方法是 i & 0x55555555,取高1位对齐低1位的方法是(i >>> 1) & 0x55555555。取高1位后应清空原高1位,否则无效位参与运算,导致本2位组运算错误,甚至产生进位至左侧相邻2位组导致运算错误

[]:正确累计结果 ():未清空的无效位 {}:合并后的2i位组
合并前1位组:[1] [1] [1] [1] [1] [1] [1] [1]
原低位1位组:(1) [1] (1) [1] (1) [1] (1) [1]
原高位1位组:[1] (1) [1] (1) [1] (1) [1] (1)
右移高1位组:(0) [1] (1) [1] (1) [1] (1) [1]
清高位1位组:(0) [1] (0) [1] (0) [1] (0) [1]
未清高位合并:[01] [11] [11] [10]
正确合并结果:[10] [10] [10] [10]

下面是所有双位情形与正确统计结果的对应关系,根据此对应关系很容易看出双位-取高位也能达到同样的效果,两种运算方式是等效的一对一映射

对应关系 取高位+取低位 双位-取高位
00 ===> 00 00 + 00 = 00 00 – 00 = 00
01 ===> 01 00 + 01 = 01 01 – 00 = 01
10 ===> 01 01 + 00 = 01 10 – 01 = 01
11 ===> 10 01 + 01 = 10 11 – 01 = 10

双位-取高位不产生借位和进位,是独立的两位数减法。如果实现为 n = (n – n >>> 1) & 0x33333333; 取高位后未清空原高位,导致原高位右移参与了本2位组的运算,影响了本组的运算正确性,甚至产生连续借位,影响左侧两位组的运算正确性

\scriptsize
\frac{\begin{matrix} \quad & \dot1 \dot1|01\\  -&01|10\end{matrix}}
{\begin{matrix} \quad & 01|11 \end{matrix}}

正确的2位数减法实现为 i = i – ((i >>> 1) & 0x55555555);相比加法实现 i = i & 0x55555555 + ((i >>> 1) & 0x55555555); 减少了一次与运算

4位组合并优化

四位一组相互合并时,每个4位组的范围为0(000)-4(100),表示4位组中累积出现1的次数;合并结果为0(0000)-8(1000),需要 四位二进制数储存累积和

第一点优化:对齐的高低4位组无需清空高4位即可加和。因为 结果是4位,每个4位组都不会向相邻的4位组产生进位,意味着4位组无效位不包含在运算结果中,不会冲掉左侧高位组的正确累积结果

第二点正确性保障:之后 合并8位组时,产生5位结果,意味着未清空的4位组无效位产生了向高4位组的进位,无效进位上溢至储存正确累计结果的4位组导致运算错误,所以应当清空合并后的高4位组无效位

[]:正确累计结果 ():未清空的无效位 {}:合并后的2i位组
合并前4位组:[0100] [0100] [0100] [0100] [0100] [0100] [0100] [0100]
原低位4位组:(0100) [0100] (0100) [0100] (0100) [0100] (0100) [0100]
原高位4位组:[0100] (0100) [0100] (0100) [0100] (0100) [0100] (0100)
右移高4位组:(0000) [0100] (0100) [0100] (0100) [0100] (0100) [0100]
合后未清高位:{(0100)[1000]} {(1000)[1000]} {(1000)[1000]} {(1000)[1000]}
未清合8位溢出:{(0100 1000 110)[1 0001]} {(0001 0001 000)[1 0000]}
合后清空高位:{(0000)[1000]} {(0000)[1000]} {(0000)[1000]} {(0000)[1000]}
清后合8位正确:{(0000 1000 000)[1 0000]} {(0001 0000 000)[1 0000]}

提前清空高位: i = i & 0x0f0f0f0f + (i >>> 4) & 0x0f0f0f0f);
和后清空高位: i = (i + (i >>> 4)) & 0x0f0f0f0f;
两种清空高位方案等效,但和后清空高位 减少一次与运算

8、16位合并优化

八位一组相互合并时,每个8位组的范围为0(0000)-8(1000),表示8位组中累积出现1的次数;合并结果为0(00000)-16(10000),需要 五位二进制数储存累积和

同4位组合并分析,对齐的高低8位组加和后,累积和结果最多占用低5位,每个8位组都不会产生影响左侧相邻8位组的进位,且下轮合并最多产生6位也不向高8位组溢出,不影响正确位置中的累积结果,因此无需清空高8位;此时8位组中的高3位是0,也无需考虑清零

提前清空高位: i = i & 0x00ff00ff + (i >>> 8) & 0x00ff00ff);
无需清空高位: i = i + (i >>> 8);
不清空高位 减少两次与运算

同理16位组加和后范围为0(000000)-32(100000),最终累积结果存储在低6位,高位如何不影响最终结果

提前清空高位: i = i & 0x0000ffff + (i >>> 16) & 0x0000ffff);
无需清空高位: i = i + (i >>> 16);
不清空高位 减少两次与运算

16位组合并后,此时高位还存在未清空的无效位,通过过滤低六位 11 1111 (0x3f) 得出结果
return i & 0x3f;

参考文献

java源码Integer.bitCount算法解析,分析原理(统计二进制bit位)
Java.Integer.bitCount(int)源码解析

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

©2018-2024 Howell版权所有 备案号:冀ICP备19000576号