程序中的所有数在计算机内存中都是以二进制的形式存储的。所谓位运算,就是直接对整数在内存中的二进制位进行操作。一般来说,如果能用位运算来解决问题,效率都是比较高的。
01 判断奇偶数
传统解法
判断数字的奇偶性从概念就能下手,一个数字如果能被2整除,就是偶数,如果不能,就是奇数。代码如下:
void odd_even(int n)
{
if (n % 2 == 1)
{
printf("n是奇数!\n");
}
}
高逼格解法
在计算机看来,数字都是以二进制形式存在的,所以如果二进制的最低位是1的话,就是奇数,是0的话,就是偶数。
void odd_even(int n)
{
if(n & 1 == 1)
{
printf("n是奇数!\n");
}
}
02交换两个数字
传统解法
刚学习C语言的时候,老师就教过我们最基本的交换算法,定义一个中间变量,经过三次赋值,就能实现两个数字的交换。代码如下:
int swap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
高逼格解法
int swap(int x, int y)
{
x = x ^ y;
y = x ^ y;
x = x ^ y;
}
这个交换两个变量而无需借助第3个临时变量过程,其实现主要是基于异或运算的如下性质:
1.任意一个变量X与其自身进行异或运算,结果为0,即X^X=0
2.任意一个变量X与0进行异或运算,结果不变,即X^0=X
3.异或运算具有可结合性,即a^ b ^ c =(a ^ b)^ c = a ^(b ^ c)
4.异或运算具有可交换性,即a ^ b = b ^ a
分析:
第一步:
a = a ^ b;
完成后a变量的结果为a ^ b;
第二步:
b = a ^ b;
此时赋值号右边的a保存的是a ^ b的值,那么将赋值号右边的a用a ^ b替换,得到:
(a ^ b) ^ b; //等价于:a ^ (b ^ b); //等价于:a ^ 0;//等价于:a;
即经过第二步运算后b中的值为a,即b = a,将a换到了b里;
第三步:
a = a ^ b;
此时赋值号右边的a保存的仍然是a ^ b的值,不变,而赋值号右边的b已经是a 了,将赋值号右边的a、b分别进行替换,即此时赋值号右边:
a ^ b;
//等价于:
(a ^ b) ^ a;
//等价于:
a ^ b ^ a;
//等价于:
a ^ a ^ b;
//等价于:
0 ^ b;
//等价于:
b;
该值赋值给a,即a = b;
即经过第三步运算后a中的值为b,即a = b,将b换到了a里;
这样经过如上的三步骤,完成了交换两个变量a、b,而无需借助第3个临时变量过程。
03 找出不重复的数字
问题:给你一组整型数据,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你找出出现一次的数 。
这道题可能很多人会用哈希表来存储,每次存储的时候,记录某个数出现的次数,最后再遍历哈希表,看看哪个数只出现了一次。这种方法的时间复杂度为O(n),空间复杂度也为 O(n)了。
这个方法简单易懂,不过确实比较low,太麻烦啦!其实用位运算,可以一步到位,绝对高逼格。
高逼格写法
上面的例子我们讲过了,两个相同的数异或的结果是0,一个数和0异或的结果是它本身,所以我们把这一组整型全部进行异或运算,例如数组如下:
int array[] = {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异或之后就等于它本身。就是下面的关系:
a ^ b ^ b = a;
最终代码如下:
int find(int[] arr, int size)
{
int tmp = arr[0];
for(int i = 1; i < size; i++)
{
tmp = tmp ^ arr[i];
}
return tmp;
}
04 m的n次方
传统解法
如果让你计算m的n次方,不可以使用库函数,最先想到的方法一定是写一个循环,循环n次,不断的乘以m,简单易学。时间复杂度为O(n)了,怕是小学生都会!
int Pow(int n)
{
int tmp = 1;
for (int i = 1; i <= n; i++)
{
tmp = tmp * m;
}
return tmp;
}
高逼格解法
想通过二进制解决问题,还得把数字拆成二进制来观察。比如计算m的15次方:
m^15 = m^8 * m^4 * m^2 * m^1; //^表示幂运算
换成二进制:
m^1111 = m^1000 * m^0100 * m^0010 * m^0001;
我们可以通过"& 1"和">> 1"来逐位读取1111,等于1时将该位代表的乘数累乘到最终结果。代码如下:
int Pow(int m, int n)
{
int res = 1;
int tmp = m;
while(n != 0)
{
if(n & 1 == 1)
{
res *= tmp;
}
tmp *= tmp;
n = n >> 1;
}
return res;
}
05 找出不大于N的最大的2的幂指数
传统解法
传统的解决方案是从1开始,不断的乘以2,只要这个数字不超过N,就一直循环下去,直到超过N退出循环。代码如下:
int findN(int N)
{
int sum = 1;
while(1)
{
if(sum * 2 >= N)
{
return sum;
}
sum = sum * 2;
}
}
高逼格解法
这个方法还是太low,我们还是把它写成二进制来解决吧。例如N等于70。那么转换成二进制就是01000110。我们要找的数就是,把二进制中最左边的1保留,后面的1全部变为0。即我们的目标数是01000000。那么如何获得这个数呢?看下过程:
1、找到最左边的1,然后把它右边的所有0变成 1:
2、把得到的结果加上1,可以得到数字10000000,即01111111+1 = 10000000。
3、把得到的10000000向右移动一位,即可得到01000000,即10000000 >> 1 = 01000000。
下面只要把第一步的问题解决就行了。
我们假设最左边的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;
}
06 检测整数N是否是2的幂次
传统解法
传统解法用N不断的除以2,当除不尽的时候,判断结果是不是1,如果是1,则N是2的次幂。
int ispow(int N)
{
while (N % 2 == 0)
{
N = N / 2;
}
return (N == 1) ? 1 : 0;
}
高逼格解法
先分析一下2的次幂的二进制表示方法:
2 0010
4 0100
8 1000
所有2的次幂都只有1个1,比如8;8-1=7,7的二进制表示为0111,7&8结果为0,所以直接来看下代码:
int ispow(int N)
{
return ((N & (N - 1)) == 0) ? 1 : 0;
}
07 求绝对值
传统解法
这个问题太简单了,如果是正数,就返回本身,如果是负数,就返回乘以负一的结果。
int abs(int n)
{
return (n > 0) ? n : -n;
}
高逼格解法
先取得n的符号:
n >> 31;
若n为正数,结果为0,若n为负数,结果为-1;
若n为正数,则有:
n ^ 0 = n;
数不变。若n为负数则有:
n ^ (-1);
需要计算n和-1的补码,然后进行异或运算,结果n变号并且为n的绝对值减1,再减去-1(加1)就是绝对值。最终代码如下:
int abs(int n)
{
return (n ^ (n >> 31)) - (n >> 31);
}
08 求两个数的最大值
传统解法
用条件运算符就可以解决:
int max(int a, int b)
{
return (a > b) ? a : b;
}
高逼格解法
先记住两个概念,如果x < y,则表达式x < y返回1,否则返回0;与0做与运算结果为0,与-1做与运算结果不变。
int max(int x, int y)
{
return x ^ ((x ^ y) & -(x < y));
}
如果x < y,则上面的语句等价于:
int max(int x, int y)
{
return x ^ ((x ^ y) & (-1));
//等价于 return x ^ (x ^ y);
//等价于 return y;
}
如果x > y,则上面的语句等价于:
int max(int x, int y)
{
return x ^ ((x ^ y) & 0); //等价于 return x;
}
最后再附一些对程序效率上没有实质提高的位运算技巧,就当作是一种装逼写法吧。
计算n+1
-~n;
计算n-1
~-n;
取相反数
~n + 1;
位运算很多情况下都是跟二进制扯上关系的,所以我们要判断是否需要位运算,很多情况下都会把他们表示成二进制,然后观察特性;或者就是利用与、或、异或的特性来观察。总之,得多看一些例子,加上自己多动手,就比较容易上手了。
好了,给大家讲这么多,这些都是笔试的重点内容,不要忘记转发给你身边需要的小伙伴哦!