与运算(&)、或运算(|)、异或运算(^)的本质 及 用途,文末附加 位运算面试题

目录

一:与运算符(&)and

1、运算规则:

2、例如:3&5

3、用途:

1)判断 奇偶性

2)清零。

3)取一个数中指定位

二:或运算(|)or

1、运算规则:

2、例如:3|5 

3、用途:

1)对一个数据的某些位 - 置 1

2)强行变成最接近的偶数

三:异或 运算符(^)xor

1、运算规则:

2、例如:3^5

3、用途:

1)使特定位翻转找一个数

2)与 0 相异或,保留原值

异或的几条性质:

3)多项式除法

4)简单的加密

5)要交换两个变量的值 - 不用引入中间变量

四:左移运算符(<<)

五:右移运算符(>>)

六:面试题实战

1、小算法:

1)乘 2 的非负整数次幂

2)除以 2 的非负整数次幂

3)判断一个数是不是 2 的正整数次幂

4)对 2 的非负整数次幂取模

5)取绝对值

6)取两个数的最大/最小值

7)判断符号是否相同

8)获取一个数二进制的某一位

2、1-1000 放在含有 1001 个元素的数组中,只有唯一的一个元素值重复,其它均只出现一次。

2'、变种:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。

3、一系列数中,除两个数外其他数字都出现过两次,求这两个数字 (综合&和^):

4、计算数值

5、二进制枚举

6、不用加减乘除做加法

7、二进制中1的个数

法一:与(&)运算 与 左移(<<)运算相结合

法二:是运用 n&(n-1)。


一:与运算符(&)and

1、运算规则:

0&0=0;0&1=0;1&0=0;1&1=1

即:两个同时为1,结果为1,否则为0

2、例如:3&5

十进制3转为二进制的3:0000 0011

十进制5转为二进制的5:0000 0101

------------------------结果:0000 0001 ->转为十进制:1

即:3&5 = 1

3、用途:

1)判断 奇偶性

一个数 and 1 的结果就是取二进制的最末位。

这可以用来判断一个整数的奇偶

二进制的 最末位为 0 表示该数为偶数,最末位为 1 表示该数为奇数

2)清零。

如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。

3)取一个数中指定位

方法:找一个数,对应X要取的位,该数的对应位为1,其余位为零,此数与X进行“与运算”可以得到X中的指定位。

例:设X=10101110,

   取X的低4位,用 X & 0000 1111 = 00001110 即可得到;

   还可用来取X的2、4、6位。

二:或运算(|)or

1、运算规则:

0|0=0;  0|1=1;  1|0=1;   1|1=1;

即 :参加运算的两个对象,一个为1,其值为1。

2、例如:3|5 

即 00000011 | 0000 0101 = 00000111,因此,3|5=7。 

3、用途:

1)对一个数据的某些位 - 置 1

方法:找到一个数,对应X要 置 1 的位,该数的对应位为 1,其余位为零。此数与 X 相或 可使 X 中的某些位 - 置1

例:将 X=10100000 的低 4 位 置 1 ,用 X | 0000 1111 = 1010 1111 即可得到

2)强行变成最接近的偶数

变为偶数,即:把二进制最末位变成 0

对这个数 or 1 之后再减一就可以了,就能把这个数强行变成最接近的偶数

使 a 的最低位为0,可以表示为:a & ~1。

~1的值为 1111 1111 1111 1110,再按"与"运算,最低位一定为0

三:异或 运算符(^)xor

1、运算规则:

0^0=0;  0^1=1;  1^0=1;   1^1=0;

即:参加运算的两个对象,如果两个位为“异”(值不同),则该位结果为1,否则 值相同,则为 0

两个位 值相同得 0,不同得 1

2、例如:3^5

=  0000 0011 | 0000 0101 =0000 0110,因此,3^5 = 6

3、用途:

1)使特定位翻转找一个数

对应X要翻转的个位,该数的对应位为1,其余位为零,此数与X对应位异或即可。

例:X=10101110,使X低4位翻转,用X ^0000 1111 = 1010 0001即可得到。

2)与 0 相异或,保留原值

X ^ 00000000 = 1010 1110。

异或其实就是不进位加法,如1+1=0,,0+0=0,1+0=1。

异或的几条性质:

1、交换律

2、结合律(即 (a^b)^c == a^(b^c) )

3、对于任何数 x,都有 x^x=0,x^0=x

4、自反性:  a^b^b = a^0 = a;

3)多项式除法

不过它最重要的性质还是自反性:A XOR B XOR B = A

即:对给定的数 A,用同样的运算因子(B)作两次 异或 运算后仍得到 A 本身。

0 和任意数字进行 异或 操作结果为 数字本身

两个相同的数字进行 异或 的结果为 0

两次 异或 同一个数 最后结果不变

这是一个神奇的性质,利用这个性质,可以获得许多有趣的应用。

4)简单的加密

比如我想对我 MM 说1314520,但怕别人知道,于是双方约定拿我的生日19880516作为密钥。

1314520 xor 19880516 = 20665500,我就把 20665500 告诉MM。

MM 再次计算 20665500 xor 19880516 的值,得到 1314520,于是她就明白了我的企图

5)要交换两个变量的值 - 不用引入中间变量

所有的程序教科书都会向初学者指出,要交换两个变量的值,必须要引入一个中间变量。

但如果使用异或,就可以节约一个变量的存储空间:

设有A,B两个变量,存储的值分别为a,b,则以下三行表达式将互换他们的值 表达式 (值) :

a=a^b;

b=b^a;

a=a^b;

四:左移运算符(<<)

将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。

例:a = a<< 2将a的二进制位左移2位,右补0,

左移1位后a = a *2; 

若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2

五:右移运算符(>>)

将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。

操作数每右移一位,相当于该数除以2

例如:a = a>> 2 将a的二进制位右移2位,

左补0 or 补1得看被移数是正还是负。

六:面试题实战

1、小算法:

1)乘 2 的非负整数次幂

int mulPowerOfTwo(int n, int m) {  // 计算 n*(2^m)
  return n << m;
}

2)除以 2 的非负整数次幂

int divPowerOfTwo(int n, int m) {  // 计算 n/(2^m)
  return n >> m;
}

注意:

我们平常写的除法是向 0 取整,而这里的右移是向下取整(注意这里的区别),

即当数大于等于 0 时两种方法等价,当数小于 0 时会有区别,如: -1 / 2 的值为 0 ,而 -1 >> 1 的值为 −1 。

3)判断一个数是不是 2 的正整数次幂

bool isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; }

4)对 2 的非负整数次幂取模

int modPowerOfTwo(int x, int mod) { return x & (mod - 1); }

5)取绝对值

int Abs(int n) {
  return (n ^ (n >> 31)) - (n >> 31);
  /* n>>31 取得 n 的符号,若 n 为正数,n>>31 等于 0,若 n 为负数,n>>31 等于 -1
     若 n 为正数 n^0=n, 数不变,若 n 为负数有 n^(-1)
     需要计算 n 和 -1 的补码,然后进行异或运算,
     结果 n 变号并且为 n 的绝对值减 1,再减去 -1 就是绝对值 */
}

6)取两个数的最大/最小值

// 如果 a>=b,(a-b)>>31 为 0,否则为 -1
int max(int a, int b) { return b & ((a - b) >> 31) | a & (~(a - b) >> 31); }
int min(int a, int b) { return a & ((a - b) >> 31) | b & (~(a - b) >> 31); }

7)判断符号是否相同

bool isSameSign(int x, int y) {  // 有 0 的情况例外
  return (x ^ y) >= 0;
}

8)获取一个数二进制的某一位

// 获取 a 的第 b 位,最低位编号为 0
int getBit(int a, int b) { return (a >> b) & 1; }

2、1-1000 放在含有 1001 个元素的数组中,只有唯一的一个元素值重复,其它均只出现一次。

每个数组元素只能访问一次,设计一个算法,将它找出来;不用辅助存储空间,能否设计一个算法实现?

解法一:

显然已经有人提出了一个比较精彩的解法,将所有数加起来,减去1+2+...+1000的和。
这个算法已经足够完美了,相信出题者的标准答案也就是这个算法,唯一的问题是,如果数列过大,则可能会导致溢出。

解法二:

异或就没有这个问题,并且性能更好。
将所有的数全部异或,得到的结果 再与 1^2^3^...^1000 的结果进行 再异或,得到的结果就是重复数。

2'、变种:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。

分析:

这题和上一题的思路略有不同,这题其他数字出现了3次,那么我们如果直接使用位运算异或操作的话无法直接找到结果

需要巧妙的运用二进制的其他特性:判断整除求余操作。

即判断所有数字二进制 1 的总个数和 0 的总个数一定有一个不是三的整数倍,

如果 0 不是三的整数倍那么就说明结果的该位二进制数字为0,同理否则为1.

在具体的操作实现上,问题中给出数组中的数据在int范围之内,那么我们就可以在实现上可以对int的32个位每个位进行依次判断该位1的个数求余3后是否为1,如果为1说明结果该位二进制为1可以将结果加上去。最终得到的值即为答案。

具体代码为:

class Solution {
    public int singleNumber(int[] nums) {
        int value=0;
        for(int i=0;i<32;i++)
        {
            int sum=0;
            for(int num:nums)
            {
                if(((num>>i)&1)==1)
                {
                    sum++;
                }
            }
            if(sum%3==1)
                value+=(1<<i);
        }
        return value;
    }
}

3、一系列数中,除两个数外其他数字都出现过两次,求这两个数字 (综合&和^):

( 题目链接:http://gdutcode.sinaapp.com/problem.php?cid=1051&pid=7 )

限制条件:按照从小到大的顺序输出.例如 2 2 1 1 3 4.最后输出的就是 3 和 4

这个题就是首先在输入的时候一直异或,就可以把这两个数异或的乘积找出来,就比如上例中 x=3^4;

然后找一个变量 tmp 来分开这两个数。

按位 与 的话可以发现会分开这两个数,分别存在 num1 和 num2 中

 思路:

具体思路就是想办法 将数组逻辑上一分为二

先异或一遍到最后得到一个数,得到的肯定是 a^b (假设两个数值分别为a和b)的值。

在看异或^的属性:不同为1,相同为0.

也就是说最终这个结果的二进制为 1 的位置上 a 和 b 是不相同的。

而我们可以找到这个第一个不同的位,然后将数组中的数分成两份,

该位为 0 的进行异或求解得到其中一个结果 a,该位为 1 的进行异或求解得到另一个结果 b。

图示:

代码示例:

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
using namespace std;
#define N 1000010
int a[N];
 
int main()
{
    //freopen("why.in", "r", stdin);
    //freopen("why.out", "w", stdout);
    int t;
    scanf("%d", &t);
    while(t--) {
        int n;
        scanf("%d", &n);
        int x = 0;
        for(int i = 1; i <= n; i++) {
            scanf("%d", &a[i]); x ^= a[i];
        }
        int num1 = 0, num2 = 0;
        int tmp = 1;
        while(!(tmp & x)) tmp <<= 1;
		cout<<tmp<<endl;
        for(int i = 1; i <= n; i++) {
            if(tmp & a[i]) num1 ^= a[i];
            else num2 ^= a[i];
        }
        printf("%d %d\n", min(num1, num2), max(num1, num2));
    }
    return 0;
}

4、计算数值

int a=729;
int b=271;
printf("%d \n",(a & b) +(a ^ b)>>1);
printf("%d  \n",(a & b) +(a | b));

输出结果:500;1000

(1)这道题咋看上去是位运算,一步一步进行位运算,不会吧,很 low 的。那么有什么捷径呢?

(2)好的,我们来读懂这道题的含义。先来说说各种位运算的本质吧。

&运算:

相当于十进制 相同位做加法的1/2

0101 & 0011   结果:二进制0001             十进制 (2^0 +2^0)/2          这里的"^"代表次幂

|运算:

相当于十进制 相同位做加法的1/2与不同位做加法求和

0101 | 0011   结果:二进制0111             十进制 (2^0 +2^0)/2 +(2^2 +2^1)

^运算:

相当于十进制不同位做加法

0101 ^ 0011  结果:二进制0110           十进制(2^2 + 2^1 )

(3)好了,我们用(2)所示的方法再来求解这道题试试

第一个输出结果的含义:729、271相同位做加法的1/2 与 729、271不同位做加法的1/2(右移1位相当于除2)求和

哎呀,这个含义不就是729与271求平均数吗,OK,结果就是500。

第二个输出结果的含义:729、271相同位做加法的1/2,729、271相同位做加法的1/2,729、271不同位做加法求和,三个结果的和

哎呀呀,这个含义不就是729与271求和吗,OK结果就是1000啦。

(4)好啦,掌握了&(与运算)、|(或运算)、^(异或运算)的本质,以后涉及这类题便可瞬间秒杀

5、二进制枚举

在遇到子集问题的处理时候,我们有时候会借助二进制枚举来遍历各种状态(效率大于dfs回溯)。

这种就属于排列组合的问题了,对于每个物品(位置)来说,就是使用和不使用的两个状态,而在二进制中刚好可以用1和0来表示。

而在实现上,通过枚举数字范围分析每个二进制数字各符号位上的特征进行计算求解操作即可。

二进制枚举的代码实现为:

for(int i = 0; i < (1<<n); i++) //从0~2^n-1个状态
{
  for(int j = 0; j < n; j++) //遍历二进制的每一位 共n位
  {
    if(i & (1 << j))//判断二进制数字i的第j位是否存在
    {
      //操作或者输出
    }
  }
}

6、不用加减乘除做加法

题目:

写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

分析

这道题咋一听可能没啥思路,简单研究一下位运算还是能独立推出来和理解的。

当然,解决这题前,需要了解上面的四种位运算。还要知道二进制的运算:0+0=0,0+1=1,1+1=0(进位)

对于加法的一个二进制运算。如果不进位那么就是非常容易的。这时候相同位都为0则为0,0和1则为1.满足这种运算的异或(不相同取1,相同取0)和或(有一个1则为1)都能满足.

看到上面操作的不足之后,我们肯定还需要解决进位的问题对于进位的两数相加,这种核心思想为

1、用两个数,一个正常m相加(不考虑进位的)。用异或a^b就是满足这种要求,先不考虑进位(如果没进位那么就是最终结果)。另一个专门考虑进位的n。两个1需要进位。所以我们用a&b与记录需要进位的。但是还有个问题,进位的要往上面进位,所以就变成这个需要进位的数左移一位。

2、然后就变成m+n重新迭代开始上面直到不需要进位的(即n=0时候)。

实现代码为:

public class Solution {
    	public int Add(int num1,int num2) {
		/*
		 *  5+3   5^3(0110)   5&3(0001) 
		 *  0101    
		 *  0011 
		 */
		int a=num1^num2;
		int b=num1&num2;
		b=b<<1;
		if(b==0)return a;
		else {
			return Add(a, b);
		}        
	 }
}

当然,这里也可以科普一下二进制求加法:average = (a&b) + ((a^b)>>1) ;

7、二进制中1的个数

这是一道经典题,在剑指offer上也有对应题目,其具体题目要求 输入一个整数,输出该数二进制表示中1的个数(其中负数用补码表示)

对于这个问题,不用位运算将它转成二进制字符串直接枚举字符 '1' 的个数也可以直接求出来,但是这样做是没有灵魂的并且效率比较差。

这里提供两种解决思路:

法一:与(&)运算 与 左移(<<)运算相结合

大家知道每个类型的数据它的背后实际都是二进制操作。大家知道 int 的数据类型的范围是 (-231,231 -1)。

并且 int 有 32 位。但是并非 32 位全部用来表示数据。真正用来表示数据大小的也是 31 位。最高位用来表示正负

首先要知道:

1<<0=1 =00000000000000000000000000000001
1<<1=2 =00000000000000000000000000000010
1<<2=4 =00000000000000000000000000000100
1<<3=8 =00000000000000000000000000001000
. . . . . .
1<<30=2^30 =01000000000000000000000000000000
1<<31=-2^31 =10000000000000000000000000000000

其次还要知道

位运算 & 与。两个十进制与运算.每一位同1为1

所以我们用 2 的正数次幂与知道的数分别进行与运算操作。

如果结果不为 0,那么就说明这位为 1.

(前面 31 个都是大于 0 的最后一个与结果是负数但是如果该位为 1 那么结果肯定不为 0)

具体代码实现为:

public int NumberOf1(int n) {
  int va=0;
  for(int i=0;i<32;i++)
  {
    if((n&(1<<i))!=0)
    {	        		
      va++;
    }
  }
  return va;	      
}

法二:是运用 n&(n-1)

n 如果不为 0,那么 n-1 就是二进制第一个为 1 的变为 0,后面全为 1.

这样的 n&(n-1) 一次运算就相当于把最后一个 1 变成 0.

这样一直到运算的数为 0 停止计算次数就好了,如下图共进行三次运算那么 n 的二进制中就有三个 1。

 

实现代码为:

public class Solution {
    public int NumberOf1(int n) {
	   int count=0;
	   while (n!=0) {
		   n=n&(n-1);
		   count++;
	   }
	   return count;
	}
}

 

 

  • 19
    点赞
  • 141
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值