Hamming Weight,即汉明重量,指的是一个位数组中非0二进制位的数量。
解决问题:
有100M的二进制数据,算出里面有多少个1 ?(1MB = 1,000,000 Byte = 8,000,000 bit)
1、遍历法:
遍历 8 亿次,右移比较 8 亿次。
public int hammingWeight(int n) {
int res = 0;
while(n!=0) {
res+= (n & 0x1);
n >>>=1;
}
return res;
}
2、 查表法:(空间换时间)
key | value(值为1的数量) |
---|---|
0000 0000 | 0 |
0000 0001 | 1 |
… | … |
1111 1111 | 8 |
遍历 1 亿次
空间: 比如java的map<String,integer>
上述键长为 8 位,那么 2 ^ 8 = 256 ,
String 所需要字节(一个英文字母或者数字占1个字节): 256 * 8 = 2048 byte
Integer 所需要字节 ( 0,1,2,3,4,5,6,7,8)(一个Integer对象粗略统计 3* 4 = 12字节) 9 * 12 = 108 byte
算上map对象总共差不多2500 byte.
若键的长度持续增加,那么比如16位:
2500 * 2 ^ ( 16 / 8 ) = 2500 * 256 = 625k byte
若是 32位:
2500 * 2 ^ ( 32 / 8 ) = 2500 * ( 2 ^ 24) = 40,000 M byte = 40G
瓶颈 :
1、 内存
2、cpu缓存 :查表之后,会有部分数据打到cpu缓存里面,方便下次使用,但是表格越大,缓存不命中的情况就越高,那么缓存就会不断换入换出,影响性能
3、汉明重量 :variable-precision SWAR 算法
第一步:
计算出来的值i的二进制可以按每2个二进制位为一组进行分组,各组的十进制表示的就是该组的汉明重量。
第二步:
计算出来的值i的二进制可以按每4个二进制位为一组进行分组,各组的十进制表示的就是该组的汉明重量。
第三步:
计算出来的值i的二进制可以按每8个二进制位为一组进行分组,各组的十进制表示的就是该组的汉明重量。
第四步:
i * (0x01010101)计算出汉明重量并记录在二进制的高八位,>>24语句则通过右移运算,将汉明重量移到最低八位,最后二进制对应的十进制数就是汉明重量。
算法时间复杂度是O(1)的。
// 计算32位二进制的汉明重量
int32_t swar(int32_t i)
{
i = (i & 0x55555555) + ((i >> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
i = (i & 0x0F0F0F0F) + ((i >> 4) & 0x0F0F0F0F);
i = (i * (0x01010101) >> 24);
return i
}
0x55555555 ===>> 0101 0101 0101 0101 0101 0101 0101 0101
0x33333333 ===>> 0011 0011 0011 0011 0011 0011 0011 0011
0x0F0F0F0F ===>> 0000 1111 0000 1111 0000 1111 0000 1111
0x01010101 ===>> 0000 0001 0000 0001 0000 0001 0000 0001
算法解析:
比如: 1101 1010 中有几个 1 ?
a、2个一组拆成 11 01 10 10 ,待会分别处理4组,加起来即可
b、处理 11 ,首先根据最上面的右移遍历法,用
while(n!=0) {
res+= (n & 0x1);
n >>>=1;
}
即:
11 and 01 = 01
x= 01 (表示有1个1)
n >>>=1 右移1位,11变成 01
01 and 01 = 01
y= 01 (表示有1个1)
然后 x + y = 10 (表示第一组总共有2个1)
c、剩下3组以此类推 ...
算法核心来了:
一、切成N组,每2位一组 (0x55555555)
于是:处理 0111 1010 0101 0101 0010 0001 1111 0010
0x55555555 ===>> 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
第一个步骤:
i = (i & 0x55555555) + ((i >> 1) & 0x55555555);
即可完成该操作。
先把偶数位的给处理了,然后右移一位把奇数位的给处理了。然后得出的结果加起来 2位一组,每2位就是原始数字每2位中所包含的1的个数。
input: 0111 1010 0101 0101 0010 0001 1111 0010
step1:
0011 1101 0010 1010 1001 0000 1111 1001 (i >>> 1)
0101 0000 0101 0101 0000 0001 0101 0000 (i & 0x55555555)
0001 0101 0000 0000 0001 0000 0101 0001 ((i >>> 1) & 0x55555555)
0110 0101 0101 0101 0001 0001 1010 0001 (+)
01 10 01 01 01 01 01 01 00 01 00 01 10 10 00 01 (拆开)
剖析前面的4位 01 10 ,01表示input 前2位有1个1,10表示继续的俩位有2个1。
那么说明input的前4位有1+2=3个1
- 接下来的步骤二就是(每2位合并)看下如何把前四位01 10合并起来,把 01 + 10 加起来变成 0011.
二、合并 2位一组的结果 (0x33333333)
- 4位表示2个2位的相加!!
- 看下上面的运算结果,前四位的 0110 ,跟 0011与运算,剩下了后面的 10.
- 然后右移2位就只剩下前面的两个数字01,再与0011去与运算,结果是原本前面的 01.
- 然后加起来就变成了 0011了
- 0011就转换成10进制就代表3个1了。
0110 0101 0101 0101 0001 0001 1010 0001 ( i )
0001 1001 0101 0101 0100 0100 0110 1000 ( i>>> 2 )
0010 0001 0001 0001 0001 0001 0010 0001 ( i & 0x33333333 )
0001 0001 0001 0001 0000 0000 0010 0000 ( ( i >>> 2) & & 0x33333333 )
0011 0010 0010 0010 0001 0001 0100 0001 (+)
剖析下前8位 0011 0010 表明,input的前4位有3个1,接下去的4位有2个1,那么前8位总共有3+2=5个1
- 接下来的步骤就是(每4位合并),比如 0011 0010 这两个4位给加起来
三、合并 4位 一组的结果 (0x0F0F0F0F)
- 8位表示2个4位的相加!!
- 前8位也就是 0011 0010, 2个4位相加 = 0101 5个1 。
- 0010 与 1111(0F) 与运算,得到 0010
- 右移动4位=0011, 0011 与 1111(F) 与运算,得到 0011
- 两者相加 = 0101
step3:
0011 0010 0010 0010 0001 0001 0100 0001 ( i )
0000 0011 0010 0010 0010 0001 0001 0100 ( i >>> 4)
0000 0010 0000 0010 0000 0001 0000 0001( i & 0x0F0F0F0F)
0000 0011 0000 0010 0000 0001 0000 0100 ((i >>> 4) & 0x0F0F0F0F))
0000 0101 0000 0100 0000 0010 0000 0101 (+)
第三步计算完后,分成了四组,每组八个二进制位,只要求出每组上二进制表示的值,相加的结果就是汉明重量。
- 那么下一个步骤就是要把 0000 0101+ 0000 0100 + 0000 0010 + 0000 0101 ,
四、合并 8位 一组的结果 (0x01010101)
- i * (0x01010101) >>> 24
- 那么还是可以用之前的方法右移8位,然后和 1111 1111 做与运算然后相加,也就是 0xFFFFFFFF。这样子做是把16位合并成8位。
- 由于是对32位二进制进行操作,所以还有一个巧妙的方法:由于32位二进制最多就32个1,所以往右移动24位,留下8位,然后4次8位相加即可得到结果。
- 那么肯定有人问,为什么剩下8位,8位不是256,应该是留下5位,5位才是32。其实,细心的朋友在看到 step3 的时候,&0x0F0F0F0F 其实每8位的高4位其实都已经是0了,所以最终step3的32位切成的4组8位,都只有低4位去相加,低4位相加,最大也就是1000 +1000 +1000 +1000 = 0010 0000 = 32。
- 别问为啥不是4个 1111 相加,因为8位最多就是8个1,也就是 1000。所以看下表g4相加:
g1 | g2 | g3 | g4 | g5 | g6 | g7 |
---|---|---|---|---|---|---|
. | . | . | 0000 0101 | 0000 0100 | 0000 0010 | 0000 0101 |
* | 相 | 乘 | 0000 0001 | 0000 0001 | 0000 0001 | 0000 0001 |
== | == | == | == | == | == | == |
. | . | . | 0000 0101 | 0000 0100 | 0000 0010 | 0000 0101 |
. | . | 0000 0101 | 0000 0100 | 0000 0010 | 0000 0101 | . |
. | 0000 0101 | 0000 0100 | 0000 0010 | 0000 0101 | . | . |
0000 0101 | 0000 0100 | 0000 0010 | 0000 0101 | . | . | . |
== | == | == | == | == | == | == |
. | . | . | 0001 0000 | . | . | . |
32位二进制与32位二进制的结果是32位。
===> 表明 input 0111 1010 0101 0101 0010 0001 1111 0010 有 16个 1
五、复习一下二进制乘法运算
六、性能:
- 遍历算法一次处理1位,swar函数一次计算32位。
- swar 函数计算32位二进制位的汉明重量,比遍历算法快32倍。比键长为8位的查表法快4倍。比键长为16位的查表法快2倍。但是他不用耗费内存,因为他就单纯计算。
- swar是一个常熟复杂度的操作,所以可以在一次循环中多次执行 swar 函数,如一次循环调用两次 swar 函数,那么一次循环可以计算64位。性能是原来的2倍。调用四次swar,那么一次循环就可以计算128位,性能是原来的4倍。但是是有极限的,当循环中处理的数组的大小超过cpu缓存的大小,这种优化效果会降低并消失。
七、扩展:
redis 的 bitcount 底层算法
查表法(上述2,键长8位) + swar ( 1次循环4次swar,一次处理128位)
那么策略就是:
当一个二进制长度小于等于128位(8个二进制)的时候,用查表法。
当一个二进制长度大于128位的时候,用swar。
此时计算一个 100 M = 800 000 000 bit来计算,需要第一轮循环 800 000 000 / 128 = 6 250 000次。
第二轮循环 0次。如果不整除,那么mod,余数进行第二轮循环去查表法解决。
第一轮循环执行次数 loop1 = n / 128
第二轮循环执行次数 loop2 = n mod 128