程序性能优化利器 - 位运算的使用技巧

1,961 阅读6分钟

前言

关于位运算,相信大家都不陌生,特别是写过一些对性能要求很严苛项目的同学,毕竟,这是一把提升程序性能效率的神兵利器。

我们都知道,程序中所有的数在计算机内存中都是以二进制的形式储存的,而位运算就是直接对整数在内存中的二进制位进行操作。比如,位与,位或,异或等。

本文着重于位运算的技巧总结,难度不会太大,掌握了还可以提高自己代码的逼格,但重点是要有耐心,理解位运算的作用。

正文

判断奇偶数

先看下,我们用高级语言提供的运算如何实现:

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. 将 (1) 代入(2),得: y = x ^ y ^ y = x ^ 0,结果为 x
  2. 将 (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;
}

总结

位运算呢,其实都是通过整型数转换成二进制去进行运算,我们需要对各种位运算的作用比较熟悉,然后拿到一道题,首先转换成二进制,看一下它有什么特点,再根据它的特点去拟运算,一开始可能会觉得比较困难,但是多看一些例子,多动手,慢慢地就能信手拈来啦~各位小伙伴加油 ~

By the way

有问题?可以给我留言或私聊 有收获?那就顺手点个赞呗~

当然,也可以到我的公众号下「6曦轩」,

回复“学习”,即可领取一份 【Java工程师进阶架构师的视频教程】~

回复“面试”,可以获得: 【本人呕心沥血整理的 Java 面试题】

回复“MySQL脑图”,可以获得 【MySQL 知识点梳理高清脑图】

在这里插入图片描述
由于我咧,科班出身的程序员,php,Android以及硬件方面都做过,不过最后还是选择专注于做 Java,所以有啥问题可以到公众号提问讨论(技术情感倾诉都可以哈哈哈),看到的话会尽快回复,希望可以跟大家共同学习进步,关于服务端架构,Java 核心知识解析,职业生涯,面试总结等文章会不定期坚持推送输出,欢迎大家关注~~~