位运算
基本概念
对于 8 位二进制,能表示的总数是 $2^8=256$,为了表示负数,将第一位作为符号位,并使用补码表示法:
正数:从 00000000 到 01111111,也就是从 0 到 127,一共 128 个值(包括 0)。
负数:从 10000000 到 11111111,代表 -128 到 -1,也是 128 个值。
由于 00000000 和 10000000 都能表示 0(一个正0,一个负0),但 0 只需一个编码,所以,负数可以“借”来这个位置,多表示一个数(就是 -128)。
负数的补码表示 = 该数的正数的反码 + 1,因为 1 + (-1) 要等于 0,而只改变符号的话,它们的二进制数相加并不等于 0,以 1 的二进制表示 00000001 为例,若使 1 - 1 = 0 成立,则 -1 应该为 11111111,而这正是 00000001 的反码 11111110 加 1 得到的。
对于 8 位二进制,只有 -128 不满足 负数的补码表示 = 该数的正数的反码 + 1,因为第一位已经被用来表示符号位,这时就不存在 +128,最大只能到 +127,而 0 只需一个表示就行,所以 10000000 就留给了 -128。
常见位运算
常见的位运算如下:
~:按位取反,0 变成 1,1 变成 0,如~1010 = 0101&:按为与运算, 1 与 1 方为 1|:按位或运算,只要有一个 为 1 ,则该位置 为 1^:按位异或,不相同的值则为 1,比如 0 异或 1,或者 1 异或 0<<:带符号左移,比如 35(00100011),左移一位为 70(01000110), -35(11011101)左移一位为 -70(10111010)>>:带符号右移,比如 35(00100011),右移一位为 17(00010001), -35(11011101)左移一位为 -18(11101110)<<<:无符号左移,比如 35(00100011),左移一位为 70(01000110)>>>:无符号右移,比如 -35(11011101),右移一位为 110(01101110)x ^= y;y ^= x;x ^= y;:x 与 y 交换s &= ~(1 << k):第 k 位置为 0
位运算实现加法
如果给两个正数 a 和 b,怎么使用位运算求出它们的和。
a 与 b 的每一位相加,如果相加时没有产生进位,结果就是异或操作(a ^ b)的值,但是如果产生了进位,分为两部分,一部分是 a 和 b 不进位的和,另外一部分是 a 和 b 进位的结果,两部分相加即可。如果此时相加还是会有进位问题,那么就循环直到进位等于 0,即不产生进位为止:
当前位的和值等于
a ^ b进位的结果为
a & b,但是进位是往前每次往前进 1 位,叠加在前面一位上,因此需要左移一位a & b << 1。
将两者相加,在循环中直到进位为 0。代码如下:
统计二进制整数中1的数量
给一个整数,计算出该整数在二进制表示中 1 的个数。可以直接使用标准库中的 Integer.bitCount() 方法,该方法使用了位运算优化策略。常见的方法是使用循环,逐位检查。
任何一位与 1 进行与运算,最终结果都是它本身,那么一个数与 1 进行与运算(&),如果结果是 1 ,则说明最后一位 是 1。知道了最后一位是否为 1,只要把数字不断向右移位和 1 进行与计算,结果为 1 则说明最后一位是 1,循环直到数字为 0 即可。注意这里需要使用无符号右移,即 >>>,代码如下:
位运算找字符串的差异
若有两个字符串 a 和 b,都是大小写字母,b 是由 a 字符串打乱字符顺序之后,再随机位置加上 1 个字符,转换而成,该如何找出这个字符。比如 a = "lasda" , b = "daldas" ,不同的字符是 "d"。
最简单的方法就是统计两个字符串中每个字符出现的次数,对比一下就可以找出多出来的字符到底是哪一个。
每个字符对应一个 ASCII 码,ASCII 码使用指定的 7 位或 8 位二进制数组合来表示 128 或 256 种可能的字符。标准 ASCII 码也叫基础 ASCII 码,使用 7 位二进制数(剩下的 1 位二进制为 0)来表示所有的大写和小写字母,数字 0 到 9、标点符号,以及在美式英语中使用的特殊控制字符。另一种方式就是我们将每个字符的 ASCII 全部加起来,差值就是多出来字符的 ASCII 码,ASCII 码的数值是 int ,可以转换成为字符。
也可以使用位运算解决,首先,可以注意到 a^b^a = b,即任何一个字符,与偶数个其他字符进行异或运算,最终结果等于本身。所以两个字符串的所有字符都进行异或,多出来的字符数量必定是奇数,其他的字符数量是偶数。因此,只需要对所有字符进行异或计算,就可以得到最终结果。代码如下:
二进制中的汉明距离
信息论中,Hamming Distance(汉明距离)表示两个等长字符串在对应位置上不同字符的数目,从另外一个方面看,汉明距离度量了通过替换字符的方式将字符串 x 变成 y 所需要的最小的替换次数。比如:
这里的问题是计算两个整数 a 和 b ,他们的二进制表示中不同的位置的数量(汉明距离)。
可以分为两步:
先异或计算,获取到数值不同的位置
计算数值不同的位的数量,正好使用之前统计二进制整数中1的数量的代码。
代码如下:
对于该循环计算1数量的代码,即使是 0 位置我们也会循环,每次都要循环完 32 位。有一个Brian Kernighan优化算法,该算法使用 x 与 x-1 做与运算,得到的结果恰好为 x 删去其二进制表示中最右侧的 1 的结果。即循环中每次 x &= x-1 操作都会删去最右侧的一个 1,直到等于 0,循环的次数就是 1 的个数:
布隆过滤器
布隆过滤器的误判率是可以预测的,与位数组的大小,以及 hash 函数的个数等相关。
假设位数组的大小是 m,一共有 k 个 hash 函数,那么每一个 hash 函数,只能 hash 到 m 位中的一个位置,所以某一位没有被 hash 到的概率是 $1-1/m$,k 个 hash 函数都 hash 之后,该位还是没有被 hash 到的概率为 $(1-1/m)^k$。
如果此时插入了 n 个元素,即位数组已经被 hash 了 n*k 次,该位还是没有被 hash 到的概率是 $(1-1/m)^{kn}$,那么该位已被 hash 变成 1 的概率为 $1-(1-1/m)^{kn}$。
如果需要检测某一个元素是不是在集合中,也就是该元素对应的 k 个位 hash 出来的值都为 1,即元素本不存在,但该元素对应的所有位都被 hash 变成 1 的概率是 $(1-(1-1/m)^{kn})^k$,当 m 很大时可近似为 $(1-e^{-kn/m})^k$。
所以,随着位数组大小 m 和 hash 函数个数的增加,误判率会下降,随着插入的元素 n 的增加,概率会上升。
最后还可以根据自己期待的误判率 P 和期待添加的个数 n,来大致计算出布隆过滤器的位数组的长度:$m=-\frac{n\ln P}{(\ln 2)^2}$。
下面实现一个简单的布隆过滤器,需要考虑的点:
位数组空间大小,其他不变,位空间越大,hash 冲突的可能性越小。
hash 函数的实现,为避免冲突,应使用多个不同的质数来当种子。
实现两个方法:添加元素和判断某元素是否存在。
下面用了简单的 hash 函数,主要是使用 hash 值得高低位进行异或,然后乘以种子,再对位数组大小进行取余数: