(Java)算法——位运算基础及基本应用

位运算基础及基本应用

  1. 在处理整形数值时,可以直接对组成整形数值的各个位进行操作。这意味着可以使用屏蔽技术获得整数中的各个位(??)
  2. 位运算是针对整数的二进制进行的位移操作
  3. 整数 32位 , 正数符号为0,负数符号为1。十进制转二进制 不足32位的,最高位补符号位,其余补零
  4. 在Java中,整数的二进制是以补码的形式存在的
  5. 位运算计算完,还是补码的形式,要转成原码,再得出十进制值
  6. 正数:原码=反码=补码 负数:反码=原码忽略符号位取反, 补码=反码+1
    例如:十进制4 转二进制在计算机中表示为(补码) 00000000 00000000 00000000 00000100

例如:十进制-4 转二进制在计算机中表示为(补码) 11111111 11111111 11111111 11111100

位运算符

&(与)

与( & )每一位进行比较,两位都为1,结果为1,否则为0

|(或)

或( | )每一位进行比较,两位有一位是1,结果就是1

^(异或)

每一位进行比较,相同为0,不同为1

规律
  1. 异或可以理解为不进位加法:
    1+1=0
    0+0=0
    1+0=1
  2. 交换律。可任意交换运算因子的位置,结果不变
    a ^ b = b ^ a
  3. 结合律
    a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c
  4. 对于任何数x,(即同自己求异或为0,同0求异或为自己)
    x ^ x = 0, x ^ 0 = x
  5. 自反性(即连续和同一个因子做异或运算,最终结果为自己)
    a ^ b ^ b = a ^ 0 = a

~ (非/取反)

每一位进行比较,按位取反(符号位也要取反)

<<(向左位移)

左移( << ) 整体左移,右边空出位补零,左边位舍弃 (-4 << 1 = -8)

>>(向右位移)

整体右移,左边空出位补零或补1(负数补1,整数补0),右边位舍弃 (-4 >> 1 = -2)

>>>(无符号右移)

同>>,但不管正数还是负数都左边位都补0 (-4 >>> 1 = 2147483646)

机器数和机器数的真值

机器数

一个数在计算机中的二进制表示形式,叫做这个数的机器数。机器数是带符号的,在计算机用机器数的最高位存放符号,正数为0,负数为1。

机器数的真值

由于机器数的第一位是符号位,所以机器数的形式值就不等于真正的数值.为了区别起见,将带符号的机器数对应的真正数值成为机器数的真值。比如0000 0001的真值 = +000 0001 = +1,1000 0001的真值 = –000 0001 = –1

原码、反码、补码

对于计算机而言,万物皆0、1,所有的数字最终都会转换成0、1的表示,有3种机器存储一个具体数字的编码方式,分别是:原码、反码和补码。

原码

原码表示法在数字前面增加了一位符号位,即最高位为符号位,正数位该位为0,负数位该位为1.比如十进制的5如果用8个二进制位来表示就是00000101,-5就是10000101。

反码

正数的反码是其本身,负数的反码在其原码的基础上,符号位不变,其余各个位取反。5的反码就是00000101,而-5的则为11111010。

补码

正数的补码是其本身,负数的补码在其原码的基础上,符号位不变,其余各位取反,最后+1。即在反码的基础上+1。5的反码就是00000101,而-5的则为11111011。
在计算机中负数采用二进制的补码表示,10进制转为二进制得到的是源码,将源码按位取反得到的是反码,反码加1得到补码

位运算应用

1.判断奇偶数

方法一

num%2 取模为0是偶数,反之则为奇数

String result=(num%2==0?)"偶数":"奇数";
方法二

偶数最低位是0.奇数最低位是1。对最后一位&1,为0是偶数,为1是奇数

String result=(num&1==0)?"偶数":"奇数";

2.获取二进制位是1还是0

实例:判断第五位的二进制位是1还是0

//方法一——将1左移4位,判断第五位是1还是0,然后右移4位判断0还是1)
String result1=(num&(1<<4)>>4)==0?"0":"1";
//方法二——第五位二级制数右移4位,&1判断0还是1
String result2=((num>>4)&1==0)?"0":"1";

问题:方法一中,左移4位判断为1还是0后,还有必要在右移4位吗?

3.交换两个变量的值

可参考我的另一篇博客
(Java版)算法——交换两个基本数据类型的变量值和数组中元素调换位置

代码及运算过程
int a = 50;    //二进制 110010
int b = 60;    //二进制 111100
a = a^b;   //110010,111100——>001110
b = a^b;   //001110,111100——>110010  ——>50
a = a^b;   //001110,110010——>111100  ——>60
System.out.println(f+" "+g);//输出结果是:60 50
利用异或的规律证明
a = a^b;   
b = a^b;   //这里 b=a^b=(a^b)^b=a^b^b=a
a = a^b;   //这里 a=a^b=(a^b)^a=b

4.不用判断语句,求整数的绝对值

num>>31,有符号右移,正数为0,负数为-1
num>>>31,无符号右移,正数为0,负数为1
num^0,为本身(同0求异或为自己)
num^-1,相当于取反;取反在+1,相当于是绝对值

int result=(num^(num>>31))+(num>>>31);

位运算实现加减乘除

加法

十进制的加法运算:
13 + 9 = 22

拆分运算过程:

  1. 不考虑进位,分别对各位数进行相加,结果为sum:
    个位数3加上9为2;十位数1加上0为1; 最终结果为12;
  2. 考虑进位,结果为carry:
    3 + 9 有进位,进位的值为10;
  3. 如果步骤2所得进位结果carry不为0,对步骤1所得sum,步骤2所得carry重复步骤1、 2、3;如果carry为0则结束,最终结果为步骤1所得sum:
    这里即是对sum = 12 和carry = 10重复以上三个步骤,
  • 不考虑进位,分别对各位数进行相加:sum = 22;
  • 只考虑进位: 上一步没有进位,所以carry = 0;
  • 步骤2carry = 0,结束,结果为sum = 22.
二进制实现上述过程:

13的二进制为0000 1101,9的二进制为0000 1001:

  1. 不考虑进位,分别对各位数进行相加:
    sum = 0000 1101 + 0000 1001 = 0000 0100

  2. 考虑进位:
    有两处进位,第0位和第3位,只考虑进位的结果为:
    carry = 0001 0010

  3. 步骤2carry == 0 ?,不为0,重复步骤1 、2 、3;为0则结束,结果为sum:
    本例中,

  • 不考虑进位sum = 0001 0110;
  • 只考虑进位carry = 0;
  • carry == 0,结束,结果为sum = 0001 0110
    转换成十进制刚好是22.
伪代码推理:

以3+9为例

a = 0011, b = 1001;
start;
first loop;
1.1 sum = 1010
1.2 carry = 0010
1.3 carry != 0 , go on;
second loop;
2.1 sum = 1000;
2.2 carry = 0100;
2.3 carry != 0, go on;
third loop;
3.1 sum = 1100;
3.2 carry = 0000;
3.3 carry == 0, stop; result = sum;
end

有的加法操作是有连续进位的情况的,所以这里要在第三步检测carry是不是为0,如果为0则表示没有进位了,第一步的sum即为最终的结果。

代码实现
	/**
     * 功能描述:递归形式实现加法
     */
    int add(int a ,int b) {
        if (b == 0) {
            return a;
        } else {
        	//进位
            int carry = (a & b) << 1;
            //不进位加法
            a = a ^ b;
            return add(a, carry);
        }
    }

    /**
     * 功能描述:非递归形式实现加法
     */
    int add2(int a ,int b){
        int carry;
        while (b != 0){
            //进位
            carry = (a & b) << 1;
            //不进位加法
            a = a ^b;
            b = carry;
        }
        return a;
    }

减法

思路:

减法操作,可以利用加法操作实现。例如:a+b=a+(-b)。需要将b由正数转为负数(二进制方式),然后执行加法操作。

为什么不实现减法器的原因:

减法比加法来的复杂,实现起来比较困难。加法运算其实只有两个操作,加、 进位,而减法呢,减法会有借位操作,如果当前位不够减那就从高位借位来做减法,这里就会问题了,借位怎么表示呢?加法运算中,进位通过与运算并左移一位实现,而借位就真的不好表示了。所以我们自然的想到将减法运算转变成加法运算。

正数变为负数,二进制如何改变?

通过2的补码来表示负数的,将数字的正负号变号(即取反+1)

  • 第一步,每一个二进制位都取相反值,0变成1,1变成0(即反码)。
  • 第二步,将上一步得到的值(反码)加1。
代码实现
	/**
     * 功能描述:减法(加上一个负数,负数=正数取反+1)
     */
    int subtraction(int a ,int b){
    	//b由正数转为负数
        b = add(~b,1);
        return add(a,b);
    }

乘法

实现方式一——利用加法累加

思路

乘数加上乘数倍的自己,然后处理正负号的问题。

  • 处理乘数和被乘数的绝对值的乘积。
  • 根据它们的符号确定最终结果的符号。
代码实现
	/**
     * 功能描述:乘法(利用加法实现)
     * @param a 被乘数
     * @param b 乘数
     * @return 乘法结果
     */
    int multiplication(int a,int b){
        // 取绝对值,如果为负则取反加一得其补码,即正数  
        int multiplicand = a < 0 ? add(~a, 1) : a;
        int multiplier = b < 0 ? add(~b , 1) : b;
        // 计算绝对值的乘积  
        int product = 0;
        int count = 0;
        while(count < multiplier) {
            product = add(product, multiplicand);
            // 这里可别用count++,都说了这里是位运算实现加法
            count = add(count, 1);
        }
        // 确定乘积的符号  
        // 只考虑最高位,如果a,b异号,则异或后最高位为1;如果同号,则异或后最高位为0;
        if((a ^ b) < 0) {
            product = add(~product, 1);
        }
        return product;
    }
缺点

第一步对绝对值作乘积运算我们是通过不断累加的方式来求乘积的,这在乘数比较小的情况下还是可以接受的,但在乘数比较大的时候,累加的次数也会增多,这样的效率不是很高

实现方式二——求乘积

思路

以13*14为例
在这里插入图片描述
如果乘数当前位为1,则取被乘数左移一位的结果加到最终结果中;如果当前位为0,则取0加到乘积中(加0也就是什么也不做);

实现步骤
  1. 判断乘数是否为0,为0跳转至步骤4
  2. 将乘数与1作与运算,确定末尾位为1还是为0,如果为1,则相加数为当前被乘数;如果为0,则相加数为0;将相加数加到最终结果中;
  3. 被乘数左移一位,乘数右移一位;回到步骤1
  4. 确定符号位,输出结果;
代码实现
	/**
     * 功能描述:乘法(推荐)
     *  考虑符号问题
     * @param a 被乘数
     * @param b 乘数
     * @return 乘法结果
     */
    int multiplication(int a,int b){
        //将乘数和被乘数都取绝对值 
        int multiplicand = a < 0 ? add(~a, 1) : a;
        int multiplier = b < 0 ? add(~b , 1) : b;

        //计算绝对值的乘积  
        int product = 0;
        while(multiplier > 0) {
            // 每次考察乘数的最后一位  
            if((multiplier & 0x1) > 0) {
                product = add(product, multiplicand);
            }
            // 每运算一次,被乘数要左移一位    
            multiplicand = multiplicand << 1;
            // 每运算一次,乘数要右移一位(可对照上图理解)
            multiplier = multiplier >> 1;
        }
        //计算乘积的符号  
        if((a ^ b) < 0) {
            product = add(~product, 1);
        }
        return product;
    }

除法

实现方式一——利用减法累减

思路

除数去减被除数,直到被除数小于除数时,此时所减的次数就是我们需要的商,而此时的被除数就是余数。

注意

需注意的是符号的确定,商的符号和乘法运算中乘积的符号确定一样,即取决于除数和被除数,同号为正,异号为负;余数的符号和被除数一样。

代码实现
	/**
     * 功能描述:除法(减法实现)
     */
    int division(int a,int b){
        // 先取被除数和除数的绝对值
        int dividend = a > 0 ? a : add(~a, 1);
        int divisor = b > 0 ? a : add(~b, 1);

        int quotient = 0;// 商
        int remainder = 0;// 余数
        // 不断用除数去减被除数,直到被除数小于被除数(即除不尽了),直到商小于被除数
        while(dividend >= divisor){
            dividend = subtraction(dividend, divisor);
            quotient = add(quotient, 1);
        }
        // 确定商的符号,如果除数和被除数异号,则商为负数
        if((a ^ b) < 0){
            quotient = add(~quotient, 1);
        }
        // 确定余数符号
        remainder = b > 0 ? dividend : add(~dividend, 1);
        // 返回商
        return quotient;
    }
缺点

如果被除数非常大,除数非常小,那就要进行很多次减法运算,效率低。

实现方式二一增大步长使用减法累减

思路

所有的int型数据都可以用[2 ^ 0, 2 ^ 1,…,2 ^ 31]这样一组基来表示(int型最高31位)。不难想到用除数的[2 ^ 31,2 ^ 30,…,2 ^ 2,2 ^ 1,2 ^ 0]倍尝试去减被除数,如果减得动,则把相应的倍数加到商中;如果减不动,则依次尝试更小的倍数。这样就可以快速逼近最终的结果。

2的i次方其实就相当于左移i位,因为int型数据最大值就是2^31,所以从31位开始

代码实现
  	/**
     * 功能描述:除法(推荐)
     */
    int division(int a,int b){
        // 先取被除数和除数的绝对值
        int dividend = a > 0 ? a : add(~a, 1);
        int divisor = b > 0 ? a : add(~b, 1);
        // 商
        int quotient = 0;
        // 余数
        int remainder = 0;
        for(int i = 31; i >= 0; i--) {
            //比较dividend是否大于divisor的(1<<i)次方,不要将dividend与(divisor<<i)比较,而是用(dividend>>i)与divisor比较,
            //效果一样,但是可以避免因(divisor<<i)操作可能导致的溢出,如果溢出则会可能dividend本身小于divisor,但是溢出导致dividend大于divisor
            if((dividend >> i) >= divisor) {
                quotient = add(quotient, 1 << i);
                dividend = subtraction(dividend, divisor << i);
            }
        }
        // 确定商的符号
        if((a ^ b) < 0){
            // 如果除数和被除数异号,则商为负数
            quotient = add(~quotient, 1);
        }
        // 确定余数符号
        remainder = b > 0 ? dividend : add(~dividend, 1);
        // 返回商
        return quotient;
    }

下篇文章总结下位运算在算法解题中具体如何使用
(Java)算法——位运算在算法题中的应用

发布了23 篇原创文章 · 获赞 15 · 访问量 3730

猜你喜欢

转载自blog.csdn.net/qq_42937522/article/details/104662072