异或运算的骚操作

异或运算

两个二进制数做异或运算,“不同为1,相同为0”,就是异或运算的结果。还可以从其他方面来理解异或运算
异或运输的性质

从性质1就可推出下面三条性质
性质2
由于是无进位相加,因此每一位,若有偶数个1,最后结果一定是0;若有奇数个1,最后结果一定是1,和异或运算的先后顺序无关。
而0 ^ n,也即n+0,最后结果为0;而n ^ n,每一位必是偶数个1,因此最后结果为0。
性质4

题目一:交换两个数

我们可通过如下操作实现交换两个数a和b

        int a = -2323;
        int b = 10;
        a = a ^ b;
        b = a ^ b;
        a = a ^ b;
        System.out.println(a);
        System.out.println(b);
图解

但是当我们想通过这种方法来实现交换数组中两个数据时,会出现一点问题

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

如果交换的两个数据是同一个,比如在swap方法的传参中i=j,此时就会出错
出错情况

第一步,自己与自己异或时,arr[0]=0,之后下面的操作,均是让arr[0]=arr[0]^0,因此最后的结果就是0。因为两个下标指向的是同一个空间,故第一步修改arr[i]时,arr[j]的值也改变了,导致了后续的错误。如果不是同一个空间,如

        int b = 10;
        a = a ^ b;
        b = a ^ b;
        a = a ^ b;

结果就不会出错。

题目二:不使用判断语句,返回a和b的最大值

直接看代码

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);
        // 无论任何情况,returnA和returnB只有一个为1
        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);
        // 如果AB符号不一样且A为正数
        // 或者AB符号一样且差为正数
        // returnA=1
        int returnA = diffAB * sa + sameAB * sc;
        // 否则returnB=1
        int returnB = flip(returnA);
        // returnA和returnB仅有一个为1
        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));
    }

}

题目三:找到缺失的数字

先看题目
找到缺失的数字

一个数组的中数字的范围是[0,n],给出了一个长度为n的数组,找到其中缺失的那个数。
本题就可以用到异或运算的性质4

    public static int missingNumber(int[] nums) {
       // [0,n]异或和   数组中数字的异或和     
        int eorAll = 0, eorHas = 0;
        for (int i = 0; i < nums.length; i++) {
            eorAll ^= i;
            eorHas ^= nums[i];
        }
        eorAll ^= nums.length;
        return eorAll ^ eorHas;
    }

题目四:只出现奇数次的数字

只出现奇数次的数字

该方法可以用于数组中某个元素只出现奇数次,其余元素均出现偶数次的情况。
我们可以用到异或的性质2和3,所有元素求异或和,最终结果就是只出现奇数次的元素

    public static int singleNumber(int[] nums) {
        int eor = 0;
        for (int num : nums) {
            eor ^= num;
        }
        return eor;
    }

题目五:只出现奇数次的数字II

只出现奇数次的数字II

该方法可以用于数组中恰好有两个元素只出现奇数次,而其余所有元素均出现偶数次。

首先有一个求二进制数最右边那个1的Brian Kernighan算法,即n&(~n+1)的结果只会留下该二进制数n最右边的那个1,其余为均为0
n&(~n+1)

可以看到( ~ n+1)的结果就是n的相反数,即-n。
然后就是主要算法,基于上面的题目四我们可知,在该题目情况下整个数组元素求异或和为a^b,而异或和一定是一个非0数,因此其最右边的那个1所在的位,a和b的该位就不相同,因此可根据此位,将数组元素划分为此位为0和此位为1的两类,其中一类的异或和就是a,而另一类的异或和就是b。

    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
        int rightOne = eor1 & (-eor1);
        int eor2 = 0;
        for (int num : nums) {
            if ((num & rightOne) == 0) {
                eor2 ^= num;
            }
        }
        return new int[] { eor2, eor1 ^ eor2 };
    }

题目六:出现少于m次的数

出现少于m次的数

该方法可以解决数组中某个数出现次数少于m次,其余元素均出现m次的情况。
首先,若某个元素出现了m次,那么该元素某一位如果为1,那么这位的1就一定出现了m次;如果某一位为0,那么这位的0就一定出现了m次。因此,我们只需要统计数组中所有元素的32位中,每一位1的个数和,如果是m的整数倍,说明出现次数少于m次的数该位就为0;反之则为1。

    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;
    }
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容