程序性能优化利器 - 位运算的使用技巧
前言
关于位运算,相信大家都不陌生,特别是写过一些对性能要求很严苛项目的同学,毕竟,这是一把提升程序性能效率的神兵利器。
我们都知道,程序中所有的数在计算机内存中都是以二进制的形式储存的,而位运算就是直接对整数在内存中的二进制位进行操作。比如,位与,位或,异或等。
本文着重于位运算的技巧总结,难度不会太大,掌握了还可以提高自己代码的逼格,但重点是要有耐心,理解位运算的作用。
正文
判断奇偶数
先看下,我们用高级语言提供的运算如何实现:
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,那么
- 将 (1) 代入(2),得: y = x ^ y ^ y = x ^ 0,结果为 x
- 将 (1)(2) 代入 (3), 得: x = x ^ y ^ x = y ^ 0, 结果为 y
找出没有重复的数
这个题目我在 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;}
m 的 n 次方
第一反应是啥,是不是这样?
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时将该位代表的乘数累乘到最终结果。
找出不大于N的最大的 2 的幂指数
先看看传统做法:
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;}