位运算:直接对整数在内存中的二进位进行操作的运算
位运算包括与,或,非,异或,同或,移位等,位运算是最接近机器码的运算,在算法当中使用位运算会带来很大的便利
注:java十进制转二进制:Integer.toBinaryString(int n) ;
1.位运算与逻辑运算的区别
运算 | 位运算 | 逻辑运算 |
---|---|---|
与 | & | && |
或 | | | || |
非 | ~ | ~ |
2.与运算的使用
与 : &
指定位清零
指定位与0
与运算,其他位与1
做与运算 ,1&1 = 1
; 1& 0 = 0
//例:24 : 0001 1000 ,对第四位清零,结果为 0001 0000 = 16
24 & (~(1 << 3))
获取指定位的值
指定位与1
与操作,其他与0进行运算都为0
, 1&1 = 1
; 1& 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 = 1
; 0|1 = 1
,0|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
, 不同为1
,1 ^ 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
位,long
的64
位…)的时候,用二进制的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,k
为4
,则其中的最小值为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
开始即可
- 获取最小的集合。这一步的操作数为
n
,k
,值记为comb
:最小集合的数值为comb = (1<<k)-1
,如题所述这里采用0001 0111
最为最小值计算 - 将连续的
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
. - 对
comb
的连续1区间进行右移,直到1的个数减少一个。进行操作的数为comb
,结果为z
。对comb
右移直到1
的个数减1,所需要进行的操作有三步。首先获取comb
的连续1
区间,再将comb
右移知道最后一位的1
在最地位,然后再右移一位。1.获取comb
的连续1区间。y
与comb
的区别就在于连续区间的左侧都为1,则进行comb&~y
运算即可消去连续区间左边的1
。2.将最末尾的1移动到最地位,由于x是最后一位1的位置,所以再除以x
即可将末尾1移动到最低位。 3.再右移一位,即可获取到z
。最终的公式为( ( comb & ~y ) / x ) >> 1
z
与y
做或运算 ,则获取到的第一个子集为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;
}