大家在学习的过程中总是经常看到:x % 2^n = x & (2^n - 1);
比如java的ThreadLocal源码中:firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
也就是:一个数取模(求余数),如果除数是2的n次方,那么等于这个 数 按位与 2的n次方-1。
还是不好理解,举一个具体的例子。
十进制
23 % 8 = 2....7
二进制形式
10111 % 1000
等价于:
10111
& 00111 (2^3-1)
= 00111 (7)
经过上面的案例,可以看到,23转换二进制是10111, 8的二进制是1000,转换成2^n-1就是0111。
最终异或的结果就是7。和我们取模计算的结果一样。
重点:为什么会这样?为什么取模会在除数是2的n次方的时候等价于按位与操作。
基本概念
什么是按位与
按位与是一种运算操作,可以理解成加减乘除。按位与的意思是,两个二进制数,只有都为1的时候结果才是1。按位:按照位置一对一对比,与:与操作,两者都为true则是true。
案例: 100001 & 000111 = 000001 。两个二进制数的最后一位都是1,所以按位与的结果是1。
2^n-1 和2^n 之间是什么关系?
2^n: 2^0 2^1 2^2 2^3 2^4
二进制表示: 0001 0010 0100 1000 10000
2^n-1: 0000 0001 0011 0111 011111
可以发现2的n次方实际就是1后面全是0,而2^n-1则是
对比十进制来学习:
118 % 10 = 18 ----118 % 10^1= 8
1231 % 100 = 31 ------1231 % 10^2 = 31
可以发现,求余数就是只保留x^n中的低位,n就是保留的位数,118%10中,n=1,保留的就是1位,也就是8。1231%100中n=2,所以保留2位就是余数,也就是31.
回到二进制中,2^n就相当于十进制中的10,100,1000。而二进制取模就是保留2^n中的n位,就是余数。
那么继续回到23 % 8的案例
23 % 8 = 2....7
二进制形式
10111 % 2^3 保留3位 就是111 =7,就是余数。
所以这就是为什么必须除数需要时2^n的时候才能用按位与替代 %。
那么为什么需要使用2^n-1呢?
上面我们知道了2^n-1就是2^n的1变成0,1后面的0全部变成1。而这个时候,1的个数刚好是n的值
比如 1000 ----> 0111 2^3 。1的个数也是3个。这就有一个特点,通过2^n-1之后的值,前面的全是0,后面的1的个数刚好是n的值。而按位或操作,必须有两个都是1才是1。那么就能保证,按位与得到的结果刚好是被除数的后n位。
举个例子:
23 % 8 = 7
10111
& 00111
------------------
00111 (7)
个人表达能力有限,感觉并没有讲清楚,但是能够弄明白十进制中的例子,代入二进制中就很容易明白了。按位与2^n-1只是为了得到后n位的操作。