异或运算:交换变量与经典题目

梳理异或运算的性质,并用它解决交换变量和数组统计等基础算法题。

约 5 分钟1,726 字#算法

1. 异或运算

异或运算的符号是^,运算规则是相同为0,不同为1。也就是说判断二者是否不相同,如果不相同则为1,其他情况都是0

异或运算还被称作无进位相加,也就是说在每一位上相加的结果不进位,这样更好理解,也就是:

​ 0 0 1 1 0 1 1 0

​ 0 1 0 1 1 1 0 1

------------------------------------

​ 0 1 1 0 1 0 1 1


异或运算有一些性质可以运用:

  1. 交换律:a ^ b == b ^ a
  2. 结合律 :(a ^ b) ^ c == a ^ (b ^ c)
  3. 与自身异或得到0:a ^ a == 0
  4. 与0异或得到自身:a ^ 0 == a

牢记这些性质对算法题有很大的帮助

2. 异或运算常见题目

2.1 不申请三方变量交换两个值

描述:给定两个变量a和b,请你在不申请第三个变量的基础上交换二者的数值

思路:我们可以用异或运算的性质来解决这道题

  1. 先让 a = a ^ b
  2. 再让 b = a ^ b,此时b实际上是 (a ^ b) ^ b,根据异或运算的交换律和结合律,b就是a ^ (b ^ b),又知道b ^ b的结果实际上是0,所以此时是a ^ 0,就等于a,所以b此刻就等于a
  3. 最后让 a = a ^ b,此时就是 (a ^ b) ^ a,再根据刚刚的分析就等于b

至此a和b就完成了交换

java
    public static void swap (int[] arr, int a, int b) {
        arr[a]  = arr[a] ^ arr[b];
        arr[b]  = arr[a] ^ arr[b];
        arr[a]  = arr[a] ^ arr[b];
    }

注意!这种交换方式只适用于a和b是两块内存空间的情况,如果a和b指向同一个内存空间,那么异或最开始就是自己和自己异或,会被刷成0

2.2 只有一种数出现奇数次

题目:给定一个数组arr,其中只有一个数num出现了奇数次,其他所有数都出现偶数次,请找出出现这个奇数次的数

思路:这道题同样可以运用异或运算的性质来解答,即用一个变量eor = 0来依次与数组中每一个数进行异或,也就是eor ^ arr[0] ^ .....arr[i]

由交换律和结合律我们知道,eor与整个数组的数依次异或可以变为eor先与所有出现次数为偶数的数异或,再与出现次数为奇数的数异或(先异或偶次,再异或奇次)

由异或的性质我们知道,这些偶数次的数可以先异或,eor与出现偶数次的数异或的结果都为0,最后与奇数次的数异或的结果就是该奇数次的数

例如:eor ^ ( 3 ^ 3 ^ 3 ^ 3 ) ^ ( 6 ^ 6) ^ (4 ^ 4 ^ 4) = eor ^ 0 ^ 0 ^ 4 = 0 ^ 4 = 4

我的代码实现:

java
    public static int findSoleOdd(int[] arr){
        int eor = 0;
        for (int i = 0; i < arr.length; i++) {
            eor ^= arr[i];
        }
        return eor;
    }

2.3 提取一个数最右侧的1

题目:在Java里面,int类型的数是32位二进制存储的,给定一个int类型的数num,找到该数的二进制的最右侧的那个1,例如:...0000101101101100 提取成为 --> 000...000000100

思路:

这道题实际上让这个数与上自己的相反数就可以了

rst = num & (-num)

我们来举个例子:

​ 11010110 取反 --> 00101001 加上1 --> 00101010

接下来将二者做与运算:

​ 11010110 ​ 00101010

可以见到,二者除了最右边的那个1相等之外,其他的都相反,与运算后只能保留最右边的那个1

2.4 有两种数出现奇数次

题目:给定一个数组arr,其中只有两个数a, b出现了奇数次,其他所有数都出现偶数次,请找出这两个出现奇数次的数

思路:

这道题是2.2的拓展,我们同样可以用到异或运算,根据2.2的分析,我们用eor = 0来异或一遍这个数组后,得到的结果应该是 eor = a ^ b

接下来我们将a和b想办法提取出来:

由于a和b一定是两个不同的数,所以a ^ b一定有一位是1,即a和b至少有一位不相同。

我们将最右边位的1提取出来(方法参见2.3),得到rightOne,假设是x位上是1,那么我们可以知道,a和b一定在x位上不同,一个是0一个是1!

依据这个我们可以把整个数组在逻辑上分为两组,x位上是1的、x位上是0的

我们用eor2 = 0对数组进行第二次异或,这次我们只让eor2异或其中x位上是1的那些数

由于数组里面那些偶数次的数对结果根本没有影响,所以eor2的结果一定是a或者b

得到了其中一个,由于eor = a ^ b,因此我们想要得到另一个只需要让eor ^ eor2就可以了

我的代码:

java
    public static int[] findTwoOdd(int[] arr){
        // 第一次异或,得到 eor = a ^ b
        int eor = 0;
        for (int i = 0; i < arr.length; i++) {
            eor ^= arr[i];
        }
        
        // 找到a和b在哪一位上不相同
        int rightOne = eor & (-eor);
        int eor2 = 0;
        for (int i = 0; i < arr.length; i++) {
            if ((arr[i] & rightOne) != 0) {
                eor2 ^= arr[i];
            }
        }
        
        // 结果数组,存储a和b
        int[] rst = {eor2, eor ^ eor2};
        return rst;

    }

其中(arr[i] & rightOne) != 0,是在判断如果该数在x位上为1,则和eor2异或

因为rightOne得到的是在x位上为1,其余位都是0的数,所以和一个数与的结果能代表该数的x位是否为1

2.5 拓展题

题目:数组中所有的数都出现了M次,只有一种数a出现了K次,1 <= K < M,返回这种数

思路分析:

我们创建一个长度为32的数组count,遍历数组,数组中的每个数转化为32进制数后,假设其在x位上为1,我们将count[x] + 1

这样一来这个数组就能记录各个位上出现了多少个1

  • 例如:一个数位9,二进制是0000....01001,可见0位和3位上是1,我们就将count[0]和count[3]都加1

我们依次遍历count数组里面的每一个数,也就是32位数的每一位

  1. 如果x位上的计数是M的倍数,那么a在x位上肯定不为1,因为在x位上为1的其他数都计数了,而其他数都出现了M次,导致x位是M的倍数

  2. 如果x位上的计数不是M的倍数,那么a在x位上一定是1,只有这样才导致x位上增加了额外K次

因此代码如下:

java
    public static int findK(int arr[], int k, int m) {

        int[] count = new int[32];
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < 32; j++) {
                count[j] += (arr[i] >>> j) & 1;
            }
        }

        int rst = 0;
        for (int i = 0; i < count.length; i++) {
            if (count[i] % m != 0) {
                rst |= 1 << i;
            }
        }
        return rst;

    }

解释一下其中的一些点:

  1. count[j] += (arr[i] >>> j) & 1;,外循环遍历数组中的每一个数,拿到arr[i],内循环遍历arr[i]的每一位,看是否为1。判断的方法是将arr[i]右移j位,再与1,这样能判断j位置是否是1,比如: 01010 右移0位后&1的结果是0,右移1位: 00101 &1后得到1,则第1位是1 00001 00001
  2. 这里用>>>无符号右移,使得能判断负数
  3. rst |= 1 << i;rst是0,即32位都是0,count数组里面哪位不是m的倍数,就是出现了k次的数在该位置为1,我们用或运算把ans的该位置变成1