算法 64式 20、位运算与异或算法整理

本文详细介绍了位运算和异或的概念、特点及其在算法中的应用。位运算包括对整数二进制位的操作,而异或在特定情况下可用于找出数组中唯一出现一次的数字。文章还提供了LeetCode上的Single Number问题作为经典例题,探讨如何利用位运算和异或在线性时间复杂度内解决问题,而不依赖额外内存。
摘要由CSDN通过智能技术生成

1算法思想

位运算与异或

1.1含义

位运算:

含义:对整数在内存中的二进制位进行操作。

 

异或:

含义:a与b两个值不同,异或结果位1;如果a与b相同,异或结果位0

运算法则: a与b异或结果=(非a&b)V(a&非b)

样例: a^a=0, 0^1=1,0^0=0,1^1=0

1.2特点

位运算:

数字的二进制表示中1的个数: 每次用n与n-1进行与运算,令n=n-1,操作次数即为所求。

异或:

数组中唯一出现1次的数字只有1个时,其余出现偶数次,则数组所有元素异或的结果即为出现1次的那个数字

数组中唯一出现1次的数字有两个时,数组所有元素异或结果从最低位到最高位的值如果为1,则根据该位上值是否为1

划分整个数组,对划分后的数组分别异或,则可以得到出现1次的数字。

还有其他特点,不一一列举。

1.3适用

1.4通用解法

位运算与异或算法:

1 位运算记住位的特点和如何操作位。

2 异或算法牢记两个数不同则异或结果为1,两个数相同,则异或结果为0.

 

1.5经典例题讲解

Single Number

Given an array of integers, every element appears three times except for one, which appears exactly once. Find that single one.

Note:

Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

 

分析:数组中每个元素出现了3次,只有一个元素出现了1次,找到这个元素。

必须在线性时间找到。尽量不要使用额外的空间。

leecode的解法:https://leetcode.com/problems/single-number-ii/?tab=Solutions

想不到。对0~31位上,统计每个数在每一位上出现次数,如果出现的总次数sum % 3 != 0

说明这一位肯定是只出现了一次的那一位,把所有位异或连接,即可。

获取每个数第i位用: 掩码mask = 1 << 1,然后 mask & nums.at(i) 即得到

代码如下:

 

class Solution {

public:

    int singleNumber(vector<int>& nums) {

              if(nums.empty())

              {

                     return 0;

              }

        unordered_map<int , int> numToTimes;

              int size = nums.size();

              int mask;

              int sum;

              int result = 0;

              for(int i = 0 ; i < 32 ; i++)

              {

                     mask = 1 << i;

                     sum = 0;

                     for(int j = 0 ; j < size ; j++)

                     {

                            //这里相与结果是第i位为1或者全0

                            if((nums.at(j) & mask) != 0)

                            {

                                   sum++;

                            }

                     }

                     if(sum % 3 != 0)

                     {

                            result |= mask;

                     }

              }

              return result;

    }

};

 

 

2 位运算与异或系列

类别-编号

题目

遁去的一

1

中国象棋将帅问题:

自古将帅不能照面

                __ __ __

10              将

9

8

7

6

5

4

3

2

1                       帅

 a b c d e f g h i

A表示将,B表示帅,A被限制在{d10,f10,d8,f8}中,B被限制在{d3,f3,d1,f1}中。

每一步A,B可以横向或纵向移动一个。A与B不能在同一条纵向直线上,比如A在d10位置,B就不能在d1,d2,d3位置

 

请写出一个程序,输出A、B所有合法位置。要求在代码中只能使用一个字节存储变量。

编程之美

https://blog.csdn.net/qingyuanluofeng/article/details/39272911

关键:

1 因此将该字节一分为二,前4位表示A,后4位表示B,这样每个棋子都有16种位置表示方法,已经足够(牛逼啊)

2 算法:

遍历A的位置

 遍历B的位置

  判断A、B的位置组合是否满足要求

  如果满足则输出

3 #define LEFT_MASK (FULL_MASK << 4) //注意,用define定义东西的时候,若有两个变量,需要加括号

4 #define RIGHT_MASK (FULL_MASK >> 4) //右掩码就是右边4位全是1

5 #define LSET(b,n) (b = ((b & RIGHT_MASK) | ((n) << 4) ) )//将比特b的左边设定为n,主要为循环遍历使用。采用的方法是,

//左边清零(通过与右掩码(00001111)相与即可),然后将数n向左移4位,然后将上述两个数用或即可,注意别漏了赋值操作

//注意n要用括号括起来,他表示一个数,不是一个字符

6 #define LGET(b) ( (b & LEFT_MASK) >> 4)//获取比特b的左边4位的方法是,先将右边4位清零,

//再右移4位即可

7    if(LGET(b) % STEP != RGET(b) % STEP)//牛逼,用九宫格模拟两者取模之后的余数不同,就表示

    //不在一条竖线上就可以

 

#define FULL_MASK 255

//#define HALF_MOVE 4

#define LEFT_MASK (FULL_MASK << 4) //注意,用define定义东西的时候,若有两个变量,需要加括号

#define RIGHT_MASK (FULL_MASK >> 4) //右掩码就是右边4位全是1

#define LSET(b,n) (b = ((b & RIGHT_MASK) | ((n) << 4) ) )//将比特b的左边设定为n,主要为循环遍历使用。采用的方法是,

//左边清零(通过与右掩码(00001111)相与即可),然后将数n向左移4位,然后将上述两个数用或即可,注意别漏了赋值操作

//注意n要用括号括起来,他表示一个数,不是一个字符

#define RSET(b,n) (b = ((b & LEFT_MASK) | (n) ) )//将比特b的右边设定为n,采用的方法是:

//右边清零(将该数与左掩码(11110000)相与),然后与该数n相或

#define LGET(b) ( (b & LEFT_MASK) >> 4)//获取比特b的左边4位的方法是,先将右边4位清零,

//再右移4位即可

#define RGET(b) (b & RIGHT_MASK)//获取比特b的右边4位的方法是,将该数右边4位与右掩码相与即可

#define STEP 3

 

void ChineseChess_define()

{

 unsigned char b;

 int iCnt = 0;

 for(LSET(b,1) ; LGET(b) <= STEP * STEP ; LSET(b,(LGET(b) + 1) ) )

 {

  for(RSET(b,1) ; RGET(b) <= STEP * STEP ; RSET(b,(RGET(b) + 1) ) )

  {

   if(LGET(b) % STEP != RGET(b) % STEP)//牛逼,用九宫格模拟两者取模之后的余数不同,就表示

    //不在一条竖线上就可以

   {

    printf("A=%d,B=%d\n",LGET(b),RGET(b));

    iCnt++;

   }

  }

 }

 printf("%d\n",iCnt);

}

2

快速找出机器故障

为了保证搜索引擎的服务质量,我们需要保证每份数据都有多个备份

假设一个机器仅存储了一个标号为ID的记录(假设ID是小于10亿的整数),假设每份数据保存了两个备份,这样

就有两个机器储存了同样的数据。

1在某个时间,如果得到一个数据文件ID的列表,是否能够快速地找出这个表中仅出现一次的ID?

2如果已经知道只有一台机器死机(也就是说只有一个备份都是)呢?如果有两台机器死机呢(假设同一个数据

的两个备份不会同时丢失)?

3如果丢失的两台机器ID相同呢?

 

样例输入:

8

2 4 3 6 3 2 5 5

2 4 3 6 3 2 5 5 4 6

样例输出:

4 6

 

编程之美

https://blog.csdn.net/qingyuanluofeng/article/details/39273013

分析:

这个问题可以转化成:有很多的ID,其中只有一个ID出现的次数小于2,其他正常ID出现的次数都等于2,如何找到

这个次数为1的ID。

这样就转化成剑指上面的:所有元素全部异或,最终剩下的数就是那个数

 

第二问转化为这群数中有两个数各出现一次,其余出现0次,所以也是剑指上的题目,需要全部异或得到数字

x,然后获取x的比特表示中最右边的1,记为该位为第n位,根据比特表示中的第n位是否为1,将数组划分

成两部分,每一部分中在分别全部异或一遍,即可。

局限:只能解决两台故障机器ID不同的情况。如果ID相同,则无法解决

 

第三问:

预先计算并保存好所有ID的求和(不变量),顺序列举当前所有剩下的ID,对它们求和,然后用总值 - 剩余值 = 死机

的机器ID值。由于总和可以先计算好,算法的时间复杂度为O(N),空间复杂度为O(1)

 

当两个ID不同时:总和 - 剩余和 = x + y

当两个ID相同时:这个时候,x = (总和-剩余和)/2

 

这个时候可以构造二元一次方程组来做,比如:总乘积/剩余乘积 = x*y(或者求出x*x + y*y = b)

联立解方程

{x + y = a

{x * y = b

此方法的缺陷是:需要事先知道原来n个数,如果题目只给你丢失的数,那就坑爹了

 

void process()

{

 int n;

 while(EOF != scanf("%d",&n))

 {

  if(n < 0)

  {

   break;

  }

  int iRemainArr[MAXSIZE];

  long long lRemainSum = 0,lRemainMul = 1,lIntactSum = 0,lIntactMul = 1;

  for(int i = 0 ; i < n ; i++)

  {

   scanf("%d",&iRemainArr[i]);

   lRemainSum += iRemainArr[i];

   lRemainMul *= iRemainArr[i];

  }

  int iIntactArr[MAXSIZE];

  for(int j = 0 ; j < n + 2 ; j++)

  {

   scanf("%d",&iIntactArr[j]);

   lIntactSum += iIntactArr[j];

   lIntactMul *= iIntactArr[j];

  }

  long long lSum = lIntactSum - lRemainSum;

  long long lMul = lIntactMul/lRemainMul;

  long long lSqrt = (long long)sqrt(double(lSum*lSum - 4*lMul) + 0.5);

  long long lX = (long long)((lSum - lSqrt)/2);

  long long lY = (long long)((lSum + lSqrt)/2);

  printf("%lld %lld\n",lX,lY);

 }

}

3

桶中取黑白球

有一个桶,里面有白球,黑球各有100个,人们按照以下规则把球取出来:

1每次从桶里面拿出两个球2如果是两个同色的球,那么就放入一个黑球

3如果是两个异色的球,就再放入一个白球

问:最后桶里面只剩下一个黑球的概率是多少?

 

int XOR(int iBlackNum,int iWhiteNum)

{

       int iRet = 1;//因为我们把黑球当成0,白球当成1,因此黑球不需要参加异或运算,最后的结果只与白球个数有关

       for(int i = 2 ; i <= iWhiteNum ; i++)

       {

              iRet = !iRet;

       }

       return iRet;

}

编程之美

https://blog.csdn.net/qingyuanluofeng/article/details/47187921

解法3:

用离散横纵的异或(XOR)

两个相同的数,异或为0

    不              1

由于当球不用时,就可以放入一个黑球,那我们只能把黑球赋值1,白球赋值0

闹中想起里面装了100个1和100个0

对每次捞出的两个数字做一次异或,并将所得结果1或0丢回桶中,这样每次操作不会改变球权值的亦或值

假设黑白球各有两个,

1取出两个黑球,放回一个黑球, 0^0 = 0,剩下的结果为(1,2)

2取出一黑一白,放回一个白球, 0^1 = 1,剩下的结果为(0,2)

3最后只能取出两个白球,放回黑球, 1^1 =0,剩下的结果为(1,0)

因为异或满足结合律,即 (a ^ b) ^ c = a ^(b ^ c),操作顺序不会影响后面结果

取球的过程相当于把里面所有的求进行异或操作,也就是1 ^ 1 ^ 0 ^ 0 = 0,因此剩下一个球的时候,桶中权值等于初始时刻所有权值的亦或值,也就是0,所以剩下

的求一定是黑球

扩展:

1如果各有99个黑球和白球,那么异或值为1,必然剩下白球。每种球个数为偶数,剩下的为黑球,个数为奇数,剩下的是白球。

2如果黑白球数量不定,我们不在乎球的数量,只需要看最后异或值

 

int XOR(int iBlackNum,int iWhiteNum)

{

       int iRet = 1;//因为我们把黑球当成0,白球当成1,因此黑球不需要参加异或运算,最后的结果只与白球个数有关

       for(int i = 2 ; i <= iWhiteNum ; i++)

       {

              iRet = !iRet;

       }

       return iRet;

}

4

给定两个32位的整数N与M,以及表示比特位置的i与j。编写一个方法,将M插入N,使得M从N的第j位开始,到第i位结束(j>i)。

假定从j位得到i位足以容纳M,也即若M=10011,那么j和i之间至少可以容纳5个位。例如,不可能出现j=3和i=2的情况,因为第3位和第2位之间放不下M。

示例输入:

N= 100 0000 0000

M=      100 11

i= 2,j =6

输出:N= 100 0100 1100

程序员面试金典

https://blog.csdn.net/qingyuanluofeng/article/details/53993092

分析:这个问题实际上就是寻找这样一个整数x,x与y进行位操作后,以y全部出现。

      等同于以下步骤:

         1:先将N从第j位到第i位之间变成0,设置数M,问题转化为如何使得某一位变成0,从而将从第j位到第i位依次都变成0

         2:将M整体向左移动i位得到新的M

         3: 将N和新的M进行或运算

 

关键:

1

书上解法:

对一个数从第j位到第i位进行清零,涉及到两次掩码运算。

首先:将0取反,得到全1的数,将该全1的数向左移动j+`位,因为这里最低位是从0开始计算的,0到第j位全为0,实际上是j+1个位都为0,因此

      向左移动j+1位,记得到P

然后:需要将第i位以下的位全部变成1,可以用1向左移动i位,然后减去1,记得到Q

最后: P|Q 得到最终掩码R,将R和N进行与运算

想不到:将0取反右移j+1位即可使得第j位前面均为1

2 注意输入的N和M都是字符串,需要转化为二进制

 

 

 

/*

对一个数从第j位到第i位进行清零,涉及到两次掩码运算。

首先:将0取反,得到全1的数,将该全1的数向左移动j+`位,

因为这里最低位是从0开始计算的,0到第j位全为0,实际上是j+1个位都为0,因此

      向左移动j+1位,记得到P

然后:需要将第i位以下的位全部变成1,可以用1向左移动i位,然后减去1,记得到Q

最后: P|Q 得到最终掩码R,将R和N进行与运算

*/

int clearBit(int N , int i , int j)

{

       if(i > j)

       {

              return -1;

       }

       int allOnes = ~0;

       int P = allOnes << (j+1);

       int Q = (1 << i) - 1;

       int R = P | Q;

       int result = R & N;

       return result;

}

 

/*

         1:先将N从第j位到第i位之间变成0

         2:将M整体向左移动i位得到新的M

         3: 将N和新的M进行或运算

*/

int replaceBit(int N , int M , int i , int j)

{

       if(i < 0 || j < 0 || i > j)

       {

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值