关于位运算,相信大家都不陌生,特别是写过一些对性能要求很严苛项目的同学,毕竟,这是一把提升程序性能效率的神兵利器。
我们都知道,程序中所有的数在计算机内存中都是以二进制的形式储存的,而位运算就是直接对整数在内存中的二进制位进行操作。比如,位与,位或,异或等。
本文着重于位运算的技巧总结,难度不会太大,掌握了还可以提高自己代码的逼格,但重点是要有耐心,理解位运算的作用。
先看下,我们用高级语言提供的运算如何实现:
if (n % 2) == 0) {
// n 是个奇数
} else {
// n 是个偶数
}
如果把 n 以二进制的形式展示的话,其实我们只需要判断最后一个二进制位是 1 还是 0 就行了,如果是 1 的话,代表是奇数,如果是 0 则代表是偶数,所以采用位运算的方式的话,将待判断的整数与 1 做位与操作,比如说 8 (1000) & 1 ,结果为 0,说明其为偶数,看下代码实现:
if (n & 1 == 1) {
// n 是个奇数。
} else {
// n 是个偶数
}
看到这里,可能有人会说,编译器不是会帮我们优化成位运算吗,但是,但是,算了,可以直接到 IDE 里跑一下看看时间效率的对比,哈哈哈哈。
交换两个数应该有很多人都写过,但是应该都是需要借助一个临时的辅助变量,代码如下:
int tmp = x;
x = y;
y = tmp;
那么,你可以不用辅助变量来实现变量的交换吗?Let’s see:
x = x ^ y // (1)
y = x ^ y // (2)
x = x ^ y // (3)
可能这里大家看了一脸懵吧(如果我猜想错了大家也别打我哈哈),还是解释一下,我们这里用的是异或运算,那么异或运算是什么呢,0 ^ 1 的时候结果才为 1,其余情况都为 0,所以异或运算有一个特性,是两个不相等的数做异或运算,结果为 1,那么
这个题目我在 LeetCode 中有遇到过,其他数都出现了两次,只有一个出现一次:
给你一组整型数据,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你来找出一个数
我们看看题干,所有的数据,只有一个数只出现一次,而其他的都是出现两次,只要这两个重点信息就可以了,通过前面的题目我们已经了解了,异或运算在两个相同的数异或时结果为 0,而其他数跟 0 异或则为自己本身,那使用位运算的解法就昭然若揭了:
int find (int[] arr) {
int tmp = arr[0];
for(int i = 1; i < arr.length; i++){
tmp = tmp ^ arr[i];
}
return tmp;
}
第一反应是啥,是不是这样?
int pow(int n){
int tmp = 1;
for(int i = 1; i <= n; i++) {
tmp = tmp * m;
}
return tmp;
}
确实是很常规的做法,效率也不是很高,但是想一想,Java 中的 Math.pow() 是不是也是这样实现的呢?答案是否定的,Math.pow() 是 native 修饰的方法,非 Java 实现,我也没有去看过具体的 C 代码,有兴趣的同学可以到 JDK 找到对应路径的类查看研究(感觉我好欠揍)。既然是 jdk 自己使用本地方法实现的,那效率肯定不会像上面那样(O(n)),要是那样,我们每个人都可以写 Java 语言了。那我们来看一下,使用位运算的实现思路:
int pow(int n){
int sum = 1;
int tmp = m;
while(n != 0){
if(n & 1 == 1){
sum *= tmp;
}
tmp *= tmp;
n = n >> 1;
}
return sum;
}
时间复杂度近为 O(logn),是不是效率上感觉提升了很多?离 jdk 大神又进了一步,哈哈哈哈~可能光看代码也要理解一段时间,
接下来加上文字描述,帮助大家理解代码,我们举个例子,比如 n = 15,则 n 的二进制表示为 1111,那么 m 的 15 次方可以拆解为:
m ^ 1111 = m ^ 0001 * m ^ 0010 * m ^ 0100 * m ^1000,
然后我们可以通过 & 1和 >>1 来逐位读取 1101,为1时将该位代表的乘数累乘到最终结果。
先看看传统做法:
int findN(int N){
int sum = 1;
while(true){
if(sum * 2 > N){
return sum;
}
sum = sum * 2;
}
}
再看看最大的 2 的幂指数,如果使用位运算的话要怎么使用,我们先看 2 的幂指数的二进制有什么特点,假设我们的数是 8 位二进制(整型一般是 32 位),那么 01000000, 00100000,00010000等等这些都是 2 的幂指数,即我们的目标,是要得到二进制数 1 的后面都为 0 的数。
目标有了,我们来看看实现的思路:
首先,我们把 n (假设为八位二进制)右边的数值全部变为 1
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
然后,再将 n + 1
n += 1;
最后,将 n 右移 1 位
n >> 1;
这样是不是就完成了找出一个数(比如 5 => 00000101,最大 2 的幂指数为 4)的最大 2 的幂指数的最大二进制数转化啦?附上完整代码:
int findN (int n) {
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
return (n + 1) >> 1;
}
位运算呢,其实都是通过整型数转换成二进制去进行运算,我们需要对各种位运算的作用比较熟悉,然后拿到一道题,首先转换成二进制,看一下它有什么特点,再根据它的特点去拟运算,一开始可能会觉得比较困难,但是多看一些例子,多动手,慢慢地就能信手拈来啦~各位小伙伴加油 ~
有问题?可以给我留言或私聊
有收获?那就顺手点个赞呗~
当然,也可以到我的公众号下「6曦轩」,
回复“学习”,即可领取一份
【Java工程师进阶架构师的视频教程】~
回复“面试”,可以获得:
【本人呕心沥血整理的 Java 面试题】
回复“MySQL脑图”,可以获得
【MySQL 知识点梳理高清脑图】
由于我咧,科班出身的程序员,php,Android以及硬件方面都做过,不过最后还是选择专注于做 Java,所以有啥问题可以到公众号提问讨论(技术情感倾诉都可以哈哈哈),看到的话会尽快回复,希望可以跟大家共同学习进步,关于服务端架构,Java 核心知识解析,职业生涯,面试总结等文章会不定期坚持推送输出,欢迎大家关注~~~
近期会通过面试问答的形式为大家推送面试相关的知识,如果有什么建议可以在公众号留言~