异或运算性质
1)异或运算就是无进位相加(这样记相对方便,不会忘)
比如3^5,就是0011 ^ 0101 = 0110
2)异或运算满足交换律、结合律,也就是同一批数字,不管异或顺序是什么,最终的结果都是一个
3)0^n=n,n^n=0
4)整体异或和如果是x,整体中某个异或和如果是y,那么剩下部分的异或和是x^y
比如a^b = c,那么b = a^c,a=b^c
这些结论最重要的就是1)结论,所有其他结论都由这个结论推论得到,4)结论用到的最多。
根据异或运算的这些结论,我们来看一个好玩的问题:
袋子里一共a个白球,b个黑球,每次从袋子里拿2个球,每个球每次被拿出机会均等。如果拿出的是2个白球、或者2个黑球,那么就往袋子里重新放入1个白球;如果拿出的是一个白球和一个黑球,那么就往袋子里重新放入一个黑球。那么最终袋子里一定只剩一个球,请问最终的球是黑的概率是多少?用a和b来表达这个概率。
解:我们设黑球是1,白球是0,那么从袋子里拿出一个黑球和一个白球,再放回一个黑球,就相当于1^0 = 1;拿出的是2个白球、或者2个黑球,那么就往袋子里重新放入1个白球,就相当于1^1=0,0^0=0,这样就很显然了。
那么从袋子里一直拿2个球在放回一个球,就可以类似于袋子里的0和1一起做异或运算的过程,因此,当黑球为偶数个数时,所有0和1做异或运算,答案为0,最终袋子里百分百为白球;当黑球为基数个数时,所有0和1做异或运算,答案为1,最终袋子里百分百是黑球。袋子里最终留下什么球,和白色球的个数无关。
异或运算应用1:交换两个数的数值
// 用异或运算交换两数的值
public class Code01_SwapExclusiveOr {
public static void main(String[] args) {
int a = -2323;
int b = 10;
a = a ^ b;
b = a ^ b;
a = a ^ b;
System.out.println(a);
System.out.println(b);
int[] arr = { 3, 5 };
swap(arr, 0, 1);
System.out.println(arr[0]);
System.out.println(arr[1]);
swap(arr, 0, 0);
System.out.println(arr[0]);
}
// 当i!=j,没问题,会完成交换功能
// 当i==j,会出错
// 所以知道这种写法即可,并不推荐
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
}
异或运算应用2:不用任何判断语句和比较操作,返回两个数的最大值
public class Code02_GetMaxWithoutJudge {
// 必须保证n一定是0或者1
// 0变1,1变0
public static int flip(int n) {
return n ^ 1;
}
// 非负数返回1
// 负数返回0
public static int sign(int n) {
return flip(n >>> 31); //无符号右移,直接把符号位的数字移到第一位
}
// 有溢出风险的实现
public static int getMax1(int a, int b) {
int c = a - b;
// c非负,returnA -> 1
// c非负,returnB -> 0
// c负数,returnA -> 0
// c负数,returnB -> 1
int returnA = sign(c);
int returnB = flip(returnA);
return a * returnA + b * returnB;
}
// 没有任何问题的实现
public static int getMax2(int a, int b) {
// c可能是溢出的
int c = a - b;
// a的符号
int sa = sign(a);
// b的符号
int sb = sign(b);
// c的符号
int sc = sign(c);
// 判断A和B,符号是不是不一样,如果不一样diffAB=1,如果一样diffAB=0
int diffAB = sa ^ sb;
// 判断A和B,符号是不是一样,如果一样sameAB=1,如果不一样sameAB=0
int sameAB = flip(diffAB);
int returnA = diffAB * sa + sameAB * sc;
int returnB = flip(returnA);
return a * returnA + b * returnB;
}
public static void main(String[] args) {
int a = Integer.MIN_VALUE;
int b = Integer.MAX_VALUE;
// getMax1方法会错误,因为溢出
System.out.println(getMax1(a, b));
// getMax2方法永远正确
System.out.println(getMax2(a, b));
}
}
异或运算应用3:找到缺失的数字
public class Code03_MissingNumber {
public static int missingNumber(int[] nums) {
int eorAll = 0, eorHas = 0;
for (int i = 0; i < nums.length; i++) {
eorAll ^= i; //异或0-n-1中所有数字
eorHas ^= nums[i]; //异或nums数组中已有数字
}
eorAll ^= nums.length;
return eorAll ^ eorHas; //根据结论4)得到结果
}
}
异或运算应用4:数组中1种数出现了奇数次,其他的数都出现了偶数次,返回出现了奇数次的数
这题运用结论3轻松解决
public class Code04_SingleNumber {
public static int singleNumber(int[] nums) {
int eor = 0;
for (int num : nums) {
eor ^= num;
}
return eor;
}
}
异或运算应用5:数组中有2种数出现了奇数次,其他的数都出现了偶数次,返回出现了奇数次的数
260. 只出现一次的数字 III - 力扣(LeetCode)
在这题中,将设这2种出现奇数次的数分别为a,b,将数组中所有的数做异或运算,得到的答案为a^b,如果分别得到a和b的值,这是个问题
public class Code05_DoubleNumber {
public static int[] singleNumber(int[] nums) {
int eor1 = 0;
for (int num : nums) {
// nums中有2种数a、b出现了奇数次,其他的数都出现了偶数次
eor1 ^= num;
}
// eor1 : a ^ b
//这是 Brian Kernighan算法,可以提取出二进制里最右侧的1
//为什么要提取出a^b最右侧的1,这样是因为,在a^b最右侧1的位置,a和b一定不相同,他们中一定是其中一个是0,另一个是1
int rightOne = eor1 & (-eor1);
int eor2 = 0;
for (int num : nums) {
if ((num & rightOne) == 0) { //说明这个数num在a^b的最右侧为1的那个位置,不是1,而是0
eor2 ^= num; //得到的eor2,在a^b的最右侧为1的那个位置,一定是0,这时eor2一定是a和b中其中一个
}
}
return new int[] { eor2, eor1 ^ eor2 }; //如果eor2是a,那么eor1^eor2一定是b
}
}
异或运算应用6:数组中只有1种数出现次数少于m次,其他数都出现了m次,返回出现次数小于m次的那种数
137. 只出现一次的数字 II - 力扣(LeetCode)
public class Code06_OneKindNumberLessMtimes {
public static int singleNumber(int[] nums) {
return find(nums, 3);
}
// 已知数组中只有1种数出现次数少于m次,其他数都出现了m次
// 返回出现次数小于m次的那种数
public static int find(int[] arr, int m) {
// cnts[0] : 0位上有多少个1
// cnts[i] : i位上有多少个1
// cnts[31] : 31位上有多少个1
int[] cnts = new int[32];
for (int num : arr) {
for (int i = 0; i < 32; i++) {
cnts[i] += (num >> i) & 1;
}
}
int ans = 0;
for (int i = 0; i < 32; i++) {
if (cnts[i] % m != 0) {
ans |= 1 << i;
}
}
return ans;
}
}