位算法的效率有多快我就不说,不信你能够去用 10 亿个数据模拟一下,今天给你们讲一讲位运算的一些经典例子。不过,最重要的不是看懂了这些例子就好,而是要在之后多去运用位运算这些技巧,固然,采用位运算,也是能够装逼的,不信,你往下看。我会从最简单的讲起,一道比一道难度递增,不过竟然是讲技巧,那么也不会太难,相信你分分钟看懂。面试
判断一个数是基于仍是偶数,相信不少人都作过,通常的作法的代码以下算法
if( n % 2) == 01
// n 是个奇数
}
复制代码
若是把 n 以二进制的形式展现的话,其实咱们只须要判断最后一个二进制位是 1 仍是 0 就好了,若是是 1 的话,表明是奇数,若是是 0 则表明是偶数,因此采用位运算的方式的话,代码以下:bash
if(n & 1 == 1){
// n 是个奇数。
}
复制代码
有人可能会说,咱们写成 n % 2 的形式,编译器也会自动帮咱们优化成位运算啊,这个确实,有些编译器确实会自动帮咱们优化。可是,咱们本身可以采用位运算的形式写出来,固然更好了。别人看到你的代码,我靠,牛逼啊。无形中还能装下逼,是否是。固然,时间效率也快不少,不信你去测试测试。函数
交换两个数相信不少人每天写过,我也相信你每次都会使用一个额外来变量来辅助交换,例如,咱们要交换 x 与 y 值,传统代码以下:工具
int tmp = x;
x = y;
y = tmp;
复制代码
这样写有问题吗?没问题,通俗易懂,万一哪天有人要为难你,**不容许你使用额外的辅助变量来完成交换呢?**你还别说,有人面试确实被问过,这个时候,位运算大法就来了。代码以下:学习
x = x ^ y // (1)
y = x ^ y // (2)
x = x ^ y // (3)
复制代码
我靠,牛逼!三个都是 x ^ y,就莫名交换成功了。在此我解释下吧,咱们知道,两个相同的数异或以后结果会等于 0,即 n ^ n = 0。而且任何数与 0 异或等于它自己,即 n ^ 0 = n。因此,解释以下:开发工具
把(1)中的 x 带入 (2)中的 x,有测试
y = x^y = (x^y)^y = x^(y^y) = x^0 = x。 x 的值成功赋给了 y。优化
对于(3),推导以下:ui
x = x^y = (x^y)^x = (x^x)^y = 0^y = y。
这里解释一下,异或运算支持运算的交换律和结合律哦。
之后你要是别人看不懂你的代码,逼格装高点,就能够在代码里面采用这样的公式来交换两个变量的值了,被打了不要找我。
讲这个呢,是想告诉你位运算的强大,让你之后可以更多着去利用位运算去解决一些问题,一时之间学不会也没事,看多了就学会了,不信?继续往下看,下面的这几道题,也是很是常见的,可能你以前也都作过。
给你一组整型数据,这些数据中,其中有一个数只出现了一次,其余的数都出现了两次,让你来找出一个数 。
这道题可能不少人会用一个哈希表来存储,每次存储的时候,记录 某个数出现的次数,最后再遍历哈希表,看看哪一个数只出现了一次。这种方法的时间复杂度为 O(n),空间复杂度也为 O(n)了。
然而我想告诉你的是,采用位运算来作,绝对高逼格!
咱们刚才说过,两个相同的数异或的结果是 0,一个数和 0 异或的结果是它自己,因此咱们把这一组整型所有异或一下,例如这组数据是:1, 2, 3, 4, 5, 1, 2, 3, 4。其中 5 只出现了一次,其余都出现了两次,把他们所有异或一下,结果以下:
因为异或支持交换律和结合律,因此:
1^2^3^4^5^1^2^3^4 = (1^1)^(2^2)^(3^3)^(4^4)^5= 0^0^0^0^5 = 5。
也就是说,那些出现了两次的数异或以后会变成0,那个出现一次的数,和 0 异或以后就等于它自己。就问这个解法牛不牛逼?因此代码以下
int find(int[] arr){
int tmp = arr[0];
for(int i = 1;i < arr.length; i++){
tmp = tmp ^ arr[i];
}
return tmp;
}
复制代码
时间复杂度为 O(n),空间复杂度为 O(1),并且看起来很牛逼。
若是让你求解 3 的 n 次方,而且不能使用系统自带的 pow 函数,你会怎么作呢?这还不简单,连续让 n 个 3 相乘就好了,代码以下:
int pow(int n){
int tmp = 1;
for(int i = 1; i <= n; i++) {
tmp = tmp * 3;
}
return tmp;
}
复制代码
不过你要是这样作的话,我只能呵呵,时间复杂度为 O(n) 了,怕是小学生都会!若是让你用位运算来作,你会怎么作呢?
我举个例子吧,例如 n = 13,则 n 的二进制表示为 1101, 那么 3 的 13 次方能够拆解为:
3^1101 = 3^0001 * 3^0100 * 3^1000。
咱们能够经过 & 1和 >>1 来逐位读取 1101,为1时将该位表明的乘数累乘到最终结果。直接看代码吧,反而容易理解:
int pow(int n){
int sum = 1;
int tmp = 3;
while(n != 0){
if(n & 1 == 1){
sum *= tmp;
}
tmp *= tmp;
n = n >> 1;
}
return sum;
}
复制代码
时间复杂度近为 O(logn),并且看起来很牛逼。
这里说一下,位运算不少状况下都是很二进制扯上关系的,因此咱们要判断是不是否位运算,不少状况下都会把他们拆分红二进制,而后观察特性,或者就是利用与,或,异或的特性来观察,总之,我以为多看一些例子,加上本身多动手,就比较容易上手了。因此呢,继续往下看,注意,先别看答案,先看看本身会不会作。
传统的作法就是让 1 不断着乘以 2,代码以下:
int findN(int N){
int sum = 1;
while(true){
if(sum * 2 > N){
return sum;
}
sum = sum * 2;
}
}
复制代码
这样作的话,时间复杂度是 O(logn),那若是改为位运算,该怎么作呢?我刚才说了,若是要弄成位运算的方式,不少时候咱们把某个数拆成二进制,而后看看有哪些发现。这里我举个例子吧。
例如 N = 19,那么转换成二进制就是 00010011(这里为了方便,我采用8位的二进制来表示)。那么咱们要找的数就是,把二进制中最左边的 1 保留,后面的 1 所有变为 0。即咱们的目标数是 00010000。那么如何得到这个数呢?相应解法以下:
一、找到最左边的 1,而后把它右边的全部 0 变成 1
二、把获得的数值加 1,能够获得 00100000即 00011111 + 1 = 00100000。
三、把 获得的 00100000 向右移动一位,便可获得 00010000,即 00100000 >> 1 = 00010000。
那么问题来了,第一步中把最左边 1 中后面的 0 转化为 1 该怎么弄呢?我先给出代码再解释吧。下面这段代码就能够把最左边 1 中后面的 0 所有转化为 1,
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
复制代码
就是经过把 n 右移而且作或运算便可获得。我解释下吧,咱们假设最左边的 1 处于二进制位中的第 k 位(从左往右数),那么把 n 右移一位以后,那么获得的结果中第 k+1 位也一定为 1,而后把 n 与右移后的结果作或运算,那么获得的结果中第 k 和 第 k + 1 位一定是 1;一样的道理,再次把 n 右移两位,那么获得的结果中第 k+2和第 k+3 位一定是 1,而后再次作或运算,那么就能获得第 k, k+1, k+2, k+3 都是 1,如此往复下去....
最终的代码以下
int findN(int n){
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8 // 整型通常是 32 位,上面我是假设 8 位。
return (n + 1) >> 1;
}
复制代码
这种作法的时间复杂度近似 O(1),重点是,高逼格。
上面讲了 5 道题,原本想写十道的,发现五道就已经写了很久了,,,,十道的话,怕大家也没耐心写完,并且一道比一道难的那种,,,,。
不过呢,我给出的这些例子中,并非让大家学会了这些题就 Ok,并且让大家有一个意识:不少时候,位运算是个不错的选择,至少时间效率会快不少,并且高逼格,装逼必备。因此呢,之后能够多尝试去使用位运算哦,之后我会再给你们找些题来说讲,遇到高逼格的,感受很不错的,就会拿来供你们学习了。
若是你以为该文章不错,不妨
一、点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
二、关注我,让咱们成为长期关系
三、关注公众号「苦逼的码农」,里面已有100多篇原创文章,我也分享了不少视频、书籍的资源,以及开发工具,欢迎各位的关注,第一时间阅读个人文章。