位运算操作常见技巧

基本的位操作

  • 与( & )每一位进行比较,两位都为1,结果为1,否则为0(-4 & 1 = 0)
  1 0 0 1 1 -->(19)[10]  表示10进制中的19
& 1 1 0 0 1 -->(25)[10]
------------------------------
  1 0 0 0 1 -->(17)[10]
  • 或( | )每一位进行比较,两位有一位是1,结果就是1(-4 | 1 = -3)
  1 0 0 1 1 -->(19)[10]
| 1 1 0 0 1 -->(25)[10]
------------------------------
  1 1 0 1 1 -->(27)[10]
  • 非( ~ ) 每一位进行比较,按位取反(符号位也要取反)(~ -4 = 3)
~ 1 0 0 1 1 -->(19)[10]
-----------------------------
  0 1 1 0 0 -->(12)[10]
  • 异或( ^ )每一位进行比较,相同为0,不同为1(^ -4 = -3)
  1 0 0 1 1 -->(19)[10]
^ 1 1 0 0 1 -->(25)[10]
-----------------------------
  0 1 0 1 0 -->(10)[10]

异或运算的性质

//1. 任何值与0相异或,得到结果为其本身
//2. 相同的值异或为0
异或运算性质:

//交换律,即 a ^ b ^ c = a ^ c ^ b
//结合律, 即 (a ^ b) ^ c = a ^ ( b ^c )
//对于任何数,都有 a ^ a = 0, a ^ 0 = a
//自反性, a ^ b ^ b = a ^ 0 = a
  • 左移( << ) 整体左移,右边空出位补零,左边位舍弃 (-4 << 1 = -8)
int a = 8;
a << 3;
移位前:0000 0000 0000 0000 0000 0000 0000 1000 -->(8)[10]
移位后:0000 0000 0000 0000 0000 0000 0100 0000 -->(64)[10]  相当于 X 2^3
  • 右移( >> ) 整体右移,左边空出位补零或补1(负数补1,整数补0),右边位舍弃 (-4 >> 1 = -2)
unsigned int a = 8;
a >> 3;
移位前:0000 0000 0000 0000 0000 0000 0000 1000 -->(8)[10]
移位后:0000 0000 0000 0000 0000 0000 0000 0001 -->(1)[10] 相当于 / 2^3int a = -8;
a >> 3;
移位前:1111 1111 1111 1111 1111 1111 1111 1000 -->(-8)[10]
移位前:1111 1111 1111 1111 1111 1111 1111 1111 -->(-1)[10] 
  • 无符号右移( >>> )同>>,但不管正数还是负数都左边位都补0 (-4 >>> 1 = 2147483646)

初阶位操作

  • 交换两个数
int x = 1 , y = 2; 
x^=y;  // x = 0011
y^=x;  // y = 0001 -->(1)[10]
x^=y;  // x = 0010 -->(2)[10] 
//x =2, y =1  x变成y y变成x
  • 加1操作
int x =1;
x =-(~x); // 0001 --> (~x)后得到1110 --> -(~x) 后得到0010 也就是2,实现了+1操作
//x = 2
  • 减1操作
int x =2;
x =~(-x); // 0010 -->(-x)后得到1110 --> ~(-x) 后得到 0001 也就是1,实现了-1操作
  • 或(|)操作和空格转英文字符为小写字符
'a'|' '='a'  
// 'a' --> (1100001)[2] 二进制表示 其是(97)[10]
// ' ' --> (0100000)[2] 二进制表示 其是(32)[10]
  1100001 -->(97)[10]
| 0100000 -->(32)[10]
------------------------------
  1100001 -->(97)[10]    还是'a'本身
'A'|' '='a'  
// 'A' --> (1000001)[2] 二进制表示 其是(65)[10]
// ' ' --> (0100000)[2] 二进制表示 其是(32)[10]
  1000001 -->(65)[10]
| 0100000 -->(32)[10]
------------------------------
  1100001 -->(97)[10]    'A'-->'a'

常用的ASCII值

'0' : 48
'A' : 65
'a' : 97
  • 与(&)操作和下划线转英文字符为大写字符
'a'&'_'='A'  
// 'a' --> (1100001)[2] 二进制表示 其是(97)[10]
// '_' --> (1011111)[2] 二进制表示 其是(95)[10]
  1100001 -->(97)[10]
| 1011111 -->(95)[10]
------------------------------
  1000001 -->(65)[10]    'a'-->'A'
'A'&'_'='A'  
// 'A' --> (1000001)[2] 二进制表示 其是(65)[10]
// '_' --> (1011111)[2] 二进制表示 其是(95)[10]
  1000001 -->(65)[10]
| 1011111 -->(95)[10]
------------------------------
  1000001 -->(65)[10]    'A'-->'A'
  • 异或(^)操作与空格实现大小写英文字符互转
'a'^' '='A'  
// 'a' --> (1100001)[2] 二进制表示 其是(97)[10]
// ' ' --> (0100000)[2] 二进制表示 其是(32)[10]
  1100001 -->(97)[10]
^ 0100000 -->(32)[10]
------------------------------
  1000001 -->(65)[10]   'a'-->'A'
'A'^' '='a'  
// 'A' --> (1000001)[2] 二进制表示 其是(65)[10]
// ' ' --> (0100000)[2] 二进制表示 其是(32)[10]
  1000001 -->(65)[10]
^ 0100000 -->(32)[10]
------------------------------
  1100001 -->(97)[10]    'A'-->'a'
  • 两个数是否异号
int x =-1 , y =2;
x ^ y <0 // true 
  1111 -->(-1)[10]
^ 0010 -->( 2)[10]
------------------------------
  1101 -->(-3)[10]  //前面的1111省略掉了
    
int x =1 , y =2;
x ^ y <0 // false 
  0001 -->(1)[10]
^ 0010 -->(2)[10]
------------------------------
  0011 -->(3)[10]  
  • 判断奇数偶数:只要根据数的最后一位是 0 还是 1 来决定即可,为 0 就是偶数,为 1 就是奇数
0 == (a & 1) // true的时候是偶数 false时奇数
  • 交换符号:将正数变成负数,负数变成正数

整数取反加1,正好变成其对应的负数(补码表示);负数取反加一,则变为其原码,即正数

int reversal(int a) {
  return ~a + 1;
}
  • 位操作统计二进制中 1 的个数,相当于求「二进制的汉明重量」
统计二进制1的个数可以分别获取每个二进制位数,然后再统计其1的个数,此方法效率比较低。这里介绍另外一种高效的方法,同样以 34520 为例,我们计算其 a &= (a-1)的结果:
方法1- 第一次:计算前:1000 0110 1101 1000 计算后:1000 0110 1101 0000
- 第二次:计算前:1000 0110 1101 0000 计算后:1000 0110 1100 0000
- 第三次:计算前:1000 0110 1100 0000 计算后:1000 0110 1000 0000 我们发现,每计算一次二进制中就少了一个 1,则我们可以通过下面方法去统计:
    
count = 0  
while(a){  
  a = a & (a - 1);  
  count++;  
}  
方法2:见下面的「返回i的二进制最低位位1的权值」
count = 0  
while(a){  
  a -= a & (-a);  
  count++;  
}  
方法3:
count = 0  
while(a){  
  count+=a&1;  
  a >>>= 1;//无符号右移 相当于 /2 
}
  • 返回i的二进制最低位位1的权值
i & (-i)
10100最低位的1权值是4 00100[2]
1001010最低位的1权值是2
111最低位的1权值是1

进阶位操作

lowbit:x&(-x)

详解链接,一文掌握数据数组

枚举所有子集
    private void enumerateSubset() {
        int s, n = 5;
		//(1<<n)等价于1*2^n 
        for (s = 0; s < (1 << n); s++) {
            System.out.printf("%s--->", addZeroForNum(Integer.toBinaryString(s), 5));
            for (int i = 0; i < n; i++) {
                //1<<i 相当于拿2的整数倍数 1 2 4 8 16 这样
                if ((s & (1 << i)) != 0) {//不为0时表示true
                    System.out.printf("%d ", i);
                }
            }
            System.out.printf("\n");
        }
    }
//比如s = 7  ->二进制数为:00111 
//当i = 0,1,2时才输出s=7中的数 
//s&(1<<0) == 7&1 == 00111&001 == 1  输出 0
//s&(1<<1) == 7&2 == 00111&010 == 1  输出 1
//s&(1<<2) == 7&4 == 00111&100 == 1  输出 2 分别对应0、1、2下标索引

所有子集:

不选时: C 5 0 C_{5}^0 C50 1种
选1个元素时: C 5 1 C_{5}^1 C51 对应:0/1/2/3/4 一共5种
选2个元素时: C 5 2 C_{5}^2 C52 一共10种
选3个元素时: C 5 3 C_{5}^3 C53 一共10种
选4个元素时: C 5 4 C_{5}^4 C54 一共5种
选5个元素时: C 5 5 C_{5}^5 C55 一共1种

一共32种选择

00000--->
00001--->0 
00010--->1 
00011--->0 1 
00100--->2 
00101--->0 2 
00110--->1 2 
00111--->0 1 2 
01000--->3 
01001--->0 3 
01010--->1 3 
01011--->0 1 3 
01100--->2 3 
01101--->0 2 3 
01110--->1 2 3 
01111--->0 1 2 3 
10000--->4 
10001--->0 4 
10010--->1 4 
10011--->0 1 4 
10100--->2 4 
10101--->0 2 4 
10110--->1 2 4 
10111--->0 1 2 4 
11000--->3 4 
11001--->0 3 4 
11010--->1 3 4 
11011--->0 1 3 4 
11100--->2 3 4 
11101--->0 2 3 4 
11110--->1 2 3 4 
11111--->0 1 2 3 4 
枚举子集的优化

假设我们有一个用二进制数x表示的集合(某一位为1代表集合含有对应元素,反之则代表集合中不含对应元素),我们应该如何来枚举它的子集?

朴素的想法是,枚举所有小于等于该数的二进制数,逐个检查当前枚举到的y其是否是x的子集(是否满足 x ∣ y = x x∣y =x xy=x)。

最优解:

for (int j = x; j; j = (j - 1) & x) {
        // ...
}

分析:

j = (j - 1) & xj 减1 后,把j最右边的1变成了0,然后把之后所有的0变成了1,再与x 求 &,就保证了得到的结果是x的子集,并且是小于前一个j的二进制数中最大的一个。利用这一方式,我们可以倒序枚举出j的所有子集,并且中间不会经过任何不合法的状态。

如果我们对n个元素的所有子集进行子集的枚举,下面的两重循环可以在O(3^n)的时间复杂度内完成。

for (int i = 1; i < (1 << n); ++i) {
    for (int j = i; j; j = (j - 1) & i) {
        // ...
    }
}
模拟集合的操作

请添加图片描述

子集的遍历

//遍历u的非空子集
for(int s =u;s>0;s =(s-1)&u){
  //s是u的一个非空子集
}

用这种方法可以在 O ( 2 p o p c o u n t ( u ) ) O(2^{popcount(u)}) O(2popcount(u))popcount(u) 表示二进制中 1 的个数)的时间复杂度内遍历u的子集,进而可以在 O ( 3 n ) O(3^n) O(3n)的时间复杂度内遍历大小为n的集合的每个子集的子集。(复杂度为 O ( 3 n ) O(3^n) O(3n)是因为每个元素都有 不在大子集中/只在大子集中/同时在大小子集中 三种状态。)

现在来讲一讲为什么是这样的一个枚举方法?

先让我们来举一个例子来模拟一下。

假设我们当前要枚举的是

s =
  (10110)2     21(10)
  (10100)2     20(10)
  (10001)2     17(10)  
  (10000)2     16(10)
  (00101)2      5(10)
  (00100)2      4(10)
  (00001)2      1(10) 

根据例子,我们发现按照上面代码得到的结果是正确的,并且是把子集按照从大到小的顺序枚举出来的。那么接下来我们来谈谈这样枚举的正确性。

首先,一个集合它自己本身也是自己的一个集合,所以我们从这个集合本身开始枚举。

既然是枚举,那我们就先考虑把当前枚举得到的子集先−1,但是这样做不能保证−1后得到的状态是原状态的子集,但是我们注意到:根据与运算&的性质,我们不难发现如果两个数a , b , a < b ,我们对这两个数进行&运算,最后的结果一定是b的子集,因为我们与运算&得到的结果,在二进制中出现1 的位,b中一定也是1。

现在已经说明了这样做确实得到了原集合的子集,但是还没有说明我们已经枚举完了原集合的子集。

其实枚举子集就相当于在原集合的二进制状态下把一些1换为0,而我们每次−1然后进行与运算其实就是在把当前子集的最右边的1的右边全部变为1,自己变为0,然后进行与运算把新增的1中不该出现的抹去,最后只剩下了原集合中存在的1了。

在这里插入图片描述

运算符优先级

img

扩展

转换
    /**
     * 把2进制字符串key,转成10进制keys
     * 数字2代表进制,可以是各种进制,转换成10进制
     *
     * @param bin
     * @return
     */
    public static String bin2Ten(String bin) {
        String res = String.valueOf(Integer.parseInt(bin, 2));
        System.out.printf("binary:%s--->ten:%s\n", bin, res);
        return res;
    }
	

    private void testOne() {
        for (int i = 1; i <= 10; i++) {
            int positive = i;
            int negative = -i;
            String nStr = Integer.toBinaryString(negative);
            System.out.println(String.format("%d:%s\t%d:%s\t%s",
                    positive, addZeroForNum(Integer.toBinaryString(positive), 4)
                    , negative, nStr.substring(nStr.length() - 4),
                    addZeroForNum(Integer.toBinaryString(positive & negative), 4)
                    )
            );
        }
    }

	
    public static String addZeroForNum(String str, int strLength) {
        int strLen = str.length();
        if (strLen < strLength) {
            while (strLen < strLength) {
                StringBuffer sb = new StringBuffer();
                sb.append("0").append(str);// 左补0
                // sb.append(str).append("0");//右补0
                str = sb.toString();
                strLen = str.length();
            }
        }
        return str;
    }
链接

Reference

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值