位运算是一种偏向于计算机底层的操作,需要我们比较了解计算机底层是如何运行的,比如二进制、补码、按位与、按位或等知识,虽然理解计算机底层来思考问题会比较复杂,也比较反人类,但是位运算却可以高效地运算(很简单的道理,编译器不需要先去理解语句的语义再转成机器语言,可以直接进行二进制运算),所以在算法题目中适当地运用一些位运算可以帮助我们AC题目
计算机的编码机制
原码
原码就是将我们看到的数据转成二进制,最高位是符号位,1为负数,0为正数,例如8位 5 的原码表示为(0000 0101)2 ,而 -5 的8位原码是(1000 0101)2
反码
反码是从原码进行变换得到的,正数的反码还是它本身,而负数的反码除了符号位之外全部取反,例如8位 5 的反码表示为(0000 0101)2 ,而 -5 的8位反码是(1111 1010)2
补码
正数的补码依旧是它本身(即正数的原反补码都是一样的),而负数补码是在反码的基础上加1得到的,例如 8位 5 的补码是(0000 0101)2,而 -5 的8位补码是(1111 1011)2补码是计算机存储器上真正存储的编码类型,它巧妙地让计算机进行加减法运算时变得方便,例如5-5=5+(-5)=(0000 0101)2 + (1111 1011)2 = (1 0000 0000)2,去掉高位(因为我们的数只有8位),即可以得到正确答案,于是计算机可以用加法器来进行减法运算
位运算
我们最常用的位运算有与、或、异或、左移、右移等,不同语言实现的位运算也有些区别,主要区别在右移,有些语言实现的右移是补符号位,而有些是直接高位补0,Java即是高位补0的一门语言,下面以Java的位运算符来说明
按位与运算
按位与运算即是两个数每一位进行与运算,规则为:同1则结果为1,否则为0
故9&7=(0000 1001)2 & (0000 0111)2 = (0000 0001) 2= 1
按位或运算
按位或运算即是两个数每一位进行或运算,规则为:同0则结果为0,否则为1
故9|7 = (0000 1001)2 | (0000 0111)2 = (0000 1111)2 = 15
异或运算
异或的规则为:同为0或同为1则结果为0,否则为1
故9^7 = (0000 1001)2 ^ (0000 0111)2 = (0000 1110)2 = 14
左移右移运算
左(右)移运算即是将所有位左(右)移指定位,低(高)位补0
例如9<<2 = (0000 1001)2 << 2 = (0010 0100)2 = 36
9 >> 2 = (0000 1001)2 >> 2 = (0000 0010)2 = 2
如果你对二进制比较熟悉,那你应该发现左移其实就是乘2,而右移就是地板除2
运用位运算解决算法题
题目1:数值的整数次方
题目描述:给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。保证base和exponent不同时为0
分析:在这道题里,我们要考虑一个情况,就是当exponent为负数时,结果应该是1/base(-exponent)。这道题如果我们用累乘的方式完成这道题会超时,如果调用Math的pow倒是可行的,我们这里使用的是算平方的方式,就是通过log(exponent)次的计算平方来得到最终的结果。
我们可以清楚地知道,127 = 124 *123 = 1222 * 122 * 12,并且在二进制中,当一个数是偶数时,其最低位为0,否则为1。我们就利用这一点,当exponent最低位为1时,表示我们应该多乘一个当前的base,而base在exponent除以2时应该平方,因为a2b = ab * ab 。 或者你可以这样理解,当exponent的最低位为1时,需要乘一次base,否则可以跳过。
public class Solution {
public double Power(double base, int exponent) {
//指数为0或基数为1可以直接返回1,减少不必要的计算
if(base == 1 || exponent == 0)
return 1;
//判断指数是否为负数
boolean n = exponent < 0 ? true : false;
//如果指数为负数先算出分母的值
exponent = n ? -exponent : exponent;
double res = 1;
while(exponent > 0){
//判断最低位为0还是为1
if((exponent & 1) == 1){
//为1则乘1次base
res *= base;
}
//指数除以2,base算平方
exponent = exponent >> 1;
base *= base;
}
return n ? 1.0/res : res;
}
}
题目2:二进制中1的个数
题目描述:输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
分析:Java的int类型是32位的,直接遍历32个位上分别为0还是为1,为1则直接计数器加一的方式也很快,是常数时间复杂度;程序员是追求完美的,有一种新的方法可以达到,有几个1就循环几次,这就是位运算的魅力。有一个不得不提的巧妙是:一个数 n 与 (n-1)做按位与运算可以将最低一个1置0
例如(1111 0000)2&(1110 1111)2 = (1110 0000)2,于是我们就可以顺利完成我们的题目解答了
public class Solution {
public int NumberOf1(int n) {
int count = 0;
//因为n可能为负数,所以注意这里不是大于0
while(n != 0){
//消灭一个1,计数加一
count++;
n &= (n-1);
}
return count;
}
}