接上篇
七、判断某数是不是2的N次幂
我们知道,10的0次幂是1,1次幂是10,2次幂是100...仔细观察一下这些数,你就会发现一个规律,那就是:这些数字当中,开头是1,后面N位上的数字全部是0。这是我们用十进制表示数字所得到的一个规律。同理,如果用二进制表示数字的话,那么对于2的N次幂也有相同的规律。用二进制表示2的0次幂为1,2的1次幂为10,2的2次幂为100...规律很明显,也1开头,后面N位都是0。
我们利用这个规律,就可以判断一个数字是不是2的N次幂。具体实现办法也很简单,假设一个数字为a,我们用”a减去1的值”与”a自身”做一个按位与运算,如果运算结果为0,那么说明a就是2的N次幂。为什么呢?就是因为2的N次幂减1之后,开头的那个1会因为“退位”变成0,而后面的0都会变成1,这样,与原来的a做按位与的运算,恰好是1和0相对,运算结果自然为0。
我们可以用一个具体的数字来证明,比如说2的3次方,也就是数字8,如果用二进制来表示的话就是”1000”,而8-1=7,7用二进制来表示就是”0111”,这两个二进制串的每一个位恰好是0和1相对,如果它们做按位与的运算,其结果恰好为0。我们根据这个运算结果是否为0就能判断出某个数是不是2的N次幂。完整的判断代码如下:
public static void main(String[] args) {
int a1 = 5;
int a2 = 8;
boolean f1 = (a1&(a1-1))==0;
boolean f2 = (a2&(a2-1))==0;
System.out.println(a1+"是2的N次幂:"+f1);
System.out.println(a2+"是2的N次幂:"+f2);
}
八、求一个集合的所有子集
所谓集合的子集,就是一个集合的部分元素所形成的集合。子集中有两种特殊情况,分别是空集和该集合自身。
一个包含n个元素的集合,恰好可以用一个n位的二进制数来表示它的每个元素有没有出现在子集中。0表示没出现,1表示出现。比如说一个集合{a,b,c},我们就可以用一个3位的二进制数来表示每个元素是否出现。“000”表示所有元素都没有出现,所形成的子集就是{}(空集),”001”表示a、b两个元素未出现,元素c出现,由此形成的子集为{c},以此类推,“010”所表示的子集为{b},”011”所表示的子集为{b,c},”100”所表示的子集为{a},”101”所表示的子集为{a,c},”110”所表示的子集为{a,b},”111”所表示的子集为{a,b,c}。
通过以上列举,可以看出:任意一个3位的二进制数s,都可以表示集合{a,b,c}的一个子集。现在我们做一个一般性推理,看看任意一个集合总共可以形成多少个子集。根据我们所学过的排列组合的知识,可以得知:假设集合的元素个数为n,可以形成2的n次方个子集。而2的n次方,用位运算的方式也可以表示为”1<<n”(一定要记住它哦,在程序中会出现)。
现在的关键问题是,如何根据s的值计算它所表示的子集中有哪几个元素呢?我们可以根据s当中”1”出现的位置来计算。接下来的问题就变成了:如何确定s中”1”出现的位置?我们可以设置一个初始值为“001”的二进制数x,并且逐位向左移动1的位置,并且与s做按位与操作,通过观察运算结果是否为0来判断s中”1”所出现的位置。
小伙伴们可能没有弄明白其中的原理,我们用具体的数字举例。假如s的值为”101”,x的初始值为“001”。
第1次测试:x左移0位得到1,s&1=1,结果不为0,说明s从右数的第1位上不为0,对应位置的”c”会出现在子集中。
第2次测试:x左移1位得到2,s&2=0,结果为0,说明s从右数的第2位上为0,对应位置的”b”不会出现在子集中。
第3次测试:x左移2位得到4,s&4=4,结果不为0,说明s从右数的第3位上不为0,对应位置的”a”会出现在子集中。
经过3次测试,说明”101”所代表的子集为{a,c}。由此可以推广为:依次把s的值设置为“000”,”001”,”010”...”111”,s取某一个值的时候,我们分别让x左移0位,1位和2位进行测试。
这里需要思考一下:为什么让x最多左移2位呢?就是因为我们现在讨论的集合{a,b,c}仅包含3个元素,如果用数组来表示的话,该数组下标最大值就是2。如果集合元素数量为n,则x最多左移n-1位。求子集的完整代码如下:
public static void main(String[] args) {
String[] set = {"a","b","c"};//以数组set表示一个集合
int n = set.length;//以n表示集合元素个数
for(int s=0;s<(1<<n);s++){//以s表示二进制数,以1<<n表示2的n次方
System.out.print("{");//先打一个左括号
for(int offset=0;offset<s;offset++){//offset表示x左移的位数
if((s&(1<<offset))!=0){//判断下标为offset的元素是否出现在子集中
System.out.print(set[offset]+" ");
}
}
System.out.println("}");//子集元素打印之后再打一个右括号
}
}
这段程序的运行结果如下:
运行结果可能有点出乎小伙伴们的预料。很多人认为元素”c”位于集合的最右边,按照程序运行的顺序,输出空集之后,第一个被输出的元素应该是”c”,但实际第一个输出的元素是”a”,这是为什么呢?就是因为:我们在排列集合的子集时,是把最右边的”c”当成第一个元素的,而程序实际运行的时候,是从左向右打印数组元素,也就是把最右边的”a”当成了第一个元素。其实无论从左向右数,还是从右向左数,所有的子集都会被列举出来。
至此,我们通过4篇文章介绍了位运算的8个经典应用。当然,位运算还在其他很多场合下有着非常广泛的应用,希望小伙伴们不要认为位运算只在应付面试的时候才能用到,掌握了位运算的技巧可以使得我们在开过程中发挥出事半功倍的效果。
(全文完)