《Java高级语法》第4节:位运算经典应用举例

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Java高级语法》第4节:位运算经典应用举例相关的知识,希望对你有一定的参考价值。

​实际开发过程中,位运算有着相当广泛的应用,并且相对于算术运算,位运算的计算速度往往更快。本节就讲解一些使用位运算解决问题的经典例子。

8.4.1判断整数的奇偶性

按照传统的思路,判断一个整数的奇偶性是通过用这个数与2求模,看运算结果是否为0。其实使用位运算也能判断整数的奇偶性。我们知道:Java语言中,所有数字存储在内存中,都要先转换成补码的形式。任何一个偶数用补码表示出来后,它的最后一个二进制位都是0,而奇数补码的最后一个二进制位都是1。假设要判断奇偶性的整数是a,我们就可以通过判断a的补码的最后一位二进制数是0还是1从而判断出a是偶数还是奇数。判断的方法就是用a与1进行按位与的操作,如果结果为0,那么a就是偶数,如果结果为1,a就是奇数。这个判断原理可以用图8-23说明:​

第八章《Java高级语法》第4节:位运算经典应用举例_Java

图8-23 判断整数奇偶性原理​

图8-23中以横线为界,分别展示了a为偶数和奇数的情况下判断奇偶性的运算结果。以下【例08_05】展示了用位运算判断整数奇偶性的完整实现过程:​

【例08_05 判断整数奇偶性】

Exam08_05.java​

import java.util.Scanner;
public class Exam08_05
public static void main(String[] args)
int a;
Scanner sc = new Scanner(System.in);
System.out.println("请输入整数a的值");
a = sc.nextInt();
String result = (a&1)==0?"偶数":"奇数";
System.out.println("a为"+result);

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​

import java.util.Scanner;
public class Exam08_06
public static void main(String[] args)
int a,abs;//对变量a求绝对值,abs表示求绝对值的结果
Scanner sc = new Scanner(System.in);
System.out.println("请输入整数a的值");
a = sc.nextInt();
int i = a>>31;//右移31位,a是正数时i值为0,a负数时i值为-1
abs = (a^i)-i;//i为0且a为正数得到a自身,i为-1且a为负数得到a的相反数
System.out.println(a+"的绝对值是:"+abs);

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​

import java.util.Scanner;
public class Exam08_07
public static void main(String[] args)
int a,b;//交换变量a和b的值
Scanner sc = new Scanner(System.in);
System.out.println("请输入整数a的值");
a = sc.nextInt();
System.out.println("请输入整数b的值");
b = sc.nextInt();
System.out.println("交换前:a=="+a+",b=="+b);
a = a^b;//①得到a加密后的数据并赋值给变量a
b = a^b;//②还原了原始a,并赋值到变量b中
a = a^b;//③还原了原始b,并赋值到变量a中,完成交换
System.out.println("交换后:a=="+a+",b=="+b);

【例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​

public class Exam08_08 
public static void main(String[] args)
int[] array = 1,3,4,2,3,1,4;
int x = 0;
for(int i=0;i<array.length;i++)
x = x^array[i];//全部数组元素参与异或运算

System.out.println("不成对的元素是:"+x);

这道题目还有另一个版本:有整型数组a和b,a数组中所有元素都出现在b数组中,但b数组比a数组多出一个元素,编写程序找到b数组中多出来的这个元素。这个版本中虽然出现了两个数组,但实现算法的思路并没有发生变化,只是由原来的一个数组全部元素参与异或运算,变成了两个数组中的元素都要参与异或运算,下面的【例08_09】展示了解答这个题目的完整实现过程:​

【例08_09找出不成对的元素之版本2】

Exam08_09.java​

public class Exam08_09 
public static void main(String[] args)
int[] a = 11,34,9,-4,100,98;
int[] b = 34,55,11,9,100,-4,98;
int x = 0;
for(int i=0;i<a.length;i++)
x = x^a[i]^b[i];//两个数组中的元素都要参与异或运算

//数组b中多一个元素,还要让数组b中最后一个元素也参与到异或运算中
x = x^b[b.length-1];
System.out.println("不成对的元素是:"+x);

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所示。​

第八章《Java高级语法》第4节:位运算经典应用举例_Java_02

图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​

public class Exam08_10 
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)//判断二进制数s的第offset+1位是否为1
System.out.print(set[offset]+" ");//打印第offset+1个元素


System.out.println("");//子集元素打印之后再打一个右括号


【例08_10】的运行结果如图8-26所示。​

第八章《Java高级语法》第4节:位运算经典应用举例_Java_03

图8-26 【例08_10】运行结果​

程序运行结果可能有点出乎读者预料。很多人认为元素“c”位于集合的最右边,按照程序运行的顺序,输出空集之后,第一个被输出的元素应该是“c”,但实际第一个输出的元素是“a”,这是因为:我们在排列集合的子集时,是把最右边的“c”当成第一个元素的,而程序实际运行的时候,是从左向右打印数组元素,也就是把最左边的“a”当成了第一个元素。其实无论从左向右数,还是从右向左数,所有的子集都会被列举出来。

除此文字版教程外,小伙伴们还可以点击这里观看我在本站的视频课程学习Java。

以上是关于《Java高级语法》第4节:位运算经典应用举例的主要内容,如果未能解决你的问题,请参考以下文章

Java千问:Java位运算经典应用

Java千问:Java位运算经典应用

《Java高级语法》第7节:枚举

《Java高级语法》第12节:Lambda表达式

《数组与循环》第8节:数组与循环经典例题

位运算的运用场景使用总结