1. 常见场景
2 具体详情
2.1 取模
2.1.1 位运算可以处理取模的原理
- 取模运算(%)是求一个数除以另一个数的余数,比如7 % 3 = 1,表示7除以3余1。取模运算在一些场景中很有用,比如判断一个数是否是奇数或偶数,或者将一个大数映射到一个小范围内。
- 位运算(&)是按位与运算,即对两个数的每一位进行逻辑与运算,得到一个新的数。比如5 & 3 = 1,表示5(0101)和3(0011)的每一位进行与运算,得到1(0001)。
位运算(&)可以用来实现取模运算(%),但是只适用于模数是2的整数次幂的情况,比如2、4、8、16等。这是因为这种情况下,模数减一后的二进制表示全是1,比如8 - 1 = 7(0111),16 - 1 = 15(1111)。这样,用一个数和模数减一进行位运算(&),就相当于截取了这个数的最低几位,也就是求余数。比如:
- int a = 13; // 00001101
- int b = a % 8; // 00000101
- int c = a & (8 - 1); // 00000101
可以看到,b和c的值都是5,说明a % 8和a & (8 - 1)等价。使用位运算(&)来实现取模运算(%)的好处是效率更高,因为位运算直接对内存中的二进制数据进行操作,不需要转换成十进制或进行除法运算。
位运算可以处理取模的原理是
基于二进制数的特性和位与运算的性质。当一个数对2的整数次幂取模时,其余数只和这个数的最低几位有关,而和高位无关。
比如:
- int a = 13; // 00001101
- int b = a % 8; // 00000101
可以看到,13对8取模的结果只和13的最低三位有关,即101。而8 - 1 = 7,其二进制表示为0111,全是1。这样,用13和7进行位与运算,就相当于保留了13的最低三位,而把高位都变成了0。比如:
- int c = a & (8 - 1); // 00000101
可以看到,c和b的值都是5,说明a % 8和a & (8 - 1)等价。这就是位运算可以处理取模的原理。
- 使用位运算来处理取模的好处是效率更高,因为位运算直接对内存中的二进制数据进行操作,不需要转换成十进制或进行除法运算。
- 据说,使用位运算实现取模只需5个CPU周期,而使用取模运算符实现至少需要26个CPU周期。
2.1.2 JDK中用应用示例
在JDK中,有一些地方使用了这种方法来实现快速取模。比如,在HashMap类中,有一个方法叫做hash(),它用来计算一个对象的哈希值,并将其映射到一个数组的索引上。这个方法中有这样一行代码:
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
这里,h >>> 16表示将h无符号右移16位。这样做的目的是为了让h的高16位和低16位进行异或操作,从而增加h的随机性和均匀性。然后,在HashMap类中还有一个方法叫做indexFor(),它用来根据哈希值和数组长度计算出数组索引。这个方法中有这样一行代码:
static int indexFor(int h, int length) {
return h & (length-1);
}
这里,length是数组长度,它必须是2的幂次方。这样做的目的是为了用位运算来实现对length取模,从而避免了使用%运算符带来的性能损耗。
2.2 用位运算判断奇偶
2.2.1 原理
用位运算判断奇偶的原理是
基于二进制数的最低位的值。如果一个数的最低位是0,那么它就是偶数;如果一个数的最低位是1,那么它就是奇数。
比如:
- int a = 10; // 00001010
- int b = 11; // 00001011
可以看到,10的最低位是0,所以它是偶数;11的最低位是1,所以它是奇数。
那么如何用位运算来判断一个数的最低位呢?我们可以用一个特殊的数,即1,来进行位与运算。1的二进制表示为00000001,只有最低位是1,其他都是0。这样,如果我们用任何一个数和1进行位与运算,就相当于保留了这个数的最低位,而把其他位都变成了0。比如:
- int c = a & 1; // 00001010 & 00000001 = 00000000
- int d = b & 1; // 00001011 & 00000001 = 00000001
可以看到,c的值是0,说明a是偶数;d的值是1,说明b是奇数。这就是用位运算判断奇偶的原理。
使用位运算来判断奇偶的好处是效率更高,因为位运算直接对内存中的二进制数据进行操作,不需要转换成十进制或进行除法运算。
据说,使用位运算判断奇偶要比使用取模运算符判断奇偶大约快4倍。
2.1.2 JDK中用应用示例
在JDK中,有一些地方使用了位运算来实现快速判断奇偶。比如,在Integer类中,有一个方法叫做bitCount(),它用来计算一个整数的二进制表示中有多少个1。这个方法中有这样一行代码:
i = i - ((i >>> 1) & 0x55555555);
这里,i >>> 1表示将i无符号右移一位。这样做的目的是为了让i的每两个二进制位相加,并存储在原来位置上。比如:
i = 10110010
i >>> 1 = 01011001
(i >>> 1) & 0x55555555 = 01011001 & 01010101 = 01010001
i - ((i >>> 1) & 0x55555555) = 10110010 - 01010001 = 01100001
可以看到,原来i中每两个二进制位中有多少个1,现在就存储在相应位置上。比如原来第一位和第二位是10,有一个1;现在第一位就是1。原来第三位和第四位是11,有两个1;现在第二位就是10,表示2。
这样做的好处是什么呢?这样做可以让我们快速判断一个数是否为奇数或偶数。因为如果一个数是奇数,它的最低位一定是1;如果一个数是偶数,它的最低位一定是0。所以我们只需要看最低位是否为1或0,就可以判断奇偶性。
比如,我们要判断5是否为奇数或偶数。我们可以用bitCount()方法来计算5的二进制表示中有多少个1:
int i = 5;
i = i - ((i >>> 1) & 0x55555555);
System.out.println(i); //输出3
我们可以看到,输出结果为3,表示5的二进制表示中有3个1。而且,最低位为1,说明5是奇数。
同理,我们要判断6是否为奇数或偶数。我们也可以用bitCount()方法来计算6的二进制表示中有多少个1:
int i = 6;
i = i - ((i >>> 1) & 0x55555555);
System.out.println(i); //输出2
我们可以看到,输出结果为2,表示6的二进制表示中有2个1。而且,最低位为0,说明6是偶数。
2.3 实现数字翻倍及减半
2.3.1 原理
用位运算实现数字翻倍及减半的原理是基于二进制数的左移和右移操作。二进制数的每一位都代表了一个2的幂次,比如:
- int a = 10; // 00001010
可以看到,10的二进制表示为00001010,从右往左数,第一位是2的0次方,第二位是2的1次方,第三位是2的2次方,以此类推。所以,10可以表示为:
- 10 = 0 * 2^7 + 0 * 2^6 + 0 * 2^5 + 0 * 2^4 + 1 * 2^3 + 0 * 2^2 + 1 * 2^1 + 0 * 2^0
那么,如果我们把这个二进制数向左移动一位,就相当于把每一位都乘以2,比如:
- int b = a << 1; // 00010100
可以看到,b的值是20,相当于a的两倍。同理,如果我们把这个二进制数向右移动一位,就相当于把每一位都除以2,比如:
- int c = a >> 1; // 00000101
可以看到,c的值是5,相当于a的一半。这就是用位运算实现数字翻倍及减半的原理。
使用位运算来实现数字翻倍及减半的好处是效率更高,因为位运算直接对内存中的二进制数据进行操作,不需要转换成十进制或进行乘法或除法运算。
据说,使用位运算来实现数字翻倍及减半要比使用乘法或除法运算符来实现数字翻倍及减半大约快10倍。
2.3.2 JDK中用应用示例
在JDK中,ArrayList是一个基于数组实现的动态列表,它可以根据需要自动调整容量。当ArrayList中的元素数量超过了数组的长度时,就需要进行扩容操作,以保证可以继续添加元素。
ArrayList的扩容操作是通过一个方法来实现的,叫做grow()。这个方法会根据当前数组的长度,计算出一个新的长度,并创建一个新的数组,然后把旧数组中的元素复制到新数组中。
那么,如何计算新的长度呢?这里就用到了位运算。位运算是一种直接对二进制数进行操作的运算,它有很多优点,比如速度快、节省空间等。在JDK中,ArrayList使用了一种位运算叫做无符号右移运算符>>>,它的作用是把一个数的二进制表示向右移动指定位数,并用0填充高位。
比如,我们要把10无符号右移1位,就相当于把10的二进制表示00001010向右移动1位,并用0填充高位,得到00000101,也就是5。这样做相当于把10除以2,并向下取整。
那么,在ArrayList中,grow()方法是如何使用无符号右移运算符来计算新长度的呢?它的逻辑是这样的:
- 首先,判断当前数组是否为空。如果为空,则新长度为默认长度10。
- 其次,判断当前数组是否已经达到最大长度(Integer.MAX_VALUE)。如果是,则无法再扩容。
- 最后,如果当前数组既不为空也不是最大长度,则新长度为当前长度加上当前长度无符号右移1位。也就是说,新长度等于当前长度的1.5倍。
比如,我们要对一个长度为10的数组进行扩容。我们可以用无符号右移运算符来计算新长度:
int oldCapacity = 10;
int newCapacity = oldCapacity + (oldCapacity >>> 1); //无符号右移1位
System.out.println(newCapacity); //输出15
我们可以看到,输出结果为15,表示新长度等于旧长度的1.5倍。
2.4 交换两数
2.4.1 原理
用位运算实现交换两数的原理是基于二进制数的异或操作。异或操作是指:数字在二进制形式上,同位上进行比较,相同为0,不同为1。比如:
int a = 10; // 00001010 int b = 20; // 00010100
可以看到,a和b的二进制表示为00001010和00010100,如果我们对它们进行异或操作,就得到:
a ^ b = 00011110 // 30
可以看到,a ^ b的结果是30,它的二进制表示为00011110。这个结果有一个很重要的性质,就是它和a或b再进行异或操作,就可以得到另一个数。比如:
a ^ (a ^ b) = b // 20 b ^ (a ^ b) = a // 10
这就是用位运算实现交换两数的原理。我们只需要把a和b分别和a ^ b进行异或操作,就可以得到对方的值。比如:
a = a ^ b; // a = 30, b = 20 b = a ^ b; // a = 30, b = 10 a = a ^ b; // a = 20, b = 10
可以看到,经过三次异或操作,a和b的值就交换了。这种方法不需要使用额外的变量来存储中间结果,也不需要进行加减乘除运算,所以效率很高。
2.4.2 JDK中用应用示例
暂无