数据结构-位运算的使用

位运算:直接对整数在内存中的二进位进行操作的运算

位运算包括与,或,非,异或,同或,移位等,位运算是最接近机器码的运算,在算法当中使用位运算会带来很大的便利

注:java十进制转二进制:Integer.toBinaryString(int n) ;

1.位运算与逻辑运算的区别

运算位运算逻辑运算
&&&
|||

2.与运算的使用

与 : &

指定位清零

指定位与0与运算,其他位与1做与运算 ,1&1 = 11& 0 = 0


//例:24 : 0001 1000 ,对第四位清零,结果为 0001 0000 = 16

24 & (~(1 << 3))

获取指定位的值

指定位与1与操作,其他与0进行运算都为01&1 = 11& 0 = 0 ,再将指定位移动到最低位,即可获取指定位的值


//24 :0001 1000 获取第四位的值, 结果为1

24 & ((1 << 3))) >> 3

保留某些位不变,清零其它位

指定位置置1 ,其他置0,与运算后只有指定位的值为原始值,其他都为0 ,即可达到目的


//24 : 0001 1000 ,保留第三位不变其他清零
24 & ((1 << 3)

判断一个数是否是2的次幂

如果一个数是2的次幂,则其二进制中只有一位为1其他为0,将其减1获得的值刚好1全部变为1,1变为0,此时做与运算,则结果为0,即可通过结果是否为0判断是否为2的次幂


//24 : 0001 1000  23 : 0000 0111 ,  24 & 23 = 0001 1000 & 0001 0111 = 16

24&(24-1)     //结果不为0 , 不是2的次幂

//64 : 0100 0000    63 : 0011 1111  , 64 & 63 = 0100 0000 & 0011 1111 =0

64 & (64-1)   //结果为0 , 是2的次幂

判断一个数是否是4的次幂

今早在leetcode看到了这个题,就顺便加一下:Power of Four

要判断是否是4的次幂,首先第一条件是要满足2的次幂,也就是只存在一位为1,其次是1的位置,必须是偶数的(16 , 4 , 1 的二进制分别为 0001 0000 , 0000 0010 , 0000 , 0001 ),以此类推可以得到.....0101 0101

的单元素子集一定是4的次幂,再已经判断是2的次幂满足的之存在单个1的情况下,与......0101 0101做与运算,结果为其本身,则1的位置是偶数位,即为4的次幂

//(需要排除0的存在)
public class Solution {
    public boolean isPowerOfFour(int num) {
        if(num<1){
            return false ;
        }
        return ((num & (num - 1)) == 0)&&((num & 0x55555555) == num);
    }
}

判断奇偶

如果一个数为奇数, 则 二进制最后一位一定为1 ,与1与运算,则结果为1 ,如果为偶数,最后一位为0 ,与1与运算结果为0


//24 : 0001 1000 

24&1 // 结果为0 ,是偶数

3.或运算的使用

设置指定位为1

指定位与1进行或运算,其它位与0进行或运算 ,1|1 = 10|1 = 10|0 = 0,即可得到结果


//24 :0001 1000 将第三位设置为1

24|(1<<2) //结果为 30 : 0001 1100

4.非运算的使用

获取负数

获取一个数的负数,就是让这个数各位取反然后末尾加1


-24 = ~24+1

5.异或操作的使用

无第三个参数交换两个数的值

再不使用第三个 参数的情况下交换两个数的值,使用的原理是一个数对另一个输异或两次,得到的是其本省


int a = 10 , b = 20 ;

a = a ^ b ;

b = a ^ b ;

a = a ^ b ;

//结果 : a = 20 , b = 10

6.移位操作的使用

扩大或者缩小2的幂次

左移符号 : <<

右移符号 : >>

无符号右移: >>> (java特有)

扩大或缩小指定倍数,其实质就是将一个数想坐或者向右移动指定位数,但要注意的是负数右移是带符号位的


//24 扩大4倍

 24<<2  // 结果96

//24减小4倍

24 >> 2    //结果6

循环移位

为了在移位过程中不丢失数据,采用循环移位的操作


//a循环左移k位

a << k | a >> 32 - k

//a循环右移k位

a >> k | a << 32 - k

7.位运算的简单应用

求平均数

求 x , y 的平均数,一般有两种方法

第一种,常规法 : (x + y ) / 2

第二种 , 二分法 : x + ( y - x ) /2

第一种方法存在缺陷,对于较大的两个数会产生溢出 ,第二种方法不会

用位运算进行求平均数可可以完全避免溢出,因为是使用逻辑电路中的半加器的原理来实现的,二进制的加法操作分为本位和进位,本位顾名思义就是加法操作之后还留在本来位置的数,进位就是要向前进一位的数,尔根据二进制加法的性质来理解,本位就是两个数进行异或操作,相同为0 , 不同为11 ^ 1 = 0 , 1 ^ 0 = 1 .而进位则是由与下操作来实现的 , 1 & 0 = 0 (不进位) , 1 & 1 = 1(进位) ,获取本位和进位的数据之后,将本位右移一位与本位相加即为平均值 。(至于为何本位要右移,那是因为原本应该进位是要加到上一位的,应该跟进位左移一位是一样的效果),则最终的公式为(x&y)+( (x ^ y ) >> 1 )


//int 最大值为 2147483247

int x = Integer.MAX_VALUE, y = Integer.MAX_VALUE - 10;

((x + y) / 2) )         //结果为:-6  常规方法,已溢出
x + (y - x) / 2          //结果为 2147483242     未溢出

((x^y)>>1)+(x&y)   //结果为 2147483242     未溢出

求绝对值

公式为:

int y = x >> 31

( x + y ) ^ y

理解为:int y = x >> 31 实际上是获取x的符号位,当x为正数,y = 0, 当x为负数 , y = -1 ( 右移是带符号的),当y = 0 , ( x + y ) ^ y = ( x + 0 ) ^ 0 = x , 当 y = -1 , ( x + y ) ^ y = ( x -1 ) ^ -1 = ~ ( x + 1 ) = -x


//24的绝对值   24 : 0001 1000 

int y = 24 >>31 = 0 ;

( 24 + 0 ) ^ 0 = 24

// -24 的绝对值 -24 : 000 1 1000 (省略1开头的符号位)

int y = -24 >> 31 = -1

(-24 + -1 ) ^ -1 = -(-24) = 24

获取最大值

公式如下:

x ^ ( ( x ^ y ) & ( ( x - y ) >> 31 ) )

解释:( x - y ) >> 31 实际上就是获取 x - y 的符号位,当 x > y时,符号位为 0 ,则原公式变为 x ^ ( (x ^ y ) & 0 ) = x ^ 0 = x,当x< y,符号位为-1 , 则原公式变为X ^ ( (x ^ y ) & -1 ) = x ^ (x ^ y ) = y

10^((10^20)&(10-20)>>31))
//结果为20

8.集合中的位运算

当元素的个数较少(因为int型的二进制一共32位,long64位…)的时候,用二进制的0,1来表示一个元素是否出现在集合中,那么一个集合就可以用一个二进制数来表示

空集

0

只含第i个元素

1<<i

含有全部元素

n<<n-1

判断第i个元素是否在集合中

S&(1<<i) ==0说明不在

往集合中加入第i个元素

S|(1<<i)

集合中除去第i个元素

S & ( ~ 1 << i )

两个集合的并集

S | R

两个集合的交集

S & R

遍历所有(n个元素的子集一共有2^n个)

for ( int i = 0 ; i < ( 1 << n ) ; i ++ ) {
    //..对i的二进制进行操作  
    //Integer.toBinaryString(i);
}

遍历一个集合的所有子集

基本的思路是删去若干个1来实现,实现方式就从删除最后一位的1开始遍历,知道第一位的1被删除为止,即可遍历所有的情况(可能有人会疑惑只是删去1的话遍历不到所有的情况,其实这里的删除只是简化了,具体的操作是对当前位减1,则当前位1变为0,后面的0又会变成1,循环往复即可获取所有情况)

首先看一个伪代码:

int sub = s  ; //s为所需要求子集的那个集合
do{

    //..对s集合的处理
    sub = ( sub - 1 ) & s ;//原理是前面的求2的次幂,每次都会把末尾的1消去
    }while(sub != s); //直到sub==0 ,则

Java代码

//获取s的所有子集
    public void getAll(int s) {
        int sub = s ;
        do {
            System.out.println(Integer.toBinaryString(sub));
            sub = (sub - 1) & s;
        } while (sub != s);
    }

遍历指定元素个数的所有集合

这个算法相比较上一个来说更抽象一些,看了好几遍才理解,但是想用一种非常通俗易懂简单的语言来描述还是不太行,只能说一下自己的理解思路:

n个元素的集合中只包含k个元素的子集,公式如下

这里写图片描述

假设n为8,k4,则其中的最小值为0000 1111,这个数的上一个数为最高位的1左移一位0001 0111,这个结果略微思索一下应该能想通,是怎么来获取这个数的呢,原理是如果四个1原本是连续的,那么他的上一个数不可能还是连续的(那样相当于乘2),而是中间出现0,那么0的位置出现在高位时候的值明显要比出现的低位的时候要小,当然,0的个数也决定着相对于原连续1的数来说乘了几个2(近似来说是倍数,因为1不再连续),以此类推,再上一个数应该是0001 1011,再上一个0001 1101….直到0001 1110,此时再上一个数就必须再多一个0,为0010 1110,然后 0011 0110,,0011 1010….直到0去到末尾….这样每次都能获取一个比上一个数大的数,最大值为1111 0000,当然,要保证从最小数遍历到最大数,必须满足的是首先开始的集合为最小的那个。

思路大概就这些,希望能看懂。但是看懂思路跟,根据思路自己组合位运算还是有很多的工作要做,那么就先来看位运算的算法,理解起来很容易,如下:

使用文字和代码一起叙述,仍旧采用n==8,k==4来计算,为了展示某些步骤的作用,把开始遍历的集合选为0001 0111,实际使用中记得从最小的0000 1111开始即可

  1. 获取最小的集合。这一步的操作数为 n,k,值记为comb:最小集合的数值为comb = (1<<k)-1,如题所述这里采用0001 0111最为最小值计算
  2. 将连续的1区间全部置为0,且左边的0置为1。这一步的操作数为comb值记为y:即把0001 0111变为0001 1000,这里的计算思路是在最低位的1上加1即可得到结果:需要两步完成,先获取最低位1的值,然后相加即可。1.获取最低位1的值:比如对于0001 1110,获取到的值应该是0000 00010,位运算操作为x= comb & -comb,原理为-comb = ~comb + 1,所有位取反之后在末尾加1,产生的进位就会指示到原末尾为1的位置上(x=0001 1110 & 1110 0010 = 0000 0010)。2.与comb相加 , y = 0001 0111 +x =0001 1000.
  3. comb的连续1区间进行右移,直到1的个数减少一个。进行操作的数为comb,结果为z。对comb右移直到1的个数减1,所需要进行的操作有三步。首先获取comb的连续1区间,再将comb右移知道最后一位的1在最地位,然后再右移一位。1.获取comb的连续1区间。ycomb的区别就在于连续区间的左侧都为1,则进行comb&~y运算即可消去连续区间左边的1。2.将最末尾的1移动到最地位,由于x是最后一位1的位置,所以再除以x即可将末尾1移动到最低位。 3.再右移一位,即可获取到z。最终的公式为( ( comb & ~y ) / x ) >> 1
  4. zy做或运算 ,则获取到的第一个子集为comb = z|y,再进行第一步操作,直到comb > 1111 0000

看一下伪代码

int comb = ( 1 << k ) -1 ;//获取最小集合
while( comb < ( 1 << n ) ) {
    //对子集的处理
    int x = comb & - comb ;//获取最末尾1的位置
    int y = comb + x ; // 第二步
    int z =( ( comb & - y ) / x ) >> 1 ;
    comb = z | y ;
}

Java代码

//获取含k个元素的所有子集
public class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> lists = new ArrayList<>();
            int comb = (1 << k )- 1; 
            while (comb < (1 << n)) {
                lists.add(getIndex(comb,n));
                int x = comb & -comb; 
                int y = comb + x;             
                int z = ((comb & ~y) / x) >> 1; 
                comb = z | y;
            }

            return lists;
    }
    private List<Integer> getIndex(int comb, int n) {
        List<Integer> list = new ArrayList<Integer>();
        char[] chars = new char[n];
        for (int i = 0; i < n; i++) {
            chars[i] = '0';
        }
        char[] result = Integer.toBinaryString(comb).toCharArray();
        for (int i = n-result.length, j = 0; i < chars.length&j<result.length; i++,j++) {
            chars[i] = result[j];
        }
        for (int i = 0; i < chars.length; i++) {
            if (chars[i] == '1') {
                list.add(i+1);
            }
        }
        return list;
    }
}

LeetCode也有这道题:[Combinations](https://leetcode.com/problems/combinations/)

位运算的高级应用

判断二进制中1的个数

给定一个整型数n,判断其二进制中1的个数
方法:

这里写图片描述

第一种:转二进制,获取字符串后判断1出现的次数
//获取二进制字符串然后统计1的个数 ,最多32次,最少1次
    public int num(int n) {
        int count = 0;
        String binary = Integer.toBinaryString(n);
        char[] chs = binary.toCharArray();
        for (int i = 0; i < chs.length; i++) {
            if (chs[i] == '1') {
                count++;
            }
        }
        return count;
    }
第二种:右移位,判断是否最低位为1:
//右移操作 最多32次最少1次
    public int num(int n) {
        int count = 0;
        while (n != 0) {
            count += n & 1; //如果结果为1,说明最低一位为1
            n >>= 1; //右移
        }

        return count;
    }
第三种:2的次幂判断法(每次会消去一个1)
public int num1(int n) {
        int count = 0;
        while (n != 0) {
            n = n & (n - 1);
            count++;
        }
        return count;
    }
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值