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) { |