实际开发过程中,位运算有着相当广泛的应用,并且相对于算术运算,位运算的计算速度往往更快。本节就讲解一些使用位运算解决问题的经典例子。
8.4.1判断整数的奇偶性
按照传统的思路,判断一个整数的奇偶性是通过用这个数与2求模,看运算结果是否为0。其实使用位运算也能判断整数的奇偶性。我们知道:Java语言中,所有数字存储在内存中,都要先转换成补码的形式。任何一个偶数用补码表示出来后,它的最后一个二进制位都是0,而奇数补码的最后一个二进制位都是1。假设要判断奇偶性的整数是a,我们就可以通过判断a的补码的最后一位二进制数是0还是1从而判断出a是偶数还是奇数。判断的方法就是用a与1进行按位与的操作,如果结果为0,那么a就是偶数,如果结果为1,a就是奇数。这个判断原理可以用图8-23说明:
图8-23 判断整数奇偶性原理
图8-23中以横线为界,分别展示了a为偶数和奇数的情况下判断奇偶性的运算结果。以下【例08_05】展示了用位运算判断整数奇偶性的完整实现过程:
【例08_05 判断整数奇偶性】
Exam08_05.java
8.4.2求整数绝对值
想要理解位运算求绝对值的原理,必须先知道如何通过位运算求出这个数自身以及它相反数的方法。我们知道:任何一个二进制位上的数,与0进行异或运算,运算的结果都与这个二进制位上的数相同。把这个结论扩展一下,从原来某个数的单独的一个二进制位扩展到这个数字本身,可以得出:任何一个整数与0进行按位异或运算后得到的就是这个整数自身。例如整数5与0进行按位异或运算的结果仍然是5。
另外,整数-1如果用补码来表示的话是32位全为1的二进制数。因此-1与任何一个整数进行按位异或运算,都可以达到“取反”的效果。按照补码的计算规则,一个正数按位取反后再加1,得到的就是它相反数。比如图中的数字5,按位取反后再加1得到的就是-5。
前文曾讲过:int型的正数经过带符号右移31位之后,得到的必然是0,而负数经过带符号右移31位得到的是-1。因此可以通过右移运算所得到的这个0或者-1,判断出这个数是正数还是负数。知道数字的正负属性,然后再用位运算的方式得到这个数本身或者是它的相反数,就能求出这个数的绝对值。下面的【例08_06】展示了用位运算求整数绝对值的完整实现过程。
【例08_06】用位运算求整数绝对值的完整实现过程
Exam08_06.java
8.4.3 不借助中间变量交换两个变量的值
在第2.6.1小节中,曾经讲过交换两个变量值的实现方法,但实现过程中需要借助中间变量。如果使用位运算进行操作,不用借助中间变量就能实现交换两个变量的值。前文讲过:a^b^b的运算结果等于a。为了表述方便,把a^b的操作称为“用b对a加密”,之所以这么称呼,就是因为a与b进行了异或运算之后,得到一个全新的值,效果如同对a加密一样。另外,把a^b^b的操作称之为“还原”, 之所以这么称呼,就是因为a^b^b的运算结果等于a,如同是把a的值“加密”之后又进行了还原,恢复了a的值。使用位运算交换两个变量的值,就是利用加密和还原操作实现的。下面的【例08_07】展示了不借助中间变量交换两个变量值的完整实现过程。
【例08_07 不借助中间变量交换两个变量值】
Exam08_07.java
【例08_07】中实现变量交换值的关键代码是语句①、②、③。为方便表述,此处把a和b最初的值称为“原始a”和“原始b”。语句①用b对a进行了加密操作,并且又赋值给了变量a,此时变量a就由原始数据变成了加密后的值。语句②把加密后的值与原始b进行异或运算,这样就还原了原始a的值,紧接着把这个值赋值给b,这样变量b中就存储了原始a的值。语句③用加密后的值与现在的b,也就是保存了原始a的变量进行异或操作,就能得到原始b的值,之后再把原始b的值赋值给变量a,这样就完成了变量a与b值的交换。
8.4.4 寻找不成对的元素
一个数组中,某个数只出现了一次,而其他数都出现了两次,要求编写程序把那个只出现了一次的数找出来。
前文讲过: 两个相同的数字进行异或运算,结果为0,而任何一个整数与0进行异或运算,其结果都是这个数本身。此外,任意N个整数进行异或操作,满足交换律。因此,只需要把数组中所有的元素都做一遍异或操作,所得到的值就是那个只出现了一次的数字。因为出现了两次的数字,它们之间进行异或操作会变为0。即使这两个数字没有挨在一起,但根据异或运算的交换律可以知道:位置关系并不影响运算结果,所以两个相同的数字只要都参与了异或运算,最终的结果都是0。而那个只出现了一次的数字,与0进行异或操作,其结果仍然是它自身的值。所以,异或运算的结果其实就是那个只出现了一次的数字。下面的【例08_08】展示了找到数组中不成对元素的完整实现过程。
【例08_08 找出不成对的元素】
Exam08_08.java
这道题目还有另一个版本:有整型数组a和b,a数组中所有元素都出现在b数组中,但b数组比a数组多出一个元素,编写程序找到b数组中多出来的这个元素。这个版本中虽然出现了两个数组,但实现算法的思路并没有发生变化,只是由原来的一个数组全部元素参与异或运算,变成了两个数组中的元素都要参与异或运算,下面的【例08_09】展示了解答这个题目的完整实现过程:
【例08_09找出不成对的元素之版本2】
Exam08_09.java
8.4.5求集合的所有子集
所谓集合的子集,就是一个集合的部分元素所形成的集合。其中空集和该集合自身也属于这个集合的子集。
一个包含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的值计算它所表示的子集中有哪几个元素呢?因为二进制数中的1表示元素出现,所以只要根据s当中1出现的位置就能算出子集中有哪些元素。接下来的问题就是:如何确定s中1出现的位置?我们可以设置一个初始值为001的二进制数x,让x和s做按位与运算,并记录运算结果。之后对x进行1位左移操作,这样就相当于向左移动了1的位置。左移x后再次与s进行按位与操作,并记录运算结果,以此类推,直到x中的1移动到做左边为止。只要观察x与s按位与运算的结果是否为0就能判断s中1所出现的位置,这个判断的原理如图8-25所示。
图8-25 判断二进制数中1的位置原理图
此处用字母i表示x中1的位置。从图8-25可以看出:如果x和s的第i位上都是1,那么x和s按位与的结果必定不为0,因此可以根据按位与的结果是否为0来判断s的第i位是否为1。例如,1在x中处于第1位时,x和s按位与的结果不为0,可以推出s的第1位上是1。同理,1在x中处于第3位时,x和s按位与的结果不为0,可以推出s的第3位上是1。通过这种方式就能判断出s当中1出现的位置,而知道了s中1的位置后,就能对应推出二进制数s代表的子集中包含哪些元素。下面的【例08_10】展示了求集合的所有子集的完整实现过程。
【例08_10 求集合的所有子集】
Exam08_10.java
【例08_10】的运行结果如图8-26所示。
图8-26 【例08_10】运行结果
程序运行结果可能有点出乎读者预料。很多人认为元素“c”位于集合的最右边,按照程序运行的顺序,输出空集之后,第一个被输出的元素应该是“c”,但实际第一个输出的元素是“a”,这是因为:我们在排列集合的子集时,是把最右边的“c”当成第一个元素的,而程序实际运行的时候,是从左向右打印数组元素,也就是把最左边的“a”当成了第一个元素。其实无论从左向右数,还是从右向左数,所有的子集都会被列举出来。
除此文字版教程外,小伙伴们还可以点击这里观看我在本站的视频课程学习Java。