Skip to content

异或运算的骚操作


异或运算性质

(1)异或运算就是无进位相加

(2)异或运算满足交换律、结合律(用无进位相加理解),也就是同一批数字,不管异或顺序是什么,最终的结果都是一个

(3)0 ^ n = n,n ^ n = 0

(4)整体异或和如果是 x,整体中某个部分的异或和如果是 y,那么剩下部分的异或和是 x ^ y,因为异或运算满足交换律、结合律

Tip

(1)理解无进位相加:异或运算的规则是不同为 1,相同为 0,无进位相加恰好符合这个性质

(2)第四点的简单证明,若 a ^ b = c,则 a = c ^ b,b = c ^ a

已知 a ^ b = c,证明 a = c ^ b,利用性质 0 ^ n = n,n ^ n = 0,两边同时异或 b

则原式变为 a ^ b ^ b = c ^ b,a ^ b ^ b = a ^ 0 = a,即 a = c ^ b

异或场景题

袋子里一共 a 个白球,b 个黑球,每次从袋子里拿 2 个球,每个球每次被拿出机会均等,如果拿出的是 2 个白球或者 2 个黑球,那么就往袋子里重新放入 1 个白球,如果拿出的是 1 个白球和 1 个黑球,那么就往袋子里重新放入 1 个黑球那么最终袋子里一定会只剩 1 个球,请问最终的球是黑的概率是多少?用 a 和 b 来表达这个概率

黑球的数量如果是偶数,最终的球是黑的概率是 0%

黑球的数量如果是奇数,最终的球是黑的概率是 100%

Tip

题目含义:白球,白球 --> 放回白球,黑球,黑球 --> 放回白球,白球,黑球 --> 放回黑球

假设白球是 0,黑球是 1,翻译一下题意就是 0 0 --> 0,1 1 --> 0,0 1 --> 1,这不就是异或运算,而放回的过程就是 a ^ b = c,把 c 这个结果放回袋子中

如果黑球的数量是偶数,最后无进位相加(异或运算)的结果一定是 0,而白球是 0,即不会有黑球剩余,黑球的概率为 0%

如果黑球的数量是奇数,对于无进位相加(异或运算)有两种情况,1 + 1 --> 0,0 + 0 --> 0,之前所有数的异或结果为 0,黑球用 1 表示,那最后一定会剩余一个 1,无进位相加的结果就是 1,即会有 1 个黑球剩余,黑球的概率为 100%

交换两个数(不比较)

思路分析


⚠️ 注意事项

了解异或运算可以实现交换两个变量的值即可,不推荐使用这种做法

数组中交换两个值,则要求索引 i != j,如果 i = j,则异或结果为 0,原来的数被异或结果 0 覆盖,之后两行已经没有意义了

代码实现

java
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];
	}

}

不比较返回两数最大值

题目链接

https://www.nowcoder.com/practice/d2707eaf98124f1e8f1d9c18ad487f76

代码实现

java
public class Code02_GetMaxWithoutJudge {

	// 必须保证 n 一定是 0 或者 1
	// 若 n = 0,返回 1
    // 若 n = 1,返回 0
	public static int flip(int n) {
		return n ^ 1;
	}

	// >>> 是无符号右移,无论正数还是负数,高位都补 0
    // flip 之前:非负数返回 0,负数返回 1
	// flip 之后:非负数返回 1,负数返回 0
    // 该方法的目的是取符号位,即判断某个数 > 0 还是 < 0
	public static int sign(int n) {
        // 最高位占一位,int 一共 32 位,右移 31 位的结果就是取符号位的值
		return flip(n >>> 31);
	}

	// 根据差值来判断大小,差值有溢出风险的实现
	public static int getMax1(int a, int b) {
		int c = a - b;
		// c非负,returnA -> 1
		// c非负,returnB -> 0
		int returnA = sign(c);
        // c负数,returnA -> 0
		// c负数,returnB -> 1
		int returnB = flip(returnA);

        // returnA 和 returnB 是互斥的,返回一个,另一个就会被舍弃
        // 简单理解,该返回谁就 return 谁,由于互斥,另一个就会被舍弃
		return a * returnA + b * returnB;
	}

	// 没有任何问题的实现,根据符号来判断大小,而不是根据差值
	public static int getMax2(int a, int b) {
		// c可能是溢出的
		int c = a - b;

        // 再次强调 sign 方法 --> 非负返回 1 ,负数返回 0

		// 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 = 0,如果一样 sameAB = 1
		int sameAB = flip(diffAB);

        // diffAB 和 sameAB 是互斥的,返回一个,另一个就会被舍弃

        // return A 的两种情况
        // (1)A 和 B 符号不一样,且 a 非负,另一个数的符号是负数,显然 a 更大,returnA
        // (2)A 和 B 符号一样,且 c 非负,a - b = c > 0,显然 a 更大,returnA
        // 为什么可以规避溢出的风险?因为这里增加了对 A,B 符号是否相同的逻辑
        // 根据符号信息进行判断,而不是直接通过 c = a - b 来判断差值的大小,避免差值 c 溢出
		int returnA = diffAB * sa + sameAB * sc;
		int returnB = flip(returnA);

        // c = a - b,a,b 谁大返回谁
		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));
	}

}

找到缺失的数字

题目链接

https://leetcode.cn/problems/missing-number/

题意:给一个长度为 n 的数组,数组中的值不重复,数组中的元素都是 [0,n] 范围的数,找出在 [0,n] 范围内数组中缺失的那个数

理解:因为数组长度为 n,则数组中元素的最大范围是 [0,n - 1],给定的范围是 [0,n],必定缺失一个数

思路分析

根据异或运算的性质 4,整体异或和如果是 x,整体中某个部分的异或和如果是 y,那么剩下部分的异或和是 x ^ y

本题中对于 0 - n 范围的异或和设为 xorAll,对于数组中元素的异或和设为 xorHas,根据性质 4,缺失的那个数就是 xorAll ^ xorHas

时间复杂度:O(n),空间复杂度:O(1)

代码实现

java
class Solution {
    public int missingNumber(int[] nums) {
        // 0 - n 范围内的异或和
        int xorAll = 0;
        // 数组中元素的异或和
        int xorHas = 0;

        for (int i = 0; i < nums.length; i++) {
            xorAll ^= i;
            xorHas ^= nums[i];
        }
        // xorAll 是 0 - n 范围内的异或和
        // 而循环中只能异或到 n - 1,最后还要异或长度
        xorAll ^= nums.length;

        // 根据异或运算的性质可以求得缺失的数
        return xorAll ^ xorHas;
    }
}

找唯一出现奇数次的数

题目链接

https://leetcode.cn/problems/single-number/

应用场景:数组中 只有 1 种数出现了奇数次,其他的数都出现了偶数次,找出出现奇数次的这种数,该题是取了一个特例

思路分析

根据异或运算的性质,0 ^ n = n,n ^ n = 0

只需要将数组中所有数异或一遍,就可以得到出现奇数次的数

数组中所有的数异或,如果一个数出现了 n 次(偶数次),那就会出现 n / 2 组 n ^ n = 0 的结果,而 0 ^ 0 = 0,也就是说 n / 2 个 0 异或,结果为 0,若一个数出现了奇数次,最后异或一定会剩余这个数,即 0 ^ 这个数 = 这个数(0 ^ n = n)

代码实现

java
class Solution {
    public int singleNumber(int[] nums) {
        int xor = 0;
        for (int num : nums) {
            xor ^= num;
        }
        return xor;
    }
}

⭐ Brian Kernighan 算法

应用场景

提取出二进制状态中最右侧的 1 这个状态

执行前:0 1 1 0 1 0 0 0

执行后:0 0 0 0 1 0 0 0

思路分析

对于原状态 n

第一步:对 n 取反得到 ~n,记为 a

第二步:~n + 1 ,记为 b

第三步:n & b 得到结果,即 n & (~n + 1) = n & (-n)

代码实现

java
int rightOne = eor1 & (-eor1);

找唯二出现奇数次的数

题目链接

https://leetcode.cn/problems/single-number-iii/

应用场景:数组中有 2 种数出现了奇数次,其他的数都出现了偶数次,找出出现奇数次的那 2 个数,该题是取了一个特例

思路分析

根据题意,a ≠ b,且 a,b 都出现了奇数次

对于数组中所有的数异或,假设结果为 c,根据异或的性质,最后可以得到 a ^ b = c

因为 a ≠ b,那在二进制中,说明 a,b 的某一位一定不同,也就是说两个数的某一位异或结果是 1,假设第 3 位不同,那整个数组的数就可以分类两类,第三位是 1 的数,第三位不是 1 的数

对于出现偶数次的数,无需关心,因为他们异或的结果为 0,都消掉了,对数组中所有的元素异或,最后只会剩下出现奇数次数的数

那也就是说,a 可能在第三位为 1 的这一组数中,b 可能在第三位不为 1 的这一组数中,要么就反过来

准备一个变量 xor2,只异或第三位上不是 1 的数,出现偶数次的数都会抵消,他们的异或和为 0,0 ^ 出现奇数个数的数 = 出现奇数个数的数,最后剩下出现奇数个数的数,又因为根据前面的思路对数组中的元素分了组,那第三位上不是 1 的这一组数中,有可能是 a 在这一组,也有可能是 b 在这一组,即 xor2 要么得到 a,要么得到 b

xor1 = a ^ b,xor2 会得到 a 或 b,此时利用异或运算的性质 4,就可以得到 a 或 b = xor1 ^ xor2

代码实现

java
class Solution {
    public int[] singleNumber(int[] nums) {
        int xor1 = 0;
        for (int num : nums) {
            xor1 ^= num;
        }
        // xor1 最后得到 xor1 = a ^ b

        // Brian Kernighan 算法,提取二进制里最右侧的 1
        int rightOne = xor1 & (-xor1);

        int xor2 = 0;
        for (int num : nums) {
            // xor2 只异或二进制里最右侧 1 那一位不是 1 的数
            if ((num & rightOne) == 0) {
                xor2 ^= num;
            }
        }
        return new int[] { xor2, xor1 ^ xor2 };
    }
}

找唯一出现次数小于 m 的数

题目链接

https://leetcode.cn/problems/single-number-ii/

应用场景:数组中只有 1 种数出现次数少于 m 次,其他数都出现了 m 次,返回出现次数小于 m 次的那种数,题目只是一个特例

思路分析

对于每一个数,将其看成是二进制位,统计出每一位出现的次数,如果一个数出现了偶数次,那这个数的每一位出现的次数一定为偶数,如果一个数出现了 m 次(假设是偶数),那二进制的某一位 % m = 0,这是毋庸置疑的

如果二进制的某一位 % m = 1,那说明这个 1 肯定是出现了少于 m 次的数(假设是出现了奇数次的数)的某一个二进制位的影响所造成的,那能说明出现了奇数次的这个数的某一位是 1

代码实现

java
class Solution {
    public int singleNumber(int[] nums) {
        return find(nums, 3);
    }

    public int find(int[] arr, int m) {
        // cnts[i]:一个十进制数的二进制数中第 i 位二进制数是 1 的个数
        int[] cnts = new int[32];

        for (int num : arr) {
            for (int i = 0; i < 32; i++) {
                // & 运算:同为 1 为 1,否则为 0
                // 如果第 i 位是 1,就累加 1,否则累加 0
                cnts[i] += (num >> i & 1);
            }
        }

        int ans = 0;
        for (int i = 0; i < 32; i++) {
            // 这里说明出现次数小于 k 次的数在这一位是 1,设置一下
            if (cnts[i] % m != 0) {
                ans |= (1 << i);
            }
        }
        return ans;
    }
}