学益得线上课堂之位运算技巧总结

程序中的所有数在计算机内存中都是以二进制的形式存储的。所谓位运算,就是直接对整数在内存中的二进制位进行操作。一般来说,如果能用位运算来解决问题,效率都是比较高的。

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;

位运算很多情况下都是跟二进制扯上关系的,所以我们要判断是否需要位运算,很多情况下都会把他们表示成二进制,然后观察特性;或者就是利用与、或、异或的特性来观察。总之,得多看一些例子,加上自己多动手,就比较容易上手了。

好了,给大家讲这么多,这些都是笔试的重点内容,不要忘记转发给你身边需要的小伙伴哦!

更多内容,关注学益得智能硬件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值