汉明距离(
Hamming Distance
)的基本思想很简单,就是找不同。当求由01二进制组成
的向量间的汉明距离可以由位运算直接进行,速度非常快。好的算法能让计算速度达到极快,Java内置bitCount源码就实现了一种速度极快的算法。在Linux机器
(CPU: i7-4790 @ 3.6GHz)测试了1亿对汉明距离只用0.5ms,是普通算法的196倍。
测试1亿对汉明距离结果
测试基于32位的01二进制数进行的。
import
java.util.Arrays
;
import
java.util.Random
;
public
class
testHammingDistance
{
//生成1亿个随机32位的Int类型数
public
static
int
[]
randomInt
(
int
num
,
int
seed
)
{
int
[]
a
=
new
int
[
num
];
Random
r
=
new
Random
(
seed
);
Arrays
.
setAll
(
a
,
w
->
r
.
nextInt
());
return
a
;
//调用Java bitCount 求汉明距离
public
static
int
hammingDistance1
(
int
hash1
,
int
hash2
)
{
int
i
=
hash1
^
hash2
;
return
Integer
.
bitCount
(
i
);
//求解正数的二进制汉明距离
public
static
int
hammingDistance2
(
int
hash1
,
int
hash2
){
int
num
=
hash1
^
hash2
;
int
count
=
0
;
for
(;
num
>
0
;
count
++)
num
&=
(
num
-
1
);
return
count
;
//主函数测试1亿对
public
static
void
main
(
String
[]
args
)
{
int
tem
=
4324523
;
int
[]
all
=
randomInt
(
100000000
,
123
);
long
startTime
=
System
.
currentTimeMillis
();
for
(
int
x:
all
){
int
tem1
=
hammingDistance1
(
tem
,
x
);
long
endTime
=
System
.
currentTimeMillis
();
long
startTime2
=
System
.
currentTimeMillis
();
for
(
int
x:
all
){
int
tem2
=
hammingDistance2
(
tem
,
x
);
long
endTime2
=
System
.
currentTimeMillis
();
System
.
out
.
println
(
"hammingDistance1 time used: "
+
(
endTime
-
startTime
)
+
"ms"
);
System
.
out
.
println
(
"hammingDistance2 time used: "
+
(
endTime2
-
startTime2
)
+
"ms"
);
hammingDistance1
time
used: 5ms
hammingDistance2
time
used: 980ms
Process finished with
exit
code 0
求汉明距离的核心是在数两个二进制数求
&
运算后1的个数,求1的个数最容易想到的是循环整个二进制获得1的个数。如下算法:
public
int
numberOf1
(
int
hash1
,
int
hash2
){
int
num
=
hash1
^
hash2
;
int
count
=
0
;
while
(
n
!=
0
){
if
((
num
&
1
)
==
1
){
++
count
;
n
=
n
>>
1
;
return
count
;
这种方法很容易理解,就是每次循环的时候对1取
&
操作,若结果等于1就统计出了一个1,然后每次循环后右移
>>
使高位的0或1移动到低位。这种算法的
时间复杂度为
O(n)
,其中n表示bit的位数。
public
int
hammingDistance2
(
int
hash1
,
int
hash2
){
int
num
=
hash1
^
hash2
;
int
count
=
0
;
for
(;
num
>
0
;
count
++)
num
&=
(
num
-
1
);
return
count
;
优化后的方法通过二进制的运算法则进行优化,优化后的时间复杂度仍然为
O(n)
,但是这里的n代表着1的个数最差的结果n才代表bit位,比第一个算法n代表bit位要好。
二进制的运算原则是逢二进一,每次循环时候我都让num减去1通过跟自身&操作消除低位1,最后num<0循环停止。有多少个1就循环多少次。
例如,二进制
0b1110
统计为3个1的过程如下:
// 第一次迭代:
0b1110
-
1
=
0b1110
-
0b0001
=
0b1101
0b1110
&
0b1101
=
0b1100
// 第二次迭代:
0b1100
-
1
=
0b1100
-
0b0001
=
0b1011
0b1100
&
0b1011
=
0b1000
// 第三次迭代:
0b1000
-
1
=
0b1000
-
0b0001
=
0b0111
0b1000
&
0b0111
=
0b0000
Java源码bitCount算法
Java的32位二进制Integer.bitCount源码如下:
public
static
int
bitCount
(
int
i
)
{
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
;
Java源码实现了复杂度为O(1)的算法
当我们想知道一个二进制数中有多少个1时候最容易想到的方法就如前两种方法一个一个的数,数一下一共多少个1就完事了。但是Java源码使用的方法是
先两个一对儿数一下有多少个1,存到原来的位置。然后对上面的数的结果再成对统计以此类推。
以8位为例,算法思想如下:
二进制 十进制
(
count个数
)
1 0 1 1 1 1 0 1 1 0 1 1 1 1 0 1
\/
\/
\/
\/
\/
\/
\/
\/
01 10 10 01 1 2 2 1
\
/
\
/
\
/
\
/
0011 0011 3 3
\
/
\
/
0110 6
这种count方法就如一个倒立的二叉树一样。每一层的节点个数为2^(n-1)个,也就是说对于32位的二进制2^(n-1)=32,n=5。所以,我们只需要数5次,对于64位二进制只需要
数6次就能知道二进制中有多少个1了,这种方法实现了时间复杂度为O(1)优秀算法。
Java源码算法实现机理
Java源码中每次
&
运算各个数的二进制,主要是为了抹除成对统计时候的左位(进行右移位运算时候高位对低位产生的影响)。
0x55555555
0b01010101010101010101010101010101
0x33333333
0b00110011001100110011001100110011
0x0f0f0f0f
0b00001111000011110000111100001111
0x00ff00ff
0b00000000111111110000000011111111
0x0000ffff
0b00000000000000001111111111111111
0x3f
0b00111111
i & 0x55555555实现整数i抹除左一位,然后错位相加。
(i »> 1) & 0x55555555表示:左位移到右边,再把左位抹除,这样就可以计算两个bit位上1的个数了:0b1011=>0b0001 + 0b0101 = 0b0110左两位有1个1,右两位有2个1。
这时i中存储了每两位的统计结果,可以进行两两相加,最后求和
0b11 11 11 11 11 (i & 0x55555555) + ((i >>> 1) & 0x55555555) = 0b0101010101 + 0b0101010101 = 0b1010101010
0b10 10 10 10 10 (i & 0x33333333) + ((i >>> 2) & 0x33333333) = 0b1000100010 + 0b00100010 = 0b1001000100
0b10 01 00 01 00 (i & 0x0f0f0f0f) + ((i >>> 4) & 0x0f0f0f0f) = 0b1000000100 + 0b0100 = 0b1000001000
0b10 00 00 10 00 (i & 0x00ff00ff) + ((i >>> 8) & 0x00ff00ff) = 0b1000 + 0b10 = 0b1010
0b00 00 00 10 10 (i & 0x0000ffff) + ((i >>> 16) & 0x0000ffff) = 0b1010 + 0 = 0b1010
dec 10
public static 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;
2.算法第二步没有优化方法
3.算法第三步:实际是计算每个byte中的1的数量,最多8(0b1000)个,占4bit,可以最后进行位与运算消位,减少一次&运算:i = (i + (i »> 4)) & 0x0f0f0f0f
4.第四,五步:同上理由,可以最后消位。但是由于int最多32(0b100000)个1,所以这两步可以不消位,最后一步把不需要的bit位抹除就可以了:i & 0x3f
最后得到Java源码最简洁的写法。
Java位运算符