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

本文介绍了如何使用位运算技巧解决实际编程问题,包括判断整数奇偶性、求绝对值、不借助中间变量交换变量值,以及寻找数组中不成对的元素。通过位操作提升效率,深入理解二进制表示和异或运算在这些场景中的应用。

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

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

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

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

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

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

除阅读文章外,各位小伙伴还可以点击这里观看我在本站的视频课程学习Java!

一、考试基本要求面向对象程序设计考试是为了测试学生是否具备面向对象编程思想和初步应用Java语言编程的能力的考试。考生要求达到以下“知识”和“技能”两方面的目标。 【知识方面】:熟练掌握面向对象程序设计的基本概念,熟练掌握Java语言的编程语法和常用类库的使用。【技能方面】:能正确阅读和理解Java程序,能编写和调试一般功能需求的Java程序。二、考试方式、时间、题型及比例1.考试方式:闭卷笔试2.考试时间:150分钟3.题型比例:总分值为200分,题型为选择20%、简答题20%、程序阅读20%、程序综合设计40%。三、考试内容及考试要求第一章 Java语言概述 [知识要点]: Java语言的产生、应用前景和特点;Java虚拟机及Java运行系统; Java语言和C++语言的相同及不同之处;熟练掌握Java应用程序的编辑、编译和运行过程。 [考试要求]: 1.了解:Java 语言的产生、应用前景和特点; 2.了解:Java虚拟机及Java运行系统; 3.掌握:Java 语言和 C++语言的相同及不同之处; 4.掌握:Java 应用程序的编写、编译和运行过程。 第二章 Java 语言基础 [知识要点]:Java 语言的数据类型;变量和常量;正确书写表达式;数组;流程控制。 [考试要求]: 1.了解:数据类型的转换(自动类型转换和强制类型转换);运算符的优先级和结合性; 2.掌握:Java语言各种数据类型; 3.掌握:Java语言算术运算符、关系运算符、逻辑运算符、位运算符和复合赋值运算符的功能及使用; 4.掌握:Java语言变量、常量的使用及其运算操作; 5.掌握:Java语言流程控制语句的功能及使用;6.掌握:Java数组的定义;数组的初始化和数组的应用;二维数组的应用。 第三章 面向对象编程[知识要点]:面向对象的基本概念;面向对象的软件开发过程。 [考试要求]: 1.了解:面向对象的概念;2.掌握:类的创建与使用;3.掌握:方法的定义和使用;4.掌握:对象的基本操作方式;5.掌握:构造方法的定义和使用;6.掌握:this关键字和static关键字的使用;7.理解:成员变量和局部变量的区别。第四章 面向对象的特性 [知识要点]:掌握面向对象的三大特性。 [考试要求]: 1.理解:封装的概念; 2.理解:继承的概念;3.理解:多态的概念;4. 掌握:final关键字的使用5. 掌握:Lambda表示式的使用第五章 抽象类和接口 [知识要点]:抽象类与接口的基本概念以及实际应用。 [考试要求]: 1.掌握:抽象类和接口的使用;2.掌握:Java中的内部类; 2.了解:单例模式; 3.了解:模板设计方法; 第六章 Java异常 [知识要点]:Java异常的基本概念;Java异常处理机制;自定义Java异常类的应用。 [考试要求]: 1.理解:异常的概念; 2.掌握:异常的处理机制;3.掌握:自定义异常的使用第七章 Java常用类[知识要点]:应用Java语言的工具类库。 [考试要求]: 1.掌握:字符串相关类的使用; 2.掌握:System类与Runtime类的使用;3.掌握:Math类与Random类的使用;4.掌握:日期类的使用。第八章 集合框架[知识要点]:应用Java语言的集合框架解决具体问题。 [考试要求]: 1.掌握:List、Map、Set集合的使用; 2.掌握:集合遍历的方法;3.掌握:泛型的使用;4.掌握:集合工具类的使用;5.掌握:Stream API的使用。第九章 Java IO[知识要点]:Java 输入输出与文件处理。 [考试要求]: 1.掌握: File类及其用法; 2.掌握:操作字流和字符流读写文件;3.了解:其他IO流;4.了解:NIO的概念及其用法;5.了解:常见字符编码。第十章 图形用户界面[知识要点]:Java的Swing组件、容器、布局管理器的概念;图形界面上的事件响应。 [考试要求]: 1.了解: AWT组件和Swing组件的联系和区别; 2.掌握:常用的Swing组件的使用;3.理解:常用的窗体和布局管理器;4.掌握:事件处理机制。第十一章 Java多线程[知识要点]:多线程的基本概念;创建和启动线程;线程的生命周期;多线程同步问题;多线程通信;线程池的概念。 [考试要求]: 1.了解: 进程和线程的区别; 2.掌握:创建线程的方法;3.理解:线程的生命周期及其状态转换;4.掌握:多线程的同步;5.掌握:多线程之间的通信;6.了解:线程池的使用。第十二章 Java网络编程[知识要点]:网络协议;使用Java开发网络程序。 [考试要求]: 1.了解:网络通信协议; 2.了解:UDP通信;3.了解:TCP通信;4.掌握:网络程序的开发。第十三章 JDBC编程[知识要点]:数据库基本概念;JDBC原理;应用JDBC接口操作数据库。 [考试要求]: 1.了解:JDBC原理; 2.掌握:Connection接口、Statement接口、ResultSet接口、PreparedStatement接口的使用;3.掌握:使用JDBC操作数据库。四、其他说明 1.此次考试为笔试考试,不能使用任何的编程机器; 2.所有程序阅读题、编程题或算法设计题均采用程序填空题方式完成。五、参考书目李松阳、马剑威.Java程序设计基础与实战(微课版),人民邮电出版社,2022年8月. ISBN: 9787115591746 根据这个考纲帮我生成三套试卷
03-20
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穆哥讲Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值